Nginx神奇的499竟然不在HTTP響應碼標準內?快來了解一下!

發表於2023-09-19

1 前言

高效能的HTTP和反向代理伺服器,Nginx用來:

  • 搭建Web Server
  • 作負載均衡
  • 供配置的日誌欄位豐富,從各類HTTP頭部到內部效能資料都有

Nginx的訪問日誌中,存在499狀態碼的日誌。但常見4xx狀態碼只有400、401、403、404等,499並未在HTTP RFC文件。這499錯誤日誌,在流量較大場景下,特別是面向Internet的Web站點場景下還是很常見 。

2 案例

某客戶反饋:Nginx伺服器連續幾天記錄較多499錯誤日誌,之後幾天趨零,再回升,整體狀況不定。

經營的To C產品,跟手機端App協同。App會定時把訊息上傳到微信訊息閘道器,後者再把這些訊息推送到該客戶的服務端(在公有云上)做業務處理,整體訊息量約每日三十萬條。對訊息閘道器來說,這服務端就是一個Web回撥介面:

499日誌趨勢:

image-20230914091509298

由於大量499日誌存在,客戶非常擔心業務已受影響,如終端消費者是否經常上傳資料失敗?499狀態碼本身意味著啥呢?查它在Nginx的 官方定義

NGX\_HTTP\_CLIENT\_CLOSED\_REQUEST | 499

client closed request(客戶端關閉了請求)?說了跟沒說沒區別。啥引起“客戶端關閉請求”?

解決問題辦法,可能不在問題自身所處層面

應用層日誌記錄的只是表象。更深層次原因可能在更底層,如傳輸層或網路層。搞清499:

  • 不僅是理解這個499碼底層含義
  • 而且透過排查,掌握一套 對HTTP返回碼進行網路分析的方法。對維護好Nginx以及其他Web服務都有助

來抓包分析HTTP返回碼真正含義。

3 錨定到網路層

軟體文件已無法查清根因,所以要下沉到網路層排查。如你處理應用層故障,如HTTP異常返回碼(4xx和5xx系列),也遇到應用層找不到答案,就抓包分析。

後文

  • “客戶端”指微信訊息閘道器
  • “服務端”指這客戶在公有云的伺服器

服務端 使用tcpdump工具抓包,Wireshark開啟抓包檔案。從抓包檔案中一般尋找可疑報文。這次抓包有不少RST報文,過濾出典型的帶RST報文的TCP流:

抓包檔案可關注【JavaEdge】聯絡本人領取。

結尾處RST。這TCP流一定跟499日誌有關係嗎?得益於TCP/IP精妙分層設計,應用層只需透過系統呼叫,就可像使用檔案IO那樣使用網路IO,具體的網路細節都由核心處理。可由此也帶來問題: 應用層視角無法“看到”具體的網路報文。

需根據關鍵資訊確定應用層日誌跟網路報文的對應關係。如這裡,可確認上面這帶有RST的TCP流,就是日誌中記錄的一條499日誌記錄。因為:

  • 客戶端IP:日誌中的remote IP跟抓包檔案裡面的IP符合
  • 時間戳:日誌的時間戳也跟這個TCP流的時間吻合
  • 應用層請求:日誌裡的HTTP URL路徑和這個TCP流裡的URL相同
04也是類似方式找到應用日誌跟報文對應關係。

真實抓包分析場景中,“如何把應用層問題跟網路層抓包關聯”,始終是關鍵環節。也是令人困擾的關鍵技術障礙。所以這裡方法可參考,再處理這種關鍵環節,也可根據上面提到的三維即IP、時間戳、應用層請求(包括URL和header),把應用層問題錨定到網路層資料包。

既然確定這個流就代表一次499事件,好好分析報文。

4 解密TCP流

TCP流示意圖紅框部分是重點。

image-20230914101947340

報文1~3,表示TCP握手成功。

報文4(客戶端發出),表示客戶端向伺服器發報文,報文裡只包含HTTP header,其宣告該請求為POST方法,但不含POST body。正常,因為HTTP協議規定資料先後順序:先header(包含method、URL、headers),後body。所以,既然method和URL單獨位於一個報文,按順序body就在後續的報文。

報文5(服務端發出),確認報文:我(服務端)確認收到了你(客戶端)發過來的報文4。

報文6(客戶端發出),距上一報文2s。這報文被Wireshark標紅,註釋TCP Previous segment not captured:它之前的TCP報文段沒被抓到。

之前的TCP報文”是啥?

就是按TCP序列號順序,排在當前報文之前的報文。先關注右邊紅框FIN標誌位,說明這是客戶端主動關閉連線的報文。

目前報文情況:

image-20230914103706786

明明HTTP POST請求的body(也稱HTTP載荷)還沒發過來,客戶端就要關閉連線?好比朋友說:“我有個事情要你幫忙,嗯,拜~”,你剛聽到上半句他的求助意向,還沒聽到這忙是個啥,他就跟你說再見。可能暫時看不出問題,先放放。

報文7(服務端發出)。伺服器收到FIN+ACK報文(6號報文),但發現序列號不是它期待的309,而是777,於是伺服器TCP協議棧判斷:有一個長度為777-309=468的TCP段(TCP segment)丟失了。

按TCP約定,這時服務端只可確認其收到的位元組的最後位置,在這裡就是上次(報文5)的ACK位置。形式上,報文7就成了一個DupAck(重複確認)。

當客戶端收到DupAck時,它就要長個心眼:“情況有點微妙,如果湊滿3個DupAck可能有丟包”。

如湊滿3個DupAck就重傳的機制,被稱為快速重傳機制,12深入學習過。

報文4的TCP資訊:

按TCP設計,客戶端將傳送的下一個報文的序列號(309)= 本次序列號(1) + 本次資料長度(308),即Next sequence number。

報文8(客戶端發出),16s之久,客戶端 重傳 了這個報文,包含POST body,長度 468 位元組。就跟777-309=468對應。

明明這468位元組的報文是第一次出現,怎麼算重傳?因為,這個抓包檔案是在服務端生成,所以它的視角無法看到多次傳送同樣這個報文的現象。但我判斷,客戶端抓包,一定可看到這個468位元組的報文被試圖傳送多次。

服務端視角判斷,一開始這報文應該是走丟,沒到達服務端,所以沒在這服務端抓包檔案裡。又因為過16s才到,很可能不是單純一次重傳,而是多次重傳後才到達。因此確實屬重傳。

報文9,服務端對這POST body的資料包回覆確認報文。

報文10,服務端發HTTP 400的響應報文給訊息閘道器。這資訊並沒有被Wireshark直接按HTTP格式進行展示,但因HTTP是文字編碼,所以可滑鼠選中Transmission Control Protcol部分,在底下文字欄直接看到HTTP 400這段文字:

這HTTP 400報文也帶FIN標誌位,即服務端作業系統“圖省事”,把應用層的應答資料(HTTP 400),跟os對TCP連線關閉的控制報文(這個FIN),合併在同一個報文。03提到的搭順風車(Piggybacking),提升網路利用率。

這階段報文:

從這些報文順序來看,確實有問題:

  • 服務端先收到HTTP header報文,沒收到期望的HTTP body報文,而是收到FIN報文(客戶端試圖關閉連線)。HTTP請求還沒發到服務端,服務端回覆HTTP響應更無從談起,客戶端就發FIN不符常理(
  • 服務端回覆HTTP 400,併傳送FIN關閉這連線
  • 客戶端回覆RST徹底關閉這連線

    客戶端先傳送了FIN,之後才發POST body。 全部過程:

服務端還沒回複資料而客戶端已經要關閉連線,按499官方定義,這種行為被Nginx判定499狀態:

  • 對內表現為記錄499日誌
  • 對外表現為回覆HTTP 400給訊息閘道器

所以,在服務端的Nginx大量499日誌條目;在訊息閘道器那頭,如果它也做Web日誌,就是400報錯。

5 從現象到本質

為什麼客戶端先傳送FIN,然後才傳送POST body?

回到Wireshark視窗,再關注6號報文:

離上個報文相差了2s。

往前看4號報文:

離3號報文相差3s。加起來,6號報文離TCP握手完成,正好隔 5s整

一般出現這種整數,就越可疑,因為如果是系統或網路錯亂導致,時間分佈應 隨機,不可能卡在整數時間。經驗看, 這和人為設定有關。於是客戶仔細檢視微信閘道器使用文件,發現5s超時設定。即若一個HTTP事務無法在5s內完成,就關閉這連線。

啥叫無法完成?

在這抓包裡即:HTTP header報文發過去了,但HTTP body報文沒一起過去(網路原因導致)。而由於初始階段報文少, 無法湊齊3個DupAck,所以快速重傳沒有啟動,只好依賴超時重傳(12 講),且這多次超時重傳也失敗,服務端只好持續等待這丟失的報文。5s後,客戶端沒收到服務端響應,就主動關閉這次連線(可以下次再試,這次就不繼續幹等)。

即該場景裡Nginx 499錯誤日誌主因:

  • 訊息閘道器—>伺服器 方向上的一個TCP包丟失(案例裡是HTTP POST body報文),引起服務端空閒等待
  • 訊息閘道器有5s超時設定,即連線達到5s,訊息閘道器就發FIN關閉連線

邏輯鏈:

  • 要解決499報錯的問題,就需要解決5s超時
  • 要解決5s超時,就要解決丟包
  • 要解決丟包,就要改善網路鏈路質量

最根本解決方案,如何確保客戶端到服務端的 網路連線 可靠穩定,使類似的報文延遲現象降到最低。只要不丟包不延遲,HTTP事務就能在5s內完成,訊息閘道器就不會啟動5s超時斷開連線機制。

跟客戶還有閘道器工程師配合,確實發現閘道器到公有云的一條鏈路有問題。更換為另外一條鏈路後,丟包率大幅降低,問題極大改善。雖然還是有極小比例的錯誤日誌(約萬分之一),但對客戶已在可接受範圍。

因為丟包,客戶端FIN報文跟HTTP POST body報文一樣,也可能丟失。不過,無論這FIN是否被服務端及時收到,這次HTTP事務本身也已在客戶端記為失敗。

鏈路丟包這種問題挺明顯,為啥沒及時發現?

  • 雖然對主要鏈路的整體狀況有細緻的監控,但這裡的閘道器到客戶的公有云服務屬於“點到點”的連結,本身也屬於客戶自身的業務,公有云難以對這種情況做監控,理想情況是客戶自己來實現監控。
  • 客戶的訊息量很大,哪怕整體失敗比例不高,但乘以絕對的訊息量,產生的錯誤的絕對數也就比較可觀了。

至於Nginx為何“創造”499狀態碼, Nginx原始碼 註釋寫得清楚。並非標新立異,而確實是為了彌補標準HTTP協議不足:

/*
 * HTTP does not define the code for the case when a client closed
 * the connection while we are processing its request so we introduce
 * own code to log such situation when a client has closed the connection
 * before we even try to send the HTTP header to it
 */
#define NGX_HTTP_CLIENT_CLOSED_REQUEST     499

6 總結

Nginx 499是Nginx定義狀態碼,不是RFC中定義HTTP狀態碼。表示“Nginx收到完整的HTTP request前(或者已經接收到完整的request但還沒來得及傳送HTTP response前),客戶端試圖關閉TCP連線”這種反常情況。

超時時間跟499報錯數量也有直接關係。如果我們有辦法延長訊息閘道器的超時時間,比如從5秒改為50秒,那麼客戶端就有比較充足的時間去等待丟失的報文被成功重傳,從而在50秒內完成HTTP事務,499日誌也會少很多。

關注網路延遲對通訊的影響。比如客戶端發出的兩個報文(報文3和報文4)間隔了3秒鐘,這在網路通訊中是個非常大的延遲。而造成這麼大延遲的原因,會有兩種可能:

  • 訊息閘道器端本身是在握手後隔了3秒才傳送了這個報文,屬 應用層問題
  • 訊息閘道器在握手後立刻傳送了這個報文,但在公網上丟失了,微信訊息閘道器就根據“超時重傳”的機制重新發了這個報文,並於3秒後到達。屬 網路鏈路問題

由於上面的抓包是在服務端做的,所以未到達伺服器的包自然也不可能抓到,也就是無法確定是具體哪一種原因(客戶端應用層問題或網路鏈路問題)導致,但這並不影響結論。

公網丟包現象不可能完全消失。千分之一左右公網丟包率是正常範圍。由於:

  • 客戶傳送量較大(主因)
  • 微信訊息閘道器設定5s超時相對較短(次要原因)

問題就會在這個案例中被集中暴露。

設定更長超時閾值(如50s)能解決?出錯率會降低不少,但新問題:

  • 訊息閘道器會有更多的資源消耗(記憶體、TCP源埠、計算能力等)
  • 訊息閘道器處理事務的平均耗時會增加

所以,5s是權衡後的合適方案。

排查的方法論,對更廣泛的應用層報錯日誌的排查,推薦:

  • 先查應用文件,初步確定問題性質,大體確定排查方向
  • 對比應用日誌和抓取的報文,在傳輸層和網路層尋找可疑報文。這步,可採用比對策略找到可疑報文:

    • 日誌中的IP跟報文中的IP對應
    • 日誌和報文的時間戳對應
    • 應用層請求資訊和報文資訊對應
  • 結合協議規範和報文現象,推匯出根因

FAQ

  • 第7個報文是DupAck,為什麼沒觸發快速重傳?
  • 訊息閘道器那頭的應用日誌應該不是499,那會是啥樣日誌?

    本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章