之前工作時候,一臺引流測試機器的一個 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 進程就會退出:
此時只剩下舊的 master,新的 master 和新 master 的 worker 在運行,這和當時線上運行的情況類似。
接著我們使用 stop 命令:
我們會發現,新的 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。
新聞熱點
疑難解答