背景
線上主api服務使用的是uWSGI+Django框架,循歷史傳承一直是通過svc守護進程運行,每次重啟無外乎通過svc -k / svc -i 通知server實現重啟,本質上就是通過向server發送SIGKILL/SIGINT信號實現結束舊進程,而后守護進程重新拉起新進程運行。
問題
此種重啟方式每次都會導致重啟時刻一小段時間(數秒--數十秒)線上server的不可用,這直接導致兩個異常來源:1,舊進程直接被kill,當前正在處理未完成的請求流程也直接中斷;2,舊server被kill掉,新server啟動初始化ready前,完全無法處理任何新請求,直接體現為大量的niginx502響應。
針對異常1,由於SIGKILL信號直接殺死進程,所以不可避免,但是對於SIGINT信號,通過增加信號處理函數,理論上可以等server處理完全部未完成請求后再結束自身,但在收到SIGINT信號后,不應該再接收新的請求,此種方式不可能解決異常2的問題。
針對異常2,要解決出現server重啟准備期間無法響應新請求的問題,只能提前將打向重啟節點的流量先屏蔽掉,server重啟ready后再恢復流量,通過提前一定時間屏蔽流量,等待歷史請求處理完成后再重啟,還可以解決掉異常1的問題。這算是保持多進程服務高可用的一種典型思路,難點在於需要能夠自動化這一流程,即:a, nginx屏蔽node流量;b,一定時間后重啟node服務;c,重啟ready后恢復node流量。對於使用一體化分布式框架,已經打通流量控制、節點重啟平台功能的大公司,這個不失為一種選擇,但對於小公司來說,自動化難度較大,而如果手動操作則不但繁瑣,而且很容易出錯,節點增多后更是難以接受的labor work。
uWSGI優雅重啟
在網上搜尋有無更好的uWSGI優雅重啟方案,找到了uWSGI官方文檔:
其中提到了數種reload的方案,簡單總結如下:
方法
|
過程
|
pros
|
cons
|
---|---|---|---|
服務端當前使用方式 |
直接通過svc發送SIGINT/SIGKILL信號 直接觸發real_run腳本中的相關信號通知 |
使用簡單 |
每次重啟所有進程(包括master),重啟完成為全新的進程 不等待已有請求完成直接結束舊進程, 新進程ready前所有新請求將無法處理,相當於服務down掉一段時間(秒級)–-靠nginx實現fail over |
Standard (default/boring) graceful reload (aka SIGHUP)
|
直接發送SIGHUP信號 master進程本身不重啟,等待已有請求處理結束后結束worker 新worker ready前,所有新請求將進入等待隊列 |
使用簡單 不會存在不一致狀態 基本重置了所有進程狀態 |
等待隊列滿了之后的新請求將直接報錯 新請求可能需要等待較長時間 |
Workers reloading in lazy-apps mode | write w to The Master FIFO wait for running workers and then restart each of them. |
avoids restarting the whole instance. |
和standar方式一樣新請求需要進入隊列等待 |
Chain reloading (lazy apps) | write c to The Master FIFO 已有請求處理完成后reload 一個worker,新worker ready后才重啟下一個worker |
新請求無需進入等待隊列等待,多個worker之中始終有可以接受請求的worker在工作 逐個worker重啟降低機器短時負載 |
只能處理worker代碼更新的重啟,無法更改uwsgi option配置 需要多個worker配置才能有較好效果 |
Zerg mode | 配置 zerg server 或者 zerg pool(綁定unix socket) 首先spawn 新worker,ready后shutdown 舊worker(具體參見下面的Zerg Dance-自動化這一過程的一種實現) |
基本已經算是0停機reload 允許master配置不同的option重啟
|
需要一個額外的進程 相對沒那么容易管理 reload時需要拷貝整個uWSGI棧 |
The Zerg Dance: Pausing instances | 通過配置3個Master FIFOs+ uWSGI 高級hooks實現開啟新進程,暫停(pause)舊進程 | truly zero-downtime reload. |
需要使用高級的uWSGI和Unix技術,配置較為復雜 |
綜合考慮,鏈式重啟的方式配置簡單,而且在多worker的情況下已經完全能夠避免異常1與異常2問題的產生,考慮到實際上更改uWSGI配置的頻率非常之低--偶爾需要按照舊有方式有損重啟master進程也可以接受,因而采用鏈式重啟實現uWSGI配置的優雅重啟即可,實際只需要在原.xml配置文件中加上 <master-fifo>/tmp/uwsgi_api.fifo</master-fifo> (對應.ini文件、命令行參數加上master-fifo也一樣) ,表示通過/tmp/uwsgi_api.fifo 管道傳輸命令,需要重啟時執行 echo c > /tmp/uwsgi_api.fifo 即可。
舊配置:
<uwsgi> <socket>0.0.0.0:3000</socket> <listen>14400</listen> <master>true</master> <processes>4</processes> <threads>12</threads> <module>wsgi</module> <profiler>true</profiler> <memory-report>true</memory-report> <limit-as>6048</limit-as> <buffer-size>65536</buffer-size> <thunder-lock>true</thunder-lock> <harakiri>30</harakiri> <lazy-apps>true</lazy-apps> </uwsgi>
新配置:
<uwsgi> <socket>0.0.0.0:3000</socket> <listen>14400</listen> <master>true</master> <master-fifo>/tmp/uwsgi_api.fifo</master-fifo> <processes>4</processes> <threads>12</threads> <module>wsgi</module> <profiler>true</profiler> <memory-report>true</memory-report> <limit-as>6048</limit-as> <buffer-size>65536</buffer-size> <thunder-lock>true</thunder-lock> <harakiri>30</harakiri> <lazy-apps>true</lazy-apps> </uwsgi>
PS: 在網上搜索到已經有人分享過 uWSGI平滑重啟的方式,多篇文章來源看上去都是同一篇--uwsgi graceful reload,所采用的也是鏈式重啟,都是通過配置 在.ini配置文件中添加:touch-chain-reload=XXX/settings.py 實現,即每次通過touch 某個代碼文件的方式實現觸發自動重啟,后面鏈式重啟邏輯本質都是一樣的,只在於我這里是通過管道發送重啟命令,而前者是通過監控代碼文件狀態實現。個人認為通過命名管道方式觸發重啟更可控一些,這樣能將重啟操作本身與代碼狀態這兩個本就不應該相關內容的事務隔離開來,而且采用touch方式,任何時候線上只要一發生代碼更新--git pull新代碼、cp覆蓋新代碼乃至編輯修改代碼(應極力避免)--無論是有意無意,都將觸發reload,這不一定是操作者本身想要的,而通過管道方式,則很明確該操作就是需要重啟server。