標籤:

是什麼讓你的 nginx 服務退出這麼慢?

筆者曾今在更新 nginx 服務的過程中發現舊的 nginx worker 進程退出非常緩慢(舊的 worker 進程始終處在 "is shutting down" 的狀態),對此非常好奇,並對此展開了一些研究,本文將介紹 nginx worker 進程退出時的準備步驟,延緩退出的原因,並介紹對應的解決辦法。

(註:下文描述時會簡單涉及一些 nginx 源碼以及 ngx_lua 相關的技術和一些示例代碼。)

準備退出

當 worker 進程接收到 master 進程要求它退出的指令後(詳見筆者另一篇文章:談談 nginx 信號集),它便會開始為退出做準備。

首先 worker 進程會將正在監聽的套接字從事件分發器(epollkqueue 等)中刪除,並將它們關閉,之後它將不再處理連接事件。

接著關閉所有的空閑連接,所謂的空閑連接,指的是當前沒有請求正在使用的連接,例如 nginx 和後端伺服器維持的長連接,或者 ngx_lua Cosocket 對象底層的長連接。

接著 worker 進程會等待所有定時器過期(ngx_lua 提供給用戶使用的定時器比較特殊,在退出階段,它會提前過期,其他的 nginx 內部的定時器不會提前過期),並同時處理尚未完成的事件。等事件處理完畢後, worker 進程會調用所有模塊註冊的 exit_process 鉤子,最後退出。

退出被延緩

了解了 worker 進程退出時的準備過程後,我們可以深入分析為什麼有的時候退出如此緩慢。

根據筆者目前的分析,目前有以下兩種情況會延緩 worker 進程的退出:

  • ngx_lua:在提前過期的定時器中使用 Cosocket
  • nginx http/2 實現上的一個 bug

第一種情況曾有人在 ngx_lua 的 issue 頁面提出過( Cosocket :setkeepalive() in a a premature timer handler blocks nginx worker from exiting · Issue #1279 · openresty/lua-nginx-module)。

比如 issue 中的示例代碼:

ngx.timer.at(100, function () -- This blocks Nginx worker from exiting local timer_sock = ngx.socket.tcp() timer_sock:connect("127.0.0.1", 8080) timer_sock:setkeepalive()end)

當然,這段代碼省略了一些錯誤處理,但是用以解釋問題已經足夠。這段代碼註冊了一個定時器,只要這個定時器運行,就會創建一個 Cosocket 對象,然後去連接本機的 8080埠,然後馬上將這個對象底層的連接置為 keep alive 狀態。

先說 connect 函數,如果和對端的連接不能一次性完成,ngx_lua 會為這次連接操作添加一個定時器,用以判斷連接超時,當然這裡是連接本機的埠,因此幾乎不會出現連接超時(對端異常除外)。

假如這裡所要連接的對端處在公網,而且網路狀況不理想的話,連接超時就有可能發生了,ngx_lua 默認的 Cosocket 連接超時是 60s(lua_socket_connect_timeout),這意味著這個 worker 進程會等待至少 60s,然後再退出。

同樣地,setkeepalive 也會為這條連接設置一個超時時間,默認也是 60s( lua_socket_keepalive_timeout) ,因此 worker 進程也不得不等到這個定時器過期,或者某個時刻對端主動關閉/異常關閉這條連接後,它才能夠退出。

讀者可能會有疑惑,之前講到 worker 進程退出時會主動關閉這些空閑的長連接,那為什麼這個示例還回造成 worker 進程退出那麼慢呢?即使是本機連接,也有可能出現無法一次完成連接( EAGAIN) 的情況,此時當前定時器的 Lua 協程就會被掛起,因此當 worker 進程在關閉所有空閑連接的時候,這個示例里 setkeepalive 是還沒被執行到的(甚至可能連接也沒有建立完成),所以這條連接在當時不是空閑的。直到後來某個時刻連接建立完成或者超時,當時的 Lua 協程重新得到運行機會,才會為這條連接添加定時器,置為空閑狀態。

另外一個阻礙 worker 進程退出的原因來自於一個 nginx HTTP/2 模塊實現上的缺陷(見 Stale workers not exiting after reload (with HTTP/2 long poll requests))。這個問題在 nginx/1.11.6 發布之後就修復了(見 nginx: 5e95b9fb33b7),1.11.6 之前的版本,如果一個 HTTP/2 協議的客戶端一直在打開新的流,會導致這條連接上一直有事件在處理(當然會伴隨著創建定時器),這會導致 worker 進程會一直無法退出,直到這條連接斷開。

shutdown timeout

舊 worker 進程不能及時退出,就會一直佔用著系統資源(CPU、內存和文件描述符等),這對系統資源是一種浪費,因此nginx/1.11.11加入了一個新的指令(即 worker_shutdown_timeout,見 Core functionality),允許用戶自定義 shutdown 超時時間,如果一個 worker 在接收到退出的指令後經過 worker_shutdown_timeout 時長後還不能退出,就會被強制退出。

它的實現原理(nginx: 97c99bb43737)也是通過創建定時器來實現的,一旦定時器過期, 所有連接都會被設置為 error 狀態(c->error = 1),這個標誌位事實上意味著 TCP 連接異常,nginx 設計上對於這種狀態的連接,都會立刻結束對應的所有請求、事件。通過這樣一個標誌位的設置,就達到了強制關閉所有連接、刪除所有定時器的目的,最終及時退出舊的 worker 進程,釋放系統資源。


推薦閱讀:

10 分鐘內快速構建能夠承載海量數據的 NGINX 日誌分析與報警平台
為什麼 node.js 的官網不用 node.js 而用 nginx 搭建?
歪果仁吐槽國內開源,勢態將逆轉還是惡化?
varnish / squid / nginx cache 有什麼不同?
Nginx實現負載均衡的原理

TAG:Nginx |