我眼中的 Nginx(四):是什麼讓你的 Nginx 服務退出這麼慢?

張超:又拍雲系統開發高級工程師,負責又拍雲 CDN 平臺相關組件的更新及維護。Github ID: tokers,活躍於 OpenResty 社區和 Nginx 郵件列表等開源社區,專一於服務端技術的研究;曾爲 ngx_lua 貢獻源碼,在 Nginx、ngx_lua、CDN 性能優化、日誌優化方面有較爲深刻的研究。

 

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

準備退出

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

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

接着關閉全部的空閒鏈接,所謂的空閒鏈接,指的是當前沒有請求正在使用的鏈接,例如 Nginx 和後端服務器維持的長鏈接,或者 ngx_lua Cosocket 對象底層的長鏈接。github

接着 worker 進程會等待全部定時器過時(ngx_lua 提供給用戶使用的定時器比較特殊,在退出階段,它會提早過時,其餘的 Nginx 內部的定時器不會提早過時),並同時處理還沒有完成的事件。等事件處理完畢後, worker 進程會調用全部模塊註冊的 exit_process 鉤子,最後退出。web

退出被延緩

瞭解了 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)[1]。服務器

好比 issue 中的示例代碼:websocket

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))[2]。這個問題在 Nginx/1.11.6 發佈以後就修復了(見 Nginx: 5e95b9fb33b7)[3],1.11.6 以前的版本,若是一個 HTTP/2 協議的客戶端一直在打開新的流,會致使這條鏈接上一直有事件在處理(固然會伴隨着建立定時器),這會致使 worker 進程會一直沒法退出,直到這條鏈接斷開。

Nginx 支持透明代理 websocket 鏈接。在 Nginx/1.13.7 版本之前,若是 worker 進程存在一些 websocket 鏈接,並且鏈接上常常有數據傳送,使得鏈接一直在正常工做的話,即便 worker 進程收到來自 master 的退出指令,它也沒法馬上退出,它須要等到這些鏈接出現異常、超時或者是某一端主動斷開後,才能正常退出。

shutdown timeout

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

它的實現原理(Nginx: 97c99bb43737)[5]也是經過建立定時器來實現的,一旦定時器過時, 全部鏈接都會被設置爲 close 和 error 狀態(c->error = 1,c->close = 1),這個標誌位事實上意味着 TCP 鏈接異常,Nginx 設計上對於這種狀態的鏈接,都會馬上結束對應的全部請求、事件。經過這樣一個標誌位的設置,就達到了強制關閉全部鏈接、刪除全部定時器的目的,最終及時退出舊的 worker 進程,釋放系統資源。

雖然這個功能早在 Nginx/1.11.11 就加入了,可是沒有徹底覆蓋到全部的狀況,例如上文所述的 websocket 鏈接的處理,那部分代碼並無判斷 c->close 和 c->error 的狀態位。因此仍然沒法儘快終止這些 websocket 鏈接。直到 Nginx/1.13.7,這個問題才被修復。因此若是讀者們遇到相似的問題,能夠考慮升級 Nginx 至少到 1.13.7 版本。


[1] issue 頁面: 

[2] 缺陷: 

[3] 修復: 

[4] 指令: 

[5] 原理: 

 

《我眼中的 Nginx》系列:

我眼中的 Nginx(三):Nginx 變量和變量插值
我眼中的 Nginx(二):HTTP/2 dynamic table size update
我眼中的 Nginx(一):Nginx 和位運算

相關文章
相關標籤/搜索