nginx升級版本導致的CLOSE_WAIT異常

背景:

在 nginx上添加 http模塊(C++),添加的 http模塊調用配置文件中配置好的動態庫程序(C++),動態庫程序實現業務代碼。最近需要用到 nginx比較高版本的鏡像流量的功能,但是我們線上 nginx版本比較低,所以需要進行 nginx版本升級。

 

問題:

升級 nginx版本後(1.8 -> 1.16),上游請求 nginx服務會隨機超時,在 nginx服務機器上查看網絡連接狀態,有很多的 CLOSE_WAIT狀態。

 

排查:

第一階段:

最開始懷疑新的 nginx版本對某些配置默認值做了修改,導致老的配置加上新的 nginx版本導致的問題,於是百度 nginx CLOSE_WAIT各種文章,嘗試後均無效果。

 

第二階段:

直接在線上機器抓包,查看超時產生 CLOSE_WAIT的原因,發現是上游服務器在長連接請求上發送了 FIN,nginx服務器只回復了 ACK,沒有繼續回覆 FIN導致的。但是爲什麼沒有回覆呢?

看抓包是上游服務器發送了請求,nginx還沒有響應上游服務器,上游服務器就發送了 FIN。猜測是不是 nginx的響應還在內核的緩衝區中沒有返回給上游服務器,所以先回復了 ACK,等待內核緩衝區數據發送給上游服務器後再返回 FIN。於是各種百度內核緩衝區和 nginx tcp連接相關的配置,各種嘗試後仍無結果。

 

 

第三階段:

打算開啓 nginx的 debug,看看日誌中有沒有什麼線索。但是肯定不能線上機器開 debug,於是就想在開發環境復現。於是查看之前抓的包,發現如果一直請求 nginx服務,大概率在某個時刻就會出現上面的情況。於是拿一個線上請求,寫了個簡單的 tcp發送工具,把 http數據發送給新版 nginx。發現確實連續發送10~20個包左右就會出現上面的現象。

 

這嘗試的期間,還發現瞭如果上游的請求在一個 tcp包中無法完全傳輸時大概率發生此問題,也就是說 nginx有時需要 recv兩次才能獲取完整的 http請求數據。並且發現 tcp發送工具設置很長的超時時間(比如2s),nginx也沒有響應,這個時候 tcp發送工具調用 close就會復現上面的場景。於是可以判斷是這時 nginx已經不處理這個連接上的請求了。

爲了進一步驗證,如果請求數據足夠小可以在一個 tcp包中發送的話,是不會出現上面的現象的。於是簡單判斷是 nginx一次無法收集齊 http請求中 Content-Length的字節數,後面即使數據到達了,nginx也無動於衷,導致客戶端超時關閉連接,nginx仍無動於衷,所以產生了 CLOSE_WAIT狀態。

 

有了上面的信息,直接在 tcp發送工具中 http頭中的 Content-Length字段寫上一個比包體大的數據,發送給 nginx,也就是無論如何永遠都無法完整的得到請求數據,這樣發送後發現必現上面的問題。

有了必現的請求方式,就編譯了 nginx的 debug版本並打開 debug日誌,從日誌中找一些線索。發現如果第一次無法成功 recv的話,後面的數據或者 close狀態被 nginx接收到後,nginx處理時只會顯示 http reading blocked(ngx_http_block_reading函數的打印),爲什麼這裏的 read event的處理函數被設置爲 ngx_http_block_reading函數了呢?

2019/08/16 10:32:05 [debug] 14202#0: *1 http run request: "/tet/testtest/tet?"
2019/08/16 10:32:05 [debug] 14202#0: *1 http reading blocked

 

第四階段:

於是查看代碼中設置 read event handler的地方( 即 r->read_event_handler = ngx_http_block_reading; ),什麼時候設置了 ngx_http_block_reading函數,確實找到了幾處,但是由於我對 nginx源碼並不是特別熟悉,所以看不太懂。於是找到了最近的 1.14版本和 1.12版本分別進行測試,發現在 1.12版本中上面的情況是可以正常處理的,但是在 1.14版本就不可以正常處理了。所以我就對比了 1.12版本和 1.14版本 nginx在 http處理相關階段和 ngx_http_block_reading函數相關的代碼做了哪些改動?最終確認到了一個地方,就是函數 ngx_http_finalize_request中:

void ngx_http_finalize_request(ngx_http_request_t *r, ngx_int_t rc) {
    c = r->connection;
    if (rc == NGX_DONE) {
        // ...
        return;
    }

    if (rc == NGX_DECLINED) {
        // ...
        return;
    }

    if (rc == NGX_ERROR || rc == NGX_HTTP_REQUEST_TIME_OUT || rc == NGX_HTTP_CLIENT_CLOSED_REQUEST || c->error) {
        // ...
        return;
    }

    // ...
    r->done = 1;
    r->read_event_handler = ngx_http_block_reading;          // 這裏就是新版本添加的設置 read event handler的地方
    r->write_event_handler = ngx_http_request_empty_handler;

    // ...
    ngx_http_finalize_connection(r);
}

 

發現在 1.14版本之後就添加了設置 recv event的處理函數爲 ngx_http_block_reading的地方,沒有看懂爲什麼要添加這一行。於是我就把這行代碼註釋掉重新編譯 nginx。發現 1.14版本可以常工作了。於是確定是這裏的問題,但是又想了一下,nginx穩定版本不可能有如此猖狂的 bug啊,我的應用場景很普遍啊,如果有這個問題,肯定早就修復了。況且不熟悉上下文的情況下修改 nginx源碼,不能保證後續是否穩定。

 

 

第五階段:

因爲我們添加的 http模塊類似 nginx的 subrequest,就是簡單封裝了一下,使用了我們自己的協議。於是我在想,如果不用我們的 http模塊,使用 nginx的 proxy module,然後使用 tcp發送工具測試一下,發現 nginx處理正常,沒有問題。這就奇怪了,同樣是封裝的 upstream功能,我們的 http模塊就是簡單的增加了一層協議,爲什麼就出現了這麼大的問題。

到這裏,確定是我們的 http模塊和高版本的 nginx兼容有問題了。於是分別查看使用 nginx的 proxy module處理的 debug日誌和我們的 http模塊處理的 debug日誌,發現在調用函數ngx_http_finalize_request的時候的參數不一致,proxy module傳入參數是 -4(NGX_DONE),所以提前返回了,不會走到設置 recv event的處理函數的地方;而我們的 http模塊的參數是 -2(NGX_AGAIN),就到了設置 recv event的處理函數的地方。所以爲什麼我們的參數不一樣呢?

於是查看 nginx的 proxy模塊的 proxy_pass處理函數和我們自己的 http模塊在讀取 http請求時處理函數的相關代碼,同樣是 recv返回的數據不夠 http頭中 Content-Length的指定數量,nginx proxy模塊在收到 NGX_AGAIN消息後,調用函數 ngx_http_finalize_request的傳參時 NGX_DONE,但我們 http模塊傳的就是 NGX_AGAIN,於是修改了下,把我們 http模塊在 recv數據不足時的 NGX_AGAIN改爲 NGX_DONE,重新編譯測試,發現 nginx功能正常。後續有時間在研究下 NGX_AGAIN和 NGX_DONE在不同時刻的不同意義。

 

下面是正常情況下,客戶端發送完請求立馬發送 FIN的抓包處理以及 nginx debug日誌:

nginx debug日誌:

epoll_wait() reported that client prematurely closed connection, 
so upstream connection is closed too while sending request to upstream ...

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章