用gunicorn
启动一个tornado
服务时报了个错:
1
2
3
4
5
6
7
8
9
| [ERROR] [MainThread] (http1connection:67) Uncaught exception
Traceback (most recent call last):
File "/Users/ferstar/.pyenv/versions/scriber/lib/python3.6/site-packages/tornado/http1connection.py", line 273, in _read_message
delegate.finish()
File "/Users/ferstar/.pyenv/versions/scriber/lib/python3.6/site-packages/tornado/httpserver.py", line 280, in finish
self.request_callback(self.request)
File "/Users/ferstar/.pyenv/versions/scriber/lib/python3.6/site-packages/tornado/wsgi.py", line 114, in __call__
WSGIContainer.environ(request), start_response
TypeError: __call__() takes 2 positional arguments but 3 were given
|
调试发现是这行代码的问题:
1
2
3
4
5
6
7
8
9
10
| # Assume the app is a WSGI callable if its not an
# instance of tornado.web.Application or is an
# instance of tornado.wsgi.WSGIApplication
app = self.wsgi
if tornado.version_info[0] < 6:
if not isinstance(app, tornado.web.Application) or \
isinstance(app, tornado.wsgi.WSGIApplication):
app = WSGIContainer(app)
elif not isinstance(app, WSGIContainer):
app = WSGIContainer(app)
|
我用的 tornado 版本是6.1
, 可以看到, web.Application
被WSGIContainer
包了一层, 实际上tornado
自6.0
以后的版本中有意在剥离WSGI
的支持, 所以比较苟的一个解决方法是退回到6.0
之前的版本, 比如5.1.1
, 即可正常; 然而作为一个有追求的攻城狮, 怎么能够因为一个小小的兼容问题就退版本号呢, 我选择硬肛, 既然gunicorn
自己的tornado worker
不能用, 那就照抄另写一个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
| # This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
# filename: gtornado.py
import copy
import gettext
import logging.config
import os
import sys
from gunicorn.workers.base import Worker
import tornado
import tornado.httpserver
import tornado.web
from tornado.ioloop import IOLoop, PeriodicCallback
class TornadoWorker(Worker):
def handle_exit(self, sig, frame):
if self.alive:
super().handle_exit(sig, frame)
def handle_request(self):
self.nr += 1
if self.alive and self.nr >= self.max_requests:
self.log.info("Autorestarting worker after current request.")
self.alive = False
def watchdog(self):
if self.alive:
self.notify()
if self.ppid != os.getppid():
self.log.info("Parent changed, shutting down: %s", self)
self.alive = False
def heartbeat(self):
if not self.alive:
if self.server_alive:
if hasattr(self, 'server'):
try:
self.server.stop()
except Exception: # pylint: disable=broad-except
pass
self.server_alive = False
else:
for callback in self.callbacks:
callback.stop()
self.ioloop.stop()
def init_process(self):
# IOLoop cannot survive a fork or be shared across processes
# in any way. When multiple processes are being used, each process
# should create its own IOLoop. We should clear current IOLoop
# if exists before os.fork.
IOLoop.clear_current()
super().init_process()
def run(self):
self.ioloop = IOLoop.current()
self.alive = True
self.server_alive = False
self.callbacks = []
self.callbacks.append(PeriodicCallback(self.watchdog, 1000))
self.callbacks.append(PeriodicCallback(self.heartbeat, 1000))
for callback in self.callbacks:
callback.start()
# Assume the app is a WSGI callable if its not an
# instance of tornado.web.Application or is an
# instance of tornado.wsgi.WSGIApplication
app = self.wsgi
# Monkey-patching HTTPConnection.finish to count the
# number of requests being handled by Tornado. This
# will help gunicorn shutdown the worker if max_requests
# is exceeded.
httpserver = sys.modules["tornado.httpserver"]
if hasattr(httpserver, 'HTTPConnection'):
old_connection_finish = httpserver.HTTPConnection.finish
def finish(other):
self.handle_request()
old_connection_finish(other)
httpserver.HTTPConnection.finish = finish
sys.modules["tornado.httpserver"] = httpserver
server_class = tornado.httpserver.HTTPServer
else:
class _HTTPServer(tornado.httpserver.HTTPServer):
def on_close(instance, server_conn): # pylint: disable=no-self-argument
self.handle_request()
super(_HTTPServer, instance).on_close(server_conn)
server_class = _HTTPServer
app_params = {
"max_buffer_size": 200 * 1024 * 1024, # 200MB
"decompress_request": True,
}
if self.cfg.is_ssl:
_ssl_opt = copy.deepcopy(self.cfg.ssl_options)
# tornado refuses initialization if ssl_options contains following
# options
del _ssl_opt["do_handshake_on_connect"]
del _ssl_opt["suppress_ragged_eofs"]
app_params["ssl_options"] = _ssl_opt
server = server_class(app, **app_params)
self.server = server
self.server_alive = True
for socket in self.sockets:
socket.setblocking(0)
server.add_socket(socket)
server.no_keep_alive = self.cfg.keepalive <= 0
server.start(num_processes=1)
self.ioloop.start()
|
主要就是直接丢掉WSGIContainer
, 使用tornado.web.Application
, 然后运行:
1
| gunicorn -k gtornado.TornadoWorker main:app -b 0.0.0.0:8080 --graceful-timeout 120 --timeout 600
|
发送个HUP
信号看看反应, 嗯, 顺利重载
1
2
3
4
5
6
7
8
| [INFO] Starting gunicorn 20.1.0
[INFO] Listening at: http://0.0.0.0:8080 (51702)
[INFO] Using worker: gtornado.TornadoWorker
[INFO] Booting worker with pid: 51756
[INFO] Handling signal: hup # 收到信号
[INFO] Hang up: Master
[INFO] Booting worker with pid: 52640 # 顺利重载
[INFO] Worker exiting (pid: 51756)
|
附上测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # filename: main.py
import asyncio
from tornado.web import Application, RequestHandler
class MainHandler(RequestHandler):
def get(self):
self.write("Hello, world")
class LongPollHandler(RequestHandler):
async def get(self):
lines = ['line 1\n', 'line 2\n']
for line in lines:
self.write(line)
await self.flush()
await asyncio.sleep(0.5)
await self.finish()
app = Application([
(r"/", MainHandler),
(r"/longpoll", LongPollHandler)
])
|
# NOTE: I am not responsible for any expired content.
create@2021-03-23T01:54:07+08:00
update@2021-11-09T11:07:43+08:00
comment@https://github.com/ferstar/blog/issues/39