nginx中被忽略的RST
所用nginx版本為1.2.0,現在看有點老了,新版本已經有很多改進,後面會提到。
問題場景就是上篇blog中最後提到的:nginx收到client的請求,然後連線一個up-server(這裡是一個tomcat),並將請求發給它,tomcat產生資料,返回給nginx;但此時由於nginx任務過重,沒能及時讀取資料,導致tcp接收緩衝滿了,tcp視窗長時間為0;tomcat的寫請求最終超時,這時tomcat直接發一個RST斷開tcp;稍後的時間裡,nginx去讀取tcp接收緩衝中的資料,並轉發給client,讀到最後時返回n=-1、errno=ECONNRESET;但nginx卻並未在意這個錯誤,nginx與client的tcp連線仍然存在,直到寫超時,nginx才主動關閉該連線,致使client白等了老長時間。
其實問題並不複雜,調一遍程式碼基本就能摸清楚流程。這裡正好借這個問題,看看RST的相關處理。
1.RST的產生
RST總的來說就是異常關閉,與其對立的就是FIN的正常關閉。tcp連線中有多種情況會產生RST,如被訪問埠未listen,close的套接字(非shutdown的)收到資料,讀緩衝區還有資料時提前close,可以參考點選開啟連結。
還有一種很常見的RST情況,就是tcp設定SO_LINGER選項。預設情況下,tcp不設該標誌,當執行close操作時,tcp會將傳送緩衝區裡的資料全部發完,然後再發送FIN來正常終止連線;但若設定了該標誌,執行close操作時,就立刻丟棄緩衝區的資料,傳送RST,並刪除該socket(參考《TCP/IP詳解:卷一》中文版p.187)。這就是所謂的不磨蹭(SO_LINGER的英文意義
粗略看來,這裡tomcat應該就是屬於這種情況,沒有去深究。
2.RST的處理
接收到RST的一方,不會進行ACK確認,直接終止連線(參考《TCP/IP詳解:卷一》中文版p.188)。
但這裡的終止連線,卻並非那麼簡單。用 ss -t 命令檢視,的確發現沒有該套接字了,但是在 /proc/pid/fd/ 下,卻仍然有該socket的描述符,而且該描述符的確還有資料在緩衝區中,可讀。
再檢視 cat /proc/net/sockstat 就很清楚了,該套接字雖然不再是inuse狀態,但仍然是alloc狀態,即仍然保持著skbuf。並且RST包會給該socket設定一個reset標誌。
在稍後的時間裡,nginx終於有時間來讀了,看程式碼流程:
ngx_http_upstream_process_upstream()
|--- ngx_event_pipe()
| |--- for(;;) {
| | ngx_http_upstream_read_upstream() // 讀到0(EOF)或-1(出錯)時,跳出
| | ngx_http_upstream_write_to_downstream()
| | }
|--- ngx_http_upstream_process_request() // 後續處理
除錯程式碼發現,nginx確實讀到了up-server已經發來的資料,並且傳送給client,讀完緩衝區的所有資料後,會讀到-1,且errno=ECONNRESET,跳出迴圈,設定upstream->error=1。但是在後續處理中卻並沒有對該錯誤做處理,以至於這個downstream連線一直存在,直到超時後才有nginx關閉。
對於wget這樣的應用程式,僅收到一部分響應後,就收到FIN,那麼它會正常終止這個連線,然後重新開始另一個連線,但請求頭為 GET Content-Range:65785-150000,請求後半部分資料。
3.新版本的改進
今天下了1.6版本的nginx用,重新試了一下這個案例,發現不再有這個問題了,除錯了一下程式碼,改進就在於後續處理函式ngx_http_upstream_process_request()中。
1.2版本的程式碼片段
if (p->upstream_done || p->upstream_eof || p->upstream_error) {
// 分別對應 完整響應,中途收到FIN,其它異常錯誤
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http upstream exit: %p", p->out);
ngx_http_upstream_finalize_request(r, u, 0);
return;
}
1.6版本的程式碼片段
if (p->upstream_done || p->upstream_eof || p->upstream_error) {
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http upstream exit: %p", p->out);
if (p->upstream_done
|| (p->upstream_eof && p->length == -1))
{
ngx_http_upstream_finalize_request(r, u, 0);
return;
}
// 收到中途FIN,或異常錯誤,以NGX_HTTP_BAD_GATEWAY終止連線
if (p->upstream_eof) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
"upstream prematurely closed connection");
}
ngx_http_upstream_finalize_request(r, u, NGX_HTTP_BAD_GATEWAY);
return;
}
程式碼很明白,就不多說了。
嘿嘿,分析異常、錯誤之類的還是很有意思的,馬上要到alibaba作PE的工作了,就當練練手把。
有錯誤的地方,請指正,謝謝!