生产环境下如何优雅地重启 Tornado
2012 12 17 01:52 AM 18815次查看
在最近的实践中,我发现这样会有些弊端,所以便有了本文。
当然,这些仍然只是我个人的探索而已,并不保证是最佳实践。
首先说下为什么我不再使用 tornado.process.fork_processes() 方法。
网站在上线后,难免会遇到需求更改或修复 bug 的时候,这就免不了要重启 Tornado 进程了。如果使用上述方法的话,会有一个主进程和多个子进程需要 kill,然后再重新运行。简单来说,代码如下:
killall python
nohup python myapp.py >> log/myapp.log &
如果除了 Tornado,还有其他 Python 进程的话,就更麻烦了,需要精确地找到这些进程的 pid,再分别干掉。于是总觉得不太优雅。后来和知乎的李申申聊天时,问了他这个问题,他推荐我使用 Supervisor。
这玩意折腾了我半天,大致配出这样一个玩意:
[unix_http_server]
file = /tmp/supervisor.sock
[supervisord]
logfile = %(here)s/log/supervisord.log
pidfile = /tmp/supervisord.pid
directory = %(here)s
[supervisorctl]
serverurl = unix:///tmp/supervisor.sock
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[program:myapp]
command = python myapp.py
directory = %(here)s
stdout_logfile = %(here)s/log/myapp.log
stderr_logfile = %(here)s/log/myapp_err.log
然后运行这条命令,就能启动服务了:supervisord -c supervisord.conf
接着坑爹的事发生了,本以为这样能重启的:supervisorctl -c supervisord.conf restart myapp
结果却出错了,Supervisor 认为没有启动成功,但其实已经重启好了。研究了一番才知道,tornado.process.fork_processes() 方法产生的子进程并不会随主进程的退出而退出,而 Supervisor 当然是不知道这些子进程的。(顺带一提,Supervisor 也不能管理守护进程。)
如此一来,如果主进程有什么异常,或者被 kill 掉了,子进程就变成僵尸进程了,确实很有问题。
思索了一番,还是决定自己创建多个进程,分布在多个端口,然后由 nginx 来反向代理到 80 端口。
其中 nginx 的部分配置如下:
http {
upstream myapps {
server 127.0.0.1:6666;
server 127.0.0.1:6667;
server 127.0.0.1:6668;
server 127.0.0.1:6669;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://myapps;
}
}
}
而 supervisord.conf 则改成这样:[program:myapp]
command = python myapp.py 666%(process_num)d
process_name = %(program_name)s%(process_num)d
numprocs = 4
numprocs_start = 6
directory = %(here)s
stdout_logfile = %(here)s/log/myapp.log
stderr_logfile = %(here)s/log/myapp_err.log
现在 myapp.py 需要接收一个端口参数了,且不再 fork 子进程,代码我就略过了。此外,如果端口号末位从 0 开始增长的话,是不需要设置 numprocs_start 的,这里只是个人喜好而已。
重启的命令则改成了这样:
supervisorctl -c supervisord.conf restart myapp:*
可是在使用过程中发现,每次重启都会导致网站有大约 10 秒无法访问,这显然不够好。
于是又写了个脚本,依次重启各个进程:
for i in {6..9}
do supervisorctl -c supervisord.conf restart myapp:myapp$i
done
nginx -s reload
这样基本任何时候网站都是可访问的,不过在重启的过程中,有些没处理完的请求可能会被直接中断掉。
搜索了一番后,找到了这两篇文章可供参考:《Proper way to stop a Tornado》和《Tornado server graceful stop》。
简单来说,就是捕捉 TERM 和 INT 信号,使 Tornado 在退出前先停止接收新请求(由 nginx 分发到其他端口),再尝试处理未完成的回调,最后才退出:
def sig_handler(sig, frame):
logging.warning('Caught signal: %s', sig)
tornado.ioloop.IOLoop.instance().add_callback(shutdown)
def shutdown():
logging.info('Stopping http server')
server.stop() # 不接收新的 HTTP 请求
logging.info('Will shutdown in %s seconds ...', settings.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN)
io_loop = tornado.ioloop.IOLoop.instance()
deadline = time.time() + settings.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN
def stop_loop():
now = time.time()
if now < deadline and (io_loop._callbacks or io_loop._timeouts):
io_loop.add_timeout(now + 1, stop_loop)
else:
io_loop.stop() # 处理完现有的 callback 和 timeout 后,可以跳出 io_loop.start() 里的循环
logging.info('Shutdown')
stop_loop()
if __name__ == '__main__':
port = int(sys.argv[1])
if settings.IPV4_ONLY:
import socket
sockets = bind_sockets(port, family=socket.AF_INET)
else:
sockets = bind_sockets(port)
server = HTTPServer(application, xheaders=True)
server.add_sockets(sockets)
signal.signal(signal.SIGTERM, sig_handler)
signal.signal(signal.SIGINT, sig_handler)
tornado.ioloop.IOLoop.instance().start()
logging.info('Exit')
试了下以这种方式重启,耗时几秒的请求也不会被强制中断;100 个并发的压力测试下,重启过程中也没有任何失败请求出现。不过如果你的 settings.MAX_WAIT_SECONDS_BEFORE_SHUTDOWN 设得超过 10 秒,就要相应地增加 supervisord.conf 中 stopwaitsecs 的时间,否则会被强行杀掉的。
最后顺带一提,修改了 Supervisor 的配置,也可以用 supervisorctl reread 来重新载入,或用 supervisorctl reload 来载入新配置并重启所有子进程。直接运行 supervisorctl 的话,可以进入命令行模式操作。
2015年10月31日更新:
文中有两点需要修正:
- 为保证线程安全,add_callback 方法应该改成 add_callback_from_signal。
- Tornado 的后续版本增加了一系列内置的 timeouts,所以只检查 callbacks 即可。
向下滚动可载入更多评论,或者点这里禁止自动加载。