1. 程式人生 > >nginx中被忽略的RST

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的英文意義

)關閉,很多伺服器都會設該標誌,以避免存在大量的TIME_WAIT套接字,因為TIME_WAIT對於http-server來說,意義不大,因為它幾乎不會在短時間內重新開啟相同的套接字。

粗略看來,這裡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的工作了,就當練練手把。

有錯誤的地方,請指正,謝謝!