從實戰出發,談談 nginx 信號集

做者:張超nginx

前言

以前工做時候,一臺引流測試機器的一個 ngx_lua 服務忽然出現了一些 HTTP/500 響應,從錯誤日誌打印的堆棧來看,是不久前新發布的版本里添加的一個 Lua table 不存在,而有代碼向其進行索引致使的。這使人百思不得其解,若是是版本回退致使的,那麼爲何使用這個 Lua table 的代碼沒有被回退,恰恰定義這個 table 的代碼被回退了呢?服務器

通過排查發現,當時 nginx 剛剛完成熱更新操做,舊的 master 進程還存在,由於要準備機器重啓,先切掉了引流流量(但有些請求還在),同時系統觸發了 nginx -s stop,這才致使了這個問題。網絡

場景復現

下面我將使用一個原生的 nginx,在個人安裝了 fedora26 的虛擬機上覆現這個過程,我使用的 nginx 版本是目前最新的 1.13.4運維

首先啓動 nginx函數

圖片描述

能夠看到 master 和 worker 都已經在運行。性能

接着咱們向 master 發送一個 SIGUSR2 信號,當 nginx 核心收到這個信號後,就會觸發熱更新。測試

圖片描述

能夠看到新的 master 和該 master fork 出來的 worker 已經在運行了,此時咱們接着向舊 master 發送一個 SIGWINCH 信號,舊 master 收到這個信號後,會向它的 worker 發送 SIGQUIT,因而舊 master 的 worker 進程就會退出:ui

圖片描述

此時只剩下舊的 master,新的 master 和新 master 的 worker 在運行,這和當時線上運行的狀況相似。lua

接着咱們使用 stop 命令:spa

圖片描述

咱們會發現,新的 master 和它的 worker 都已經退出,而舊的 master 還在運行,併產生了 worker 出來。這就是當時線上的狀況了。

事實上,這個現象和 nginx 自身的設計有關:當舊的 master 準備產生 fork 新的 master 以前,它會把 nginx.pid 這個文件重命名爲 nginx.pid.oldbin,而後再由 fork 出來的新的 master 去建立新的 nginx.pid,這個文件將會記錄新 master 的 pid。nginx 認爲熱更新完成以後,舊 master 的使命幾乎已經結束,以後它隨時會退出,所以以後的操做都應該由新 master 接管。固然,在舊 master 沒有退出的狀況下經過向新 master 發送 SIGUSR2 企圖再次熱更新是無效的,新 master 只會忽略掉這個信號而後繼續它本身的工做。

問題分析

更不巧的是,咱們上面提到的這個 Lua table,定義它的 Lua 文件早在運行 init_by_lua 這個 hook 的時候,就已經被 LuaJIT 加載到內存並編譯成字節碼了,那麼顯然舊的 master 必然沒有這個 Lua table,由於它加載那部分 Lua 代碼是舊版本的。

而索引該 table 的 Lua 代碼並無在 init_by_lua 的時候使用到,這些代碼都是在 worker 進程裏被加載起來的,這時候項目目錄裏的代碼都是最新的,因此 worker 進程加載的都是最新的代碼,若是這些 worker 進程處理到相關的請求,就會出現 Lua 運行時錯誤,外部表現則是對應的 HTTP 500。

吸取了這個教訓以後,咱們須要更加合理地關閉咱們的 nginx 服務。 因此一個更加合理的 nginx 服務啓動關閉腳本是必需的,網上流傳的一些腳本並無對這個現象作處理,咱們更應該參考 NGINX 官方提供的腳本。

圖片描述

這段代碼引自 NGINX 官方的 /etc/init.d/nginx 。

nginx 信號集

接下來咱們來全面梳理下 nginx 信號集,這裏不會涉及到源碼細節,感興趣的同窗能夠自行閱讀相關源碼。

咱們有兩種方式來向 master 進程發送信號,一種是經過 nginx -s signal 來操做,另外一種是經過 kill 命令手動發送。

第一種方式的原理是,產生一個新進程,該進程經過 nginx.pid 文件獲得 master 進程的 pid,而後把對應的信號發送到 master,以後退出,這種進程被稱爲 signaller。

第二種方式要求咱們瞭解 nginx -s signal 到真實信號的映射。下表是它們的映射關係:

operation signal
reload SIGHUP
reopen SIGUSR1
stop SIGTERM
quit SIGQUIT
hot update SIGUSR2 & SIGWINCH & SIGQUIT
stop vs quit

stop 發送 SIGTERM 信號,表示要求強制退出,quit 發送 SIGQUIT,表示優雅地退出。 具體區別在於,worker 進程在收到 SIGQUIT 消息(注意不是直接發送信號,因此這裏用消息替代)後,會關閉監聽的套接字,關閉當前空閒的鏈接(能夠被搶佔的鏈接),而後提早處理全部的定時器事件,最後退出。沒有特殊狀況,都應該使用 quit 而不是 stop。

reload

master 進程收到 SIGHUP 後,會從新進行配置文件解析、共享內存申請,等一系列其餘的工做,而後產生一批新的 worker 進程,最後向舊的 worker 進程發送 SIGQUIT 對應的消息,最終無縫實現了重啓操做。

reopen

master 進程收到 SIGUSR1 後,會從新打開全部已經打開的文件(好比日誌),而後向每一個 worker 進程發送 SIGUSR1 信息,worker 進程收到信號後,會執行一樣的操做。reopen 可用於日誌切割,好比 NGINX 官方就提供了一個方案:

圖片描述

這裏 sleep 1 是必須的,由於在 master 進程向 worker 進程發送 SIGUSR1 消息到 worker 進程真正從新打開 access.log 之間,有一段時間窗口,此時 worker 進程仍是向文件 access.log.0 裏寫入日誌的。經過 sleep 1s,保證了 access.log.0 日誌信息的完整性(若是沒有 sleep 而直接進行壓縮,頗有可能出現日誌丟失的狀況)。

hot update

某些時候咱們須要進行二進制熱更新,nginx 在設計的時候就包含了這種功能,不過沒法經過 nginx 提供的命令行完成,咱們須要手動發送信號。

經過上面的問題復現,你們應該已經瞭解到如何進行熱更新了,咱們首先須要給當前的 master 進程發送 SIGUSR2,以後 master 會重命名 nginx.pid 到 nginx.pid.oldbin,而後 fork 一個新的進程,新進程會經過 execve 這個系統調用,使用新的 nginx ELF 文件替換當前的進程映像,成爲新的 master 進程。新 master 進程起來以後,就會進行配置文件解析等操做,而後 fork 出新的 worker 進程開始工做。

接着咱們向舊的 master 發送 SIGWINCH 信號,而後舊的 master 進程則會向它的 worker 進程發送 SIGQUIT 信息,從而使得 worker 進程退出。向 master 進程發送 SIGWINCH 和 SIGQUIT 都會使得 worker 進程退出,可是前者不會使得 master 進程也退出。

最後,若是咱們以爲舊的 master 進程使命完成,就能夠向它發送 SIGQUIT 信號,讓其退出了。

worker 進程如何處理來自 master 的信號消息

實際上,master 進程再向 worker 進程通信,不是使用 kill 函數,而是使用了經過管道實現的 nginx channel,master 進程向管道一端寫入信息(好比信號信息),worker 進程則從另一端收取信息,nginx channel 事件,在 worker 進程剛剛起來的時候,就被加入事件調度器中(好比 epoll,kqueue),因此當有數據從 master 發來時,便可被事件調度器通知到。

nginx 這麼設計是有理由的,做爲一個優秀的反向代理服務器,nginx 追求的就是極致的高性能,而 signal handler 會中斷 worker 進程的運行,使得全部的事件都被暫停一個時間窗口,這對性能是有必定損失的。

不少人可能會認爲當 master 進程向 worker 進程發送信息以後,worker 進程馬上會有對應操做迴應,然而 worker 進程是很是繁忙的,它不斷地處理着網絡事件和定時器事件,當調用 nginx channel 事件的 handler 以後,nginx 僅僅只是處理了一些標誌位。真正執行這些動做是在一輪事件調度完成以後。因此這之間存在一個時間窗口,尤爲是業務複雜且流量巨大的時候,這個窗口就有可能被放大,這也就是爲何 NGINX 官方提供的日誌切割方案裏要求 sleep 1s 的緣由。

固然,咱們也能夠繞過 master 進程,直接向 worker 進程發送信號,worker 能夠處理的信號有

signal effect
SIGINT 強制退出
SIGTERM 強制退出
SIGQUIT 優雅退出
SIGUSR1 從新打開文件

總結

nginx 信號操做在平常運維中是最多見的,也是很是重要的,這個環節若是出現失誤則可能形成業務異常,帶來損失。因此理清楚 nginx 信號集是很是必要的,能幫助咱們更好地處理這些工做。

另外,經過此次的經驗教訓和對 nginx 信號集的認知,咱們認爲如下幾點是比較重要的:

慎用 nginx -s stop,儘量使用 nginx -s quit熱更新以後,若是肯定業務沒問題,儘量讓舊的 master 進程退出關鍵性的信號操做完成後,等待一段時間,避免時間窗口的影響不要直接向 worker 進程發送信號

相關文章
相關標籤/搜索