์ด ์ค๋๋ ๋ฌธ์ 1210 ์์ ๋งํ๋ฏ์ด gunicorn์ ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ์ด ๋๊ธธ ๋ ์ค๋ฅ๋ฅผ ๊ธฐ๋กํ๊ณ ๋ด ํ๊ฒฝ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋ฐ๋น์ GNU/๋ฆฌ๋ ์ค 7.8
nginx
ํ์ด์ฌ3.4
gunicorn(19.8.1)(ํ๋ ์ด์์ ์์ ์ ํฌํจ)
Flask-SocketIO, ํด๋ผ์ด์ธํธ๋ ์น ์์ผ ์ ์ก์ ์ง์ ํฉ๋๋ค.
์ด ์ค๋ฅ ๋ก๊ทธ, ๋ ๊ฐ์ ํด๋ผ์ฐ๋ ๋ ๋ฆฝ ํ๋ก๋์ ์ธ์คํด์ค๊ฐ ๋ชจ๋ ์ง์์ ์ผ๋ก ๋ก๊ทธ๋ฅผ ๊ธฐ๋กํ๋ ๊ฒ์ ์ ์ธํ๊ณ ํด๋ผ์ด์ธํธ๋ฅผ ํฌํจํ์ฌ ๋ชจ๋ ๊ฒ์ด ์ ์๋ํ์ง๋ง Mac์ธ ๊ฐ๋ฐ ์์คํ ์์๋ ์ด๋ฅผ ์ฌํํ ์ ์์ต๋๋ค.
๋ง์ ๋์์ ์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค.
์์ฒญ ์ฒ๋ฆฌ ์ค๋ฅ /socket.io/?EIO=3&transport=websocket
์ญ์ถ์ (๊ฐ์ฅ ์ต๊ทผ ํธ์ถ ๋ง์ง๋ง):
ํ์ผ "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", 56ํ, ํธ๋ค
self.handle_request(listener_name, req, ํด๋ผ์ด์ธํธ, addr)
handle_request์ ํ์ผ "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/workers/async.py", 116ํ
resp.close()
ํ์ผ "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", 409ํ, ๋ซ๊ธฐ
self.send_headers()
ํ์ผ "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", 325ํ, send_headers
tosend = self.default_headers()
default_headers์ ํ์ผ "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", 306ํ
elif self.should_close():
ํ์ผ "/opt/apps/lms/virtualenv/lib/python3.4/site-packages/gunicorn/http/wsgi.py", 229ํ, should_close์ ์์ต๋๋ค.
self.status_code < 200 ๋๋ self.status_code๊ฐ (204, 304)์ธ ๊ฒฝ์ฐ:
AttributeError: 'Response' ๊ฐ์ฒด์ 'status_code' ์์ฑ์ด ์์ต๋๋ค.
๊ทธ๊ฒ์ ์ฌํํ๋ ๊ฐ๋จํ ์๊ฐ ์์ต๋๊น? ๋ํ ๊ฐ๋ฅํ๋ฉด ์ต์ ๋ง์คํฐ๋ก ์๋ํ์ญ์์ค.
์ด์ ์ ํ๋ก๋์ ํ๊ฒฝ๊ณผ ๋์ผํ ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋์ธ ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ์์ ์ฌ๋ฌ ๋ฒ ์๋ํ์ง๋ง ์ฌํํ ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ฒ์ 19.9.0 ๋ฆด๋ฆฌ์ค ๋ก๊ทธ๋ฅผ ํ์ธํ์ต๋๋ค.๊ด๋ จ๋ ํญ๋ชฉ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.
์ด ์ค๋ฅ ๋ก๊ทธ๋ฅผ ๋ณด๊ณ ์๋ก์ด ๊ฒ์ ์ฐพ์ผ๋ฉด ์ฌ๊ธฐ์ ๊ฒ์ํ๊ฒ ์ต๋๋ค.
ํนํ ํด๋ผ์ด์ธํธ์์ ์น ์์ผ ํ๋กํ ์ฝ๋ก์ ๋ชจ๋ ์ฐ๊ฒฐ์ ๊ฐ์ ํ ๋ ์ด ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๋ด ์ค์ ์ BoWuGit๊ณผ ๋์ผํฉ๋๋ค. ์
๊ทธ๋ ์ด๋ ์ ์ ํด๋ง ํ๋กํ ์ฝ์ ํ์ฉํ๋ฉด ํ์๋์ง ์์ง๋ง ๋ค๋ฅธ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค.
`
[์ค๋ฅ] ์์ฒญ ์ฒ๋ฆฌ ์ค๋ฅ /socket.io/?EIO=3&transport=polling&t=MPHUoV&sid=cd64be7c940e474d8728b114c3fb9bbe
์ญ์ถ์ (๊ฐ์ฅ ์ต๊ทผ ํธ์ถ ๋ง์ง๋ง):
ํ์ผ "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", 56ํ, ํธ๋ค
self.handle_request(listener_name, req, ํด๋ผ์ด์ธํธ, addr)
handle_request์ ํ์ผ "/usr/local/lib/python3.6/site-packages/gunicorn/workers/async.py", 107ํ
respiter = self.wsgi(ํ๊ฒฝ, resp.start_response)
ํ์ผ "/usr/local/lib/python3.6/site-packages/flask/app.py", 1994ํ, __call__
self.wsgi_app(ํ๊ฒฝ, start_response) ๋ฐํ
ํ์ผ "/usr/local/lib/python3.6/site-packages/flask_socketio/__init__.py", 43ํ, __call__
์์_์๋ต)
ํ์ผ "/usr/local/lib/python3.6/site-packages/engineio/middleware.sys"
py", 47ํ, __call__
๋ฐํ self.engineio_app.handle_request(ํ๊ฒฝ, ์์_์๋ต)
handle_request์ ํ์ผ "/usr/local/lib/python3.6/site-packages/socketio/server.py", 360ํ
๋ฐํ self.eio.handle_request(ํ๊ฒฝ, ์์_์๋ต)
handle_request์ ํ์ผ "/usr/local/lib/python3.6/site-packages/engineio/server.py", 279ํ
์์ผ = self._get_socket(sid)
ํ์ผ "/usr/local/lib/python3.6/site-packages/engineio/server.py", 439ํ, _get_socket
raise KeyError('์ธ์
์ฐ๊ฒฐ์ด ๋๊ฒผ์ต๋๋ค')
`
๊ทธ๋ฌ๋ ์น ์์ผ์ ์ฐ๊ฒฐ์ ๊ฐ์ ์คํํ๊ธฐ ๋๋ฌธ์์ด ์ค๋ฅ๊ฐ ๋ค์ ํ์๋์ง ์์๊ธฐ ๋๋ฌธ์ ์๋ก ๊ด๋ จ์ด ์๋ค๊ณ ์๊ฐํฉ๋๋ค.
eventlet 0.24.1์ ์ฌ์ฉํ ๋ gunicorn 19.9.0 ๋ฐ Flask-socketIO 3.0.2์์๋ ์ด ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
AttributeError: 'Response' ๊ฐ์ฒด์ 'status_code' ์์ฑ์ด ์์ต๋๋ค.
๋ํ ๋ค์ ์๊ตฌ ์ฌํญ๊ณผ ํจ๊ป ์ด ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
Flask==1.0.2
gunicorn==19.5.0
python-socketio==2.0.0
eventlet==0.24.1
์ด๋ฆฐ ์์ผ ์ฐ๊ฒฐ์ด ์๋ ์น ๋ธ๋ผ์ฐ์ ๋ฅผ ๋ซ์ ๋ ์ค๋ฅ ๋ฉ์์ง:
Error handling request /socket.io/?EIO=3&transport=websocket&sid=d43ec0ae0bb946debc51f1ca2e5b8a94
Traceback (most recent call last):
File "/usr/lib/python2.7/dist-packages/gunicorn/workers/async.py", line 52, in handle
self.handle_request(listener_name, req, client, addr)
File "/usr/lib/python2.7/dist-packages/gunicorn/workers/async.py", line 114, in handle_request
resp.close()
File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 403, in close
self.send_headers()
File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 319, in send_headers
tosend = self.default_headers()
File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 300, in default_headers
elif self.should_close():
File "/usr/lib/python2.7/dist-packages/gunicorn/http/wsgi.py", line 233, in should_close
if self.status_code < 200 or self.status_code in (204, 304):
AttributeError: 'Response' object has no attribute 'status_code'
์ด ๋ฌธ์ ๋ python-engineio ์ต์ ๋ฒ์ ์์ ์์ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
python-engineio ์ต์ ๋ฒ์ (2.3.2)์ผ๋ก ํ ์คํธํ์ง๋ง ์ฌ์ ํ ์๋ํ์ง ์์ต๋๋ค.
์ด ๋ฌธ์ ์ ๋ํ ์์์ด ์์ต๋๊น? Sentry-python์ ์ฌ์ฉํ ๋ ๋์ผํ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค.
๋๋ ๊ฐ์ ๋ฌธ์ ๊ฐ์๋ค
์ด๋ฒคํธ๋ : 0.25.1
ํ๋ผ์คํฌ ์์ผ: 4.2.1
์ด๊ธฐ์ฝ: 19.9.0
๊ทธ๊ฒ์ ์ฌํํ๋ ๋ฐฉ๋ฒ? cna ๋น์ ์ ๊ฐ๋จํ ์๋ฅผ ์ ๊ณตํฉ๋๊น?
๋๋ ๋ํ ๊ทธ๊ฒ์ ์ฌํํ๋ ๋ฐฉ๋ฒ์ ์ ๋ชจ๋ฅด๊ฒ ์ง๋ง ๋ด gunicorn ์ฑ์์ ํ์ด์ง๋ฅผ ์๋ก ๊ณ ์น ๋ ์์ฃผ ๋ฐ์ํฉ๋๋ค.
๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๊ณ ๋ด ํ๊ฒฝ์ @eazow ์ ๋์ผํ์ง๋ง gunicorn == 20.0.4์
๋๋ค.
๋ฒ๊ทธ ์ถ์ ์ ์ํด ์ผํธ๋ฆฌ๋ฅผ ์ค์นํ ํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ ๊ฐ์ต๋๋ค.
๋ฌธ์ ๋ ๋ค์์ ํตํด ์ฌํํ ์ ์์ต๋๋ค.
ํฅ๋ฏธ๋กญ๊ฒ๋ ์ ํ์ด์ง๋ฅผ ์ด๋ฉด ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ต๋๋ค. ์ด์ ๋ ํ์คํ์ง ์์ต๋๋ค. ๊ฐ์ฌ ํด์!
@cowbonlin ๊ณผ ๊ฐ์ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๊ฐ์ gunicorn ๋ฒ์ ๋.
Sentry๋ฅผ ์ค์นํ ํ ์์ฒญ๋ ์์ ์ด ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ฐ๋ฆฌ๊ฐ ์ผํธ๋ฆฌ ์ด์ ์ ์ค๋ฅ๋ฅผ ์ถ์ ํ์ง ์์๊ธฐ ๋๋ฌธ์ ์ด๊ฒ์ด ํญ์ ๋ฐ์ํ๋์ง ์ฌ๋ถ๋ฅผ ๋งํ๊ธฐ๋ ์ด๋ ต์ต๋๋ค.
์ฐ๋ฆฌ ์๋ฒ์ ์ค์ ๊ธฐ๋ฅ์ ์ํฅ์ ๋ฏธ์น์ง ์๋ ๊ฒ ๊ฐ์ง๋ง ์ด๊ฒ์ ์์ฒญ๋ ์คํธ์ผ ๋ฟ์ ๋๋ค.
์ฐ๋ฆฌ๋ ๊ฐ์ ์ผ์ ๊ฒช๊ณ ์์ต๋๋ค. Sentry๊ฐ ์ค์น๋์์ง๋ง ๋นํ์ฑํ๋์์ต๋๋ค. ์ด๋ค ์์ด๋์ด?
์ผํธ๋ฆฌ๊ฐ ์ค์น๋ ๊ฒ๊ณผ ๋์ผํ ๋ฌธ์ ์ ๋๋ค.
์ผํธ๋ฆฌ(๋นํ์ฑํ ์ฌ๋ถ) ์์ด ์ฌ์์ฐํ๋ ์๊ฐ ์์ต๋๊น?
๋ํ ๋ค์์คํ์ด์ค ๋์ ์๋์ผ๋ก /api๋ฅผ ๋๋ ์ต๋๋ค.
๋ํ ๋ค์์คํ์ด์ค ๋์ ์๋์ผ๋ก /api๋ฅผ ๋๋ ์ต๋๋ค.
๋ฌด์จ ๋ป์ธ๊ฐ์ ? ์ด ์ผํธ๋ฆฌ์ ๊ด๋ จ์ด ์์ต๋๊น?
๋ํ ๋ค์์คํ์ด์ค ๋์ ์๋์ผ๋ก /api๋ฅผ ๋๋ ์ต๋๋ค.
๋ฌด์จ ๋ป์ธ๊ฐ์ ? ์ด ์ผํธ๋ฆฌ์ ๊ด๋ จ์ด ์์ต๋๊น?
์๋์, ์ด๊ฒ์ socket.io ๋ค์์คํ์ด์ค์ ๊ด๋ จ์ด ์์ต๋๋ค. ์ ๊ฑฐ๋ฅผ ์๋ํ๋๋ฐ ์ ๊ฑฐํ ํ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. ๊ทธ๋ฌ๋ gunicorn ๋๋ nginx๊ฐ ์๋ ๋ก์ปฌ ์ปดํจํฐ์์ ์ด ๋ค๋ฅธ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ด๋ ๊ด๋ จ์ด ์์ ์ ์์ต๋๋ค.
๋ด ์๊ตฌ ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
sentry_sdk == 0.14.3
Flask_SocketIO == 4.2.1
eventlet == 0.25.1
์ด๊ฒ์ ์๋ฒ ์ธก์ flask-socketio ์ฝ๋์ ๋๋ค.
socketio = SocketIO(engineio_logger=True, logger=True, debug=True, cors_allowed_origins="*", path='/socket.io')
...
socketio.init_app(app, async_mode="eventlet")
๊ทธ๋ฆฌ๊ณ ์ด๊ฒ์ ํด๋ผ์ด์ธํธ ์ธก์ React ์์ผ io ์ฝ๋์ ๋๋ค.
this.socket = io.connect(`http://localhost:5000?info=${someInfo}`, {
transports: ['websocket', 'polling'] // an attempt to keep polling as a fallback but start on websockets
});
๋์์ด ๋๋์ง ์๋ ค์ฃผ์ธ์. Ubuntu์์ ์ค๋ฅ๋ ์์ ๊ฒ๊ณผ ๊ฐ๊ณ Windows์์ ๋ก์ปฌ๋ก ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
```์ถ์ (๊ฐ์ฅ ์ต๊ทผ ํธ์ถ ๋ง์ง๋ง):
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 599ํ, handle_one_response
์ฐ๊ธฐ(b'')
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ์ค 491, ์ฐ๊ธฐ
๋ฐ์ AssertionError("start_response() ์ ์ write()")
AssertionError: start_response() ์ ์ write()
์์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ๋์ ๋ค๋ฅธ ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ญ์ถ์ (๊ฐ์ฅ ์ต๊ทผ ํธ์ถ ๋ง์ง๋ง):
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 357ํ, __init__
self.handle()
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 390ํ, ํธ๋ค
self.handle_one_request()
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 466ํ, handle_one_request
self.handle_one_response()
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 609ํ, handle_one_response
์ฐ๊ธฐ(err_body)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", ์ค 538, ์ฐ๊ธฐ
wfile.flush()
ํ์ผ "C:\ProgramData\Anaconda3\lib\socket.py", 607ํ, ์ฐ๊ธฐ ์ค
๋ฐํ self._sock.send(b)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", 397ํ, ๋ณด๋ด๊ธฐ
๋ฐํ self._send_loop(self.fd.send, ๋ฐ์ดํฐ, ํ๋๊ทธ)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", 384ํ, _send_loop
send_method(๋ฐ์ดํฐ, *args) ๋ฐํ
ConnectionAbortedError: [WinError 10053] ํธ์คํธ ์ปดํจํฐ์ ์ํํธ์จ์ด์ ์ํด ์ค์ ๋ ์ฐ๊ฒฐ์ด ์ค๋จ๋์์ต๋๋ค.
์์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ ๋์ ๋ค๋ฅธ ์์ธ๊ฐ ๋ฐ์ํ์ต๋๋ค.
์ญ์ถ์ (๊ฐ์ฅ ์ต๊ทผ ํธ์ถ ๋ง์ง๋ง):
fire_timers์ ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\hub.py", ๋ผ์ธ 461
์๊ฐ์ ๋
ธ๋์()
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\hubs\timer.py", 59ํ, __call__
cb( ์ธ์, * kw)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\semaphore.py", 147ํ, _do_acquire
waiter.switch()
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenthread.py", ๋ผ์ธ 221, ๋ฉ์ธ
๊ฒฐ๊ณผ = ํจ์( ์ธ์, * kwargs)
"C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py" ํ์ผ, 818ํ, process_request
proto.__init__(conn_state, self)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 359ํ, __init__
self.finish()
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\wsgi.py", 732ํ, ์๋ฃ
BaseHTTPServer.BaseHTTPRequestHandler.finish(์์ฒด)
ํ์ผ "C:\ProgramData\Anaconda3\lib\socketserver.py", 784ํ, ์๋ฃ
self.wfile.close()
ํ์ผ "C:\ProgramData\Anaconda3\lib\socket.py", 607ํ, ์ฐ๊ธฐ ์ค
๋ฐํ self._sock.send(b)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", 397ํ, ๋ณด๋ด๊ธฐ
return self._send_loop(self.fd.send, ๋ฐ์ดํฐ, ํ๋๊ทธ)
ํ์ผ "C:\ProgramData\Anaconda3\lib\site-packages\eventlet\greenio\base.py", 384ํ, _send_loop
send_method(๋ฐ์ดํฐ, *์ธ์) ๋ฐํ
ConnectionAbortedError: [WinError 10053] ํธ์คํธ ์ปดํจํฐ์ ์ํํธ์จ์ด์ ์ํด ์ค์ ๋ ์ฐ๊ฒฐ์ด ์ค๋จ๋์์ต๋๋ค```
์ผํธ๋ฆฌ๊ฐ ์์ ํ ๋นํ์ฑํ๋๋ฉด ์ด ์ค๋ฅ๊ฐ ์ฌ๋ผ์ง๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. gunicorn์ด ์ด๊ฒ์ ์ฒ๋ฆฌํ ๋งํผ ์ถฉ๋ถํ ๊ฒฌ๊ณ ํ๋ค๋ฉด ์ข์ ๊ฒ์ ๋๋ค.
๋ฒํ @benoitc
์ผํธ๋ฆฌ๊ฐ ์์ ํ ๋นํ์ฑํ๋๋ฉด ์ด ์ค๋ฅ๊ฐ ์ฌ๋ผ์ง๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค. gunicorn์ด ์ด๊ฒ์ ์ฒ๋ฆฌํ ๋งํผ ์ถฉ๋ถํ ๊ฒฌ๊ณ ํ๋ค๋ฉด ์ข์ ๊ฒ์ ๋๋ค.
Sentry์ FlaskIntegration
๋ฅผ ๋นํ์ฑํํด๋ ์ค๋ฅ๊ฐ ์ฌ๋ผ์ง๋๋ค.
๋น์ทํ ํ๋์ ๋ณด์ ๋๋ค. ํ๋ก๋์ ์์ New Relic์ ์ฌ์ฉํ๋ฉด flask-socketio์์ ์ด ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. ๊ฐ๋ฐ ์์๋ flask-socketio๊ฐ ์ด๊ธฐํ๋๊ธฐ ์ ์ werkzeug ๋๋ฒ๊ฑฐ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ก๋ํด์ผ ํฉ๋๋ค(๋ฐ๋ผ์ engineio์ wsgi ์ฑ์๋ ์ ์ฉ๋์ง ์์). ๋ฌธ์ ๋ ํ๋ก๋์ ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ์ ๋ง ์ํ์ง ์๋๋ค๋ ๊ฒ์ ๋๋ค.
gunicorn ๊ตฌ์ฑ์ post_request์์ ์๋ต์ ๋์ฒดํ ์ ์์ง๋ง resp.status_code์ ์ํ ์ฝ๋๋ฅผ ๊ฐ์ ๋ก ์ ์ฉํ๋ ค๊ณ ํ์ต๋๋ค. ๊ทธ๋๋ ๊ฑธ๋ฆฌ์ง ์์์ต๋๋ค.
์ด ์ค๋ฅ๋ Sentry์ FlaskIntegration์ Gunicorn ๋ฐ Flask-SocketIO์ ํจ๊ป ์ฌ์ฉํ์ฌ ์ฌํํ ์ ์์ต๋๋ค. ๋นจ๋ฆฌ ํด๊ฒฐ ๊ฐ๋ฅํ ๊น์?
@Canicio ์ฐ๋ฆฌ๋ ์ค๋ฅ๋ฅผ ์ ๊ฑฐํ๋ ค๊ณ ์๋ํ์ผ๋ฉฐ ํตํฉ์ ๋นํ์ฑํํ ํ์๋ ์ค๋ฅ๊ฐ ์ง์๋๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๋๊ตฌ๋ ์ง @benoitc ์ ๋ํ ๊ณต์ ๊ฐ๋ฅํ ์ฝ๋/์ต์ํ์ ์๊ฐ ์์ต๋๊น?
ํ์ ํ๋:
import sentry_sdk
from flask import Flask
from flask_socketio import SocketIO
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn="https://[email protected]/0",
integrations=[FlaskIntegration()]
)
app = Flask(__name__)
socketio = SocketIO(app)
@app.route('/')
def index():
return '''
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io()
</script>
์๊ตฌ ์ฌํญ:
flask
sentry-sdk[flask]
flask-socketio
eventlet
์์ gunicorn ๊ตฌ์ฑ:
bind = '[::]:4444'
worker_class = 'eventlet'
accesslog = '-'
/
๋ก๋ ์ ์น ์์ผ์ ์ฐ๊ฒฐ๋ฉ๋๋ค. ์น ์์ผ ์ฐ๊ฒฐ ํด์ ์(์: ๋ค๋ฅธ ๊ณณ์ผ๋ก ์ด๋, ์๋ก ๊ณ ์นจ) ๋ค์๊ณผ ๊ฐ์ ์์ธ๊ฐ ์์ฑ๋ฉ๋๋ค.
[2020-09-23 07:24:49 +0000] [16303] [ERROR] Error handling request /socket.io/?EIO=3&transport=websocket&sid=29f4c1adfac343d6bc6db56acf8fd0ee
Traceback (most recent call last):
File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/workers/base_async.py", line 55, in handle
self.handle_request(listener_name, req, client, addr)
File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/workers/base_async.py", line 115, in handle_request
resp.close()
File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 402, in close
self.send_headers()
File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 318, in send_headers
tosend = self.default_headers()
File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 299, in default_headers
elif self.should_close():
File "/home/ziddey/projects/sentry/venv_sentry/lib/python3.8/site-packages/gunicorn/http/wsgi.py", line 219, in should_close
if self.status_code < 200 or self.status_code in (204, 304):
AttributeError: 'Response' object has no attribute 'status_code'
2001:470:1f07:7eb:9dd4:254c:35d7:236c - - [23/Sep/2020:07:24:49 +0000] "GET /socket.io/?EIO=3&transport=websocket&sid=29f4c1adfac343d6bc6db56acf8fd0ee HTTP/1.1" 500 0 "-" "-"
์ฐธ๊ณ : ์ ๋ ์ค์ ๋ก Sentry๋ฅผ ์ฌ์ฉํ ์ ์ด ์์ต๋๋ค. ์ด๊ฒ์ ์ผํธ๋ฆฌ ์์ ํ์ด์ง์์ ๊ฐ์ ธ์จ ๊ฒ์
๋๋ค. dsn
์์ ๋ ์ฐ๋ฆฌ ํ
์คํธ์์ ์ ์๋ํฉ๋๋ค.
integrations=[FlaskIntegration()]
์ ์ฃผ์์ ๋ฌ๋ฉด ์ค๋ฅ๊ฐ ์ ๊ฑฐ๋ฉ๋๋ค(๋ฌผ๋ก ํจ๊ณผ์ ์ผ๋ก ๋ณด์ด๋ฅผ ๋นํ์ฑํํจ).
๊ทธ๋งํ ๊ฐ์น๊ฐ ์๊ธฐ ๋๋ฌธ์ ์ค๋ฅ ์์ด eventlet ๋์ gevent-websocket์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ๋ชจ๋ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ข์, ์ข ๋์์ด. Sentry/newrelic์ด ์๋ต์ ๋ํํ๋ ๊ฒ ๊ฐ์ต๋๋ค. Sentry๊ฐ ์์ผ๋ฉด ์์ํ ๋๋ก <eventlet.wsgi._AlreadyHandled object at 0x7fd0f5b1c0d0>
๋ฅผ ์ป๊ณ gunicorn์ EventletWorker.is_already_handled()๊ฐ ๋ฐ๋ณต์ ์ค์งํฉ๋๋ค.
๊ทธ๋ฌ๋ Sentry๋ฅผ ์ฌ์ฉํ๋ฉด ๋์ <sentry_sdk.integrations.wsgi._ScopedResponse object at 0x7f30155a5100>
์ ๊ฐ์ด ๋์ด ํ์ธ์ ์คํจํฉ๋๋ค.
๋์ , ์ฐ๋ฆฌ๋ ๊ทธ๊ฒ์ด ๋น์ด ์๋์ง ํ์ธํ๊ธฐ ์ํด respiter๋ฅผ ์ฟ๋ณผ ์ ์์ต๋๋ค. ๋ด์ผ์ ๋ ์ง์ผ๋ด์ผ ํฉ๋๋ค.
์ข์ต๋๋ค. ์ ๊ฐ ์๊ฐํด๋ธ ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
eventlet_fix.py:
์๋ ํธ์ง ์ฐธ์กฐ
๊ทธ๋ฆฌ๊ณ ๋ด gunicorn config.py: worker_class = 'eventlet_fix.EventletWorker
.
๋ฌธ์ ๋ sentry/newrelic์ด ์๋ต์ ๋ํํ๋ฏ๋ก eventlet์ ALREADY_HANDLED
์ ๋ํด ๊ฐ๋จํ ํ์ธํ ์ ์๋ค๋ ๊ฒ์
๋๋ค. ์ด๋ฏธ ์ฒ๋ฆฌ๋ ์์ฒญ์ ํน์ฑ์ gunicorn์ start_response
๊ฐ ํธ์ถ๋์ง ์๋๋ค๋ ๊ฒ์ด๋ฏ๋ก ๋์ ์๋ต ์ํ๊ฐ ์๋์ง ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ๋์ wsgi ํธ์ถ์ ๊ฐ๋ก์ฑ์ด ์๋ต ์ํ๋ฅผ ํ์ธํ๊ณ ํ์์ ๋ฐ๋ผ ์๋ต ๊ฐ์ ํดํนํ์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์์ฒญ์ด ์ฌ์ ํ gunicorn์ ์ํด ๊ธฐ๋ก๋ ์ ์์ต๋๋ค. ๋์ ์๋ ๋์์ ์ ์งํ๋ ค๋ฉด StopIteration
๋ฅผ ๋์ ์ฌ๋ฆด ์ ์์ต๋๋ค.
์ํ๋ฅผ 101๋ก ํดํนํ๋ ๊ฒ์ ์ฌ๊ธฐ์์์ ์ฌ์ฉ ์ฌ๋ก(flask-socketio websocket)์ ์ ํฉํ์ง๋ง, ๊ทธ๋ ์ง ์์ผ๋ฉด headers_sent
๋ฐ should_close
๊ฐ ๊ฐ์ ๋ก True๋ก ์ง์ ๋๋ฏ๋ก None์ผ๋ก ๋๋ ๊ฒ๋ ๊ด์ฐฎ์ต๋๋ค.
๋ค์ ๋งํ์ง๋ง, status
๊ฐ ์ค์ ๋์ง ์์ ๊ฒฝ์ฐ start_response
๊ฐ ํธ์ถ๋์ง ์์์ผ๋ฏ๋ก ์์ฒญ์ด ์ธ๋ถ์ ์ผ๋ก "์ด๋ฏธ ์ฒ๋ฆฌ"๋์ด ์์ด์ผ ํ๋ค๊ณ ๊ฐ์ ํฉ๋๋ค.
ํธ์ง : ์ข์ง ์์ต๋๋ค. ์ฌํ๊ฐํ ํ์๊ฐ ์์ ๊ฒ์
๋๋ค. ์์ฒญ์ ์ํํ๋ ๋ฐ ์๊ฐ์ด ๊ฑธ๋ฆฌ๋ฉด resp.status
start_response
๊ฐ ํธ์ถ๋์ง ์์ต๋๋ค.
edit2: ํดํน๋ ์๋ต ๋ฐ๋ณต์๊ฐ ์๋ ์์ ๋ ๋ฒ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
from functools import wraps
from gunicorn.workers.geventlet import EventletWorker as _EventletWorker
class HackedResponse:
def __init__(self, respiter, resp):
self.respiter = iter(respiter)
self.resp = resp
if hasattr(respiter, "close"):
self.close = respiter.close
def __iter__(self):
return self
def __next__(self):
try:
return next(self.respiter)
except StopIteration:
if not self.resp.status:
self.resp.status = "101" # logger derives status code from status instead of using status_code
self.resp.status_code = 101 # not actually needed since headers_sent/force_close result in status_code not being checked anymore
self.resp.headers_sent = True
self.resp.force_close()
raise
def wsgi_decorator(wsgi):
@wraps(wsgi)
def wrapper(environ, start_response):
respiter = wsgi(environ, start_response)
resp = start_response.__self__
return HackedResponse(respiter, resp)
return wrapper
class EventletWorker(_EventletWorker):
def load_wsgi(self):
super().load_wsgi()
self.wsgi = wsgi_decorator(self.wsgi)
๋ถ๋ช
ํ ์ด๊ฒ์ ๋จ์ง ์์ญ์ด ํจ์น์
๋๋ค. ์ค์ ์์ ์ ์ ์ฌ์ ์ผ๋ก base_async.py์ handle_request
์ ํฌํจ๋ ์ ์์ต๋๋ค. ํต์ฌ์ resp.status
( start_response
ํธ์ถ๋จ) ๋๋ resp.headers_sent
์ ํ์ธํ์ฌ respiter
๋ฅผ ๋ฐ๋ณตํ ํ start_response
๊ฐ ํธ์ถ๋์๋์ง (๊ฐ์ ์ ์ผ๋ก) ํ์ธํ๋ ๊ฒ์ผ ์ ์์ต๋๋ค. resp.headers_sent
(์์ฒญ์ ์ค์ ๋ก ์๋ตํ๋ค๋ ํ์ธ).
@benoitc
@ziddey ๊ฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ๋ฐฉ๋ฒ์ ์ฐพ์์ต๋๋ค.
@ziddey ๊ทํ์ ์์ ๋ํ ๋น ๋ฅธ ์ง๋ฌธ(์ ๋ ์ผํธ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์).
@benoitc ๋ ํ์ฌ ํ ์คํธํ ์ ์์ง๋ง https://github.com/benoitc/gunicorn/issues/1852#issuecomment -697189261 ๋ฐ https://github.com/benoitc/gunicorn/blob/4ae2a05c37b332773957f90 ์์ ์ญ์ถ์ ์ ๋ณด๊ณ ์์ต๋๋ค.
์ผ๋ฐ์ ์ผ๋ก is_already_handled
๋ True๋ฅผ ๋ฐํํ๊ณ ์ฌ๊ธฐ์ ๋๋ฉ๋๋ค.
๊ทธ๋ฌ๋ ์๋ต์ด ๋ํ๋๊ธฐ ๋๋ฌธ์ ํด๋น ๋ฐฉ๋ฒ์ ์๋ํ์ง ์์ต๋๋ค. ๋์ ์คํ์ด ์งํ๋๊ณ 115ํ์์ โโ์คํจํฉ๋๋ค. resp.close()
ํค๋ ์ ์ก์ ์๋ํ์ง๋ง start_response
๊ฐ ํธ์ถ๋์ง ์์์ผ๋ฏ๋ก ์ํ ์ฝ๋๊ฐ ์์ต๋๋ค. ๊ทธ๋ ๊ฒ ๋๋๋ผ๋ ๊ฒฐ๊ตญ ๋ถ๋ช
ํ ์คํจํ ๊ฒ์
๋๋ค.
๊ทธ ๊ฒฐ๊ณผ handle_error
์ ์ํด ๋ค์ ๋ฐ์ํ๊ณ ์ฒ๋ฆฌ๋๋ AttributeError ๊ฐ ๋ฐ์ํฉ๋๋ค. ์์ฒญ์ ์ด๋ฏธ ์ธ๋ถ์์ ์ฒ๋ฆฌ๋์์ผ๋ฏ๋ก ์ฌ๊ธฐ์๋ ๋ก๊ทธ ์คํธ ์ธ์๋ ์๋ฌด๋ฐ ํด๊ฐ ์์ต๋๋ค.
๋๋ Sentry์ ๋ํด ๋๋ฌด ๋ง์ด ๋งํ ์ ์์ต๋๋ค. ์ ๋ ์ฌ์ฉํ์ง ์์ต๋๋ค.
๊ทธ๋ฌ๋ ํ ๊ฐ์ง ์ธ๋ถ ์ฌํญ์ ํ์ฌ ์ด๋ฏธ ์ฒ๋ฆฌ๋ ๋ฉ์ปค๋์ฆ์ผ๋ก ์ธํด ์ก์ธ์ค ๋ก๊น
์ด ๋ฐ์ํ์ง ์๋๋ค๋ ๊ฒ์
๋๋ค. ์ธ๋ถ์์ ์ด๋ป๊ฒ ์ฒ๋ฆฌ๋์๋์ง ์ ์ ์๋ ๋ฐฉ๋ฒ์ด ์๊ธฐ ๋๋ฌธ์ ์ด๊ฒ์ด ๊ธฐ์ ์ ์ผ๋ก ์๋ฏธ๊ฐ ์๋ค๊ณ ์๊ฐํฉ๋๋ค. ํดํน๋ ์๋ต์์ ์ํ ์ฝ๋๋ฅผ 101๋ก ์ค์ ํ๊ณ headers_sent
๋ฅผ True๋ก ์ค์ ํ์ฌ ํธ๋ค๋ฌ๊ฐ ๊ณ์ ์งํ๋๊ณ ์์ฒญ์ ๋ํ ์ก์ธ์ค๊ฐ ๊ณ์ ๊ธฐ๋ก๋ฉ๋๋ค.
resp.status
๋ฅผ ํ์ธํ๋ ๊ฒ์ start_response
๊ฐ ํธ์ถ๋์๋์ง ํ์ธํ๊ธฐ ์ํ ์ต์ข
ํ
์คํธ์
๋๋ค.
@benoitc ๊ฐ ์ด๊ฒ์ ๋ค์ ๋ฐฉ๋ฌธํฉ๋๋ค. ์์ฒญ์ด ์ด๋ฏธ ์ฒ๋ฆฌ๋์๋ค๋ ๊ฒฐ๋ก ์ ๋ด๋ฆฌ๊ธฐ ์ํด environ['gunicorn.socket']
๋์ ๊ธฐ๋ณธ ๊ฐ์ฒด์ ๋ํ ์ผ์ข
์ ํ๋ก์๊ฐ ๋ ์ ์์ต๋๋ค. ๊ทธ๋ ๊ฒ ํ๋ฉด ์์ผ์ ์ง์ ์ก์ธ์คํ ๋ ๊ธฐ๋ก๋ ์ ์๊ณ (์: eventlet์ ๋ํด get_socket()
๋ํ) is_already_handled
์ ๊ฐ์ ์ฉ๋๋ก ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
์ก์ธ์ค ๋ก๊น ์ด ํ์ํ ๊ฒฝ์ฐ์๋ ์ฌ์ ํ ์๋ต ์ํ๋ฅผ ํดํนํด์ผ ํฉ๋๋ค.
๊ฐ์ฅ ์ ์ฉํ ๋๊ธ
๋ฒํ @benoitc