生產環境下如何優雅地重啓 Tornado

之前我在《Tornado 使用經驗》一文中,提到了調用 tornado.process.fork_processes() 來提高性能的方法。
在最近的實踐中,我發現這樣會有些弊端,所以便有了本文。
當然,這些仍然只是我個人的探索而已,並不保證是最佳實踐。

首先說下爲什麼我不再使用 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 的話,可以進入命令行模式操作。你可能還會對下列文章感興趣:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章