咱們基於Vanilla開發了一個相似於一個網關的流量分發服務,在原來的業務線上對不一樣的業務使用不一樣的後端(PHP、Python、Lua...)進行處理,最近在緊鑼密鼓的測試(固然這裏我們主要看問題),在掃蕩日誌的過程當中發現有這樣的一條 [error]
(日誌已打碼)nginx
沒錯,就是條: attempt to set ngx.status after sending out response headers while sending to client,大體意思是我在響應頭已經發出後又嘗試對 ngx.status 進行了修改,但是我確定不會想那麼幹的,並且頁面請求看着明明是正常的。git
本着認真負責的態度,我又對代碼邏輯和寫法前先後後梳理數次,然事實上並無發現我試圖那麼幹,至少本意是肯定的。面對這個幽靈般的錯誤,一個程序員的直覺告訴我,確定是我寫了一個bug?或者個人某些邏輯觸發了Vanilla的bug?或者觸發了OpenResty的bug?越想越激動,我必須把它找出來。程序員
爲了不你們混淆各類Vanilla,這裏先附上Vanilla項目地址:github
Github:https://github.com/idevz/vanilla後端
GitOSC:http://git.oschina.net/idevz/vanillaide
邏輯上肉眼沒看出什麼問題,只能經過debug來解決。到底哪行報出來的錯誤呢?在公司開發機上添加 --with-debug
參數從新編譯了OpenResty,打開debug日誌。測試
一看果真是在響應發出後報的錯,但日誌沒有反應出報錯的具體位置。沒辦法,我只能經過「二分步進法」,打一堆日誌來跟進,人肉找出來到底什麼地方報的錯。lua
最後跟到這樣一處邏輯:.net
請求正常完成後,response:response()
執行結果肯定是true,問題必定出在 ngx.eof()
, 個人本意在於若是在routerShutdown階段(Vanilla請求處理的第二個階段)請求完成響應,則後面的幾個階段就再也不執行,直接結束當前請求。查閱文檔發現 ngx.eof()
只是顯式指定了響應流輸出結束,後面的代碼邏輯會在服務端繼續執行。而我指望的當前請求直接終止,不該該使用 ngx.eof()
而是 ngx.exit()
。下面咱們細節來認識下這兩個API。debug
雖然在OpenResty ngx-lua
模塊文檔中這兩個API文檔位置緊鄰,但用法和功能方面卻大相徑庭。
用法: ngx.exit(status) 執行上下文: rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, ngx.timer., balancer_by_lua, ssl_certificate_by_lua* ngx.exit()
的使用相對簡單些:
ngx.exit()
會中斷當前請求,並將傳入的狀態碼(status)返回給nginx。ngx.exit()
會中斷當前執行的phrase(ngx-lua模塊處理請求的階段,如content_by_lua*),進而繼續執行下面的phrase。ngx.exit()
須要進一步注意的是參數status的使用,status能夠傳入ngx-lua所定義的全部的HTTP狀態碼常量(如:ngx.HTTP_OK、ngx.HTTP_GONE、ngx.HTTP_INTERNAL_SERVER_ERROR等)和兩個ngx-lua模塊內核常量(只支持NGX_OK和NGX_ERROR這兩個,若是傳入其餘的如ngx.AGAIN等則進程hang住)。ngx.exit()
最佳實踐是同 return
語句組合使用,目的在於加強請求被終止的語義(return ngx.exit(...)
)。用法: ok, err = ngx.eof() 執行上下文: rewrite_by_lua*, access_by_lua*, content_by_lua* ngx.eof
除了前面所說的顯式指定了響應流輸出的結束,後面的邏輯繼續在服務端執行外,還須要注意如下幾點:
ngx.eof()
來使客戶端主動斷開鏈接,這個技巧能夠用來作一些back-ground jobs 而不須要HTTP客戶端等待鏈接(不過文檔推薦的back-ground jobs的處理方式是 ngx.timer.at
API,詳情請看文檔說明)。location
配置的上游模塊時,你應該配置這些上游模塊來忽略客戶端鏈接的中斷,若是默認不是忽略的話。例如默認的標準 ngx_http_proxy_module
模塊會在客戶端斷開鏈接後當即同時終止子請求和主請求,因此在模塊 ngx_http_proxy_module
將proxy_ignore_client_abort
設置爲開啓(proxy_ignore_client_abort on;
)就十分重要。v0.8.3
起, ngx.eof()
執行成功返回1,失敗則返回 nil
和錯誤描述信息。實踐發現 ngx.exit()
和 ngx.eof()
本質區別在於ngx.exit()
做用在於中斷當前操做,不論是ngx-lua模塊請求處理的當前階段仍是整個請求,而 ngx.eof()
只是結束響應流的輸出,中斷HTTP鏈接,後面的代碼邏輯還會繼續在服務端執行,並且 ngx.eof()
支持運行的上下文比 ngx.exit()
少太多, ngx.eof()
有返回值, ngx.exit()
則沒有,由於請求已經結束。
其實這是一個不大不小的bug,說它小,由於後來我在文檔中對ngx.status的描述中發現這麼一句 Setting ngx.status after the response header is sent out has no effect but leaving an error message in your nginx's error log file
說明,也就是試圖在響應頭髮出後更改ngx.status會在錯誤日誌中記錄一條 [error]
可是這個錯誤對本次請求的響應沒有影響;說它大,若是沒有仔細查出來這個沒有影響,那一切都是未知,極可能給系統埋下一個未知的坑,不知道哪天就會爆出來坑你一下,關鍵的一點仍是對API的理解。OpenResty的文檔是我見過開源項目中寫的比較好的,雖然是英文。仍是值得仔細研習。