詭異的502問題

現(xiàn)象

業(yè)務(wù)中有個場景,接收到 HTTP 請求之后,根據(jù)條件可能修改某些參數(shù)杰扫,轉(zhuǎn)發(fā)請求,拿到結(jié)果之后稍作修改寫到 ServletResponse 中膘掰。

起初發(fā)現(xiàn)有的請求會返回 HTTP code 502章姓,但是多次復(fù)現(xiàn)之后發(fā)現(xiàn)經(jīng)過轉(zhuǎn)發(fā)的請求都會導(dǎo)致502,構(gòu)造參數(shù)直接訪問相應(yīng)的機器卻能夠正確返回识埋。

背景

我們的應(yīng)用容器是 tomcat凡伊,但是出問題的請求進入的原始機器和轉(zhuǎn)發(fā)后訪問的機器稍有不同,問題機器在 tomcat 前窒舟,同服務(wù)器還有個 nginx 系忙,所以有同事認為是這個多出來的 nginx 做了什么導(dǎo)致出錯。但是直覺上感覺不太可能惠豺,因為 nginx 作為一個標準組件银还,不會無故做這樣的攔截,一定是應(yīng)用代碼有什么問題耕腾。

過程

這里定義如下:

server A见剩,本機有 nginx ;

server B扫俺,本機無 nginx苍苞。

通過 cURL 訪問,能進一步將問題范圍縮小狼纬。

curl 'http://0.0.0.0:80/path?param=a' -H 'Host: www.xxx.com'

直接訪問 server A羹呵,構(gòu)造參數(shù)讓請求不進行轉(zhuǎn)發(fā),此時能拿到正確的返回結(jié)果疗琉,HTTP code 200.

直接訪問 server B冈欢,也不進行轉(zhuǎn)發(fā),此時也能拿到正確的結(jié)果盈简。

問題在于轉(zhuǎn)發(fā)的時候凑耻,server A 轉(zhuǎn)發(fā)到 server B太示,之后再返回給用戶,就一定會 502.

進一步加業(yè)務(wù)日志之后可以發(fā)現(xiàn)香浩,轉(zhuǎn)發(fā)到 server B 的請求类缤,在 server B 上的返回是正確的,HTTP code 200, body 也是正確的邻吭。

Empty reply

再構(gòu)造一個會進行轉(zhuǎn)發(fā)的請求餐弱,觀察 cURL 的輸出,發(fā)現(xiàn)如下:

* Empty reply from server
* Connection #0 to host www.xxx.com left intact
curl: (52) Empty reply from server

同時看到 nginx 的 access log 如下:

"GET /index.jsp HTTP/1.1" 200 0 "-" "curl/7.54.0" "-"

看 nginx 的 log-format囱晴,200 是 http status code膏蚓,后面的 0 是 body_bytes_sent, 也就是說 cURL 接收到的 body size 為 0.

于是開始懷疑,拿到請求轉(zhuǎn)發(fā)的 response 之后畸写,往回寫的時候出了什么問題導(dǎo)致 body 沒有寫驮瞧。

但是實際看了代碼之后,確認已經(jīng)往response.getOutputStream() 中寫入了 body枯芬,外層的 try-catch 也沒有捕捉到異常剧董,沒看到日志。

難不成 outputStream 里的數(shù)據(jù)還會被 tomcat 丟棄破停?

invalid chunked response

翻了下 nginx 的 error log,發(fā)現(xiàn)這個記錄:

[error] 18604#0: *9 upstream sent invalid chunked response while reading upstream, client: 127.0.0.1, server: localhost, request: "GET /index.jsp HTTP/1.1", upstream: "http://127.0.0.1:8080/index.jsp", host: "localhost"

日志中的 IP / host 信息都是本地模擬出來的尉剩,但是錯誤信息是一致的真慢。

于是就很有意思了,意思是 upstream 給 nginx 返回了一個 invalid chunked response理茎,也就是說 tomcat 回寫給 nginx 的數(shù)據(jù)分塊是無效的黑界。

tcpdump chunked data

只能靠 cURL 發(fā)請求來判斷兩端的請求、響應(yīng)數(shù)據(jù)皂林,不夠判定出問題的環(huán)節(jié)朗鸠,即使加上日志也還不夠。最好能夠拿到 tomcat 返回給 nginx 的數(shù)據(jù)础倍。想到了用 tcpdump烛占,直接抓取問題機器的 tcp 數(shù)據(jù),然后本地用 wireshark 分析沟启。

命令如下:

tcpdump -vvv -i any 'port 8080 or port 80'   -w request_to_80

意思是監(jiān)聽所有網(wǎng)卡(any)的數(shù)據(jù)忆家,并且指定 來源或者目標端口是 80 或者 8080 ,保存到文件

這里有兩個注意點:

  • 需要注意權(quán)限問題德迹,可能需要 sudo 權(quán)限或者直接用 root 用戶
  • 指定 any 網(wǎng)卡芽卿,
    • 一般 80 端口沒有問題,請求從 80 端口進入胳搞,響應(yīng)從 80 端口發(fā)出
    • 但是 nginx 和本機 tomcat 的數(shù)據(jù)通信是不經(jīng)過外部網(wǎng)絡(luò)的卸例,走的是虛擬網(wǎng)卡 loopback

端口的問題可以在本機實驗称杨,比如觀察到開發(fā)用的機器上需要監(jiān)聽這兩個網(wǎng)口:

en0
lo0

dump 下來,wireshark 通過 "Follow TCP Stream" 就能看到 HTTP 請求的響應(yīng)數(shù)據(jù)

發(fā)現(xiàn) tomcat 返回給 nginx 的數(shù)據(jù)類似如下:

GET /info?custom=12121 HTTP/1.0
Host: www.xxx.com
Connection: close
User-Agent: curl/7.54.0
Accept: */*
Cookie: xxx=yyy;

HTTP/1.1 200 OK
Set-Cookie: xxx=yyy
Transfer-Encoding: chunked
Connection: close
Vary: Accept-Encoding
Content-Language: zh-CN
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Date: Wed, 23 May 2018 06:55:42 GMT
Connection: close

{"code":200,"data":{...}}

乍一看沒什么問題筷转,再看正常返回 200 的請求:

ET /info?custom=12121 HTTP/1.1
Host: wwx.xxx.com
User-Agent: curl/7.54.0
Accept: */*
Cookie: xxx=yyy;

HTTP/1.1 200 OK
Set-Cookie: xxx=yyy
Transfer-Encoding: chunked
Connection: keep-alive
Keep-Alive: timeout=5
Vary: Accept-Encoding
Content-Language: zh-CN
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 23 May 2018 06:55:07 GMT

526
{"code":200,"data":{...}}
0

再次看了異常請求的 header 和 body 之后姑原,發(fā)現(xiàn)異常請求的 body 開頭沒有數(shù)字。

查看這個 header 的作用及傳輸分塊的示例之后旦装,發(fā)現(xiàn)確實有問題:header 中指定了 chunked页衙,但是 body 并沒有分塊?結(jié)合正常的請求來看阴绢,按照正確的格式將數(shù)據(jù)分塊之后是能被 nginx 接受并處理的店乐。

那么接下來的疑問就是?為什么拿回來的數(shù)據(jù)呻袭,本來應(yīng)該是分塊的眨八,但是被解開了?

為什么正常的響應(yīng)一定是分塊的左电?因為如果不分塊廉侧,header 中不會被中間某個組件加 Transfer-Encoding: chunked這個頭。

接下來就是分析:請求返回過程中篓足,誰對 response body 中的內(nèi)容做了解分塊操作段誊。

HttpClient chunk

轉(zhuǎn)發(fā)的過程是使用 HttpClient 發(fā)起了一個新的 http 請求,拿到結(jié)果之后將 response copy 到 servletResponse 中栈拖,查看 copy response 的那部分代碼连舍,沒有發(fā)現(xiàn)問題。

于是懷疑起了 tomcat 和 HttpClient涩哟,先看 HttpClient索赏。

思路就是看 HttpClient 請求回來之后到輸出之前有沒有 dechunk

寫了個簡單的測試用例,拿到結(jié)果之后輸出:

String str = EntityUtils.toString(response.getEntity());

果然是明文贴彼,沒有分塊潜腻。

看源碼,來自:httpclient 4.3.2:

實際處理流程器仗,正向
org.apache.http.protocol.HttpRequestExecutor#doSendRequest
—> org.apache.http.impl.AbstractHttpClientConnection#receiveResponseEntity
—> org.apache.http.impl.entity.EntityDeserializer#deserialize
—> org.apache.http.impl.entity.EntityDeserializer#doDeserialize

// 準備 inputStream 的時候會判斷是否分塊
org.apache.http.impl.entity.LaxContentLengthStrategy#determineLength 
// 可以看到:header 中出現(xiàn) Transfer-Encoding: chunked 的時候就會處理分塊

org.apache.http.impl.io.ChunkedInputStream#read(byte[], int, int) // 具體的融涣,ChunkedInputStream 會處理 CRLF 分隔符和chunk大小,重載了 read 方法青灼,也就是實際讀取 stream 的時候就會讀到一個 de-chunked 過的 buffer 數(shù)據(jù)

解決方案

最后是將 Transfer-Encoding: chunked這個頭從 response 中去掉暴心,又后續(xù)流程中的 nginx 決定是否去做分塊。在業(yè)務(wù)代碼里也就是不做分塊杂拨,也不提供錯誤信息专普,之前的 response 被 nginx 認為無效也是因為 header 暗示說分塊,但是實際的 body 沒有分塊導(dǎo)致的弹沽。

總結(jié)

這個問題隱藏確實比較深檀夹,前前后后排查了很長時間筋粗,從發(fā)現(xiàn)問題,到懷疑應(yīng)用部署結(jié)構(gòu)炸渡,到懷疑 nginx 娜亿,走了不少彎路。但是收獲也是巨大的蚌堵,對 tcpdump 的應(yīng)用买决,以及 HTTP 標準中的某些細節(jié),HttpClient 對標準的實現(xiàn)吼畏,都有了一定的了解督赤。尤其 tcpdump,有了這個工具泻蚊,攔截到原始的 tcp 報文數(shù)據(jù)躲舌,排查問題就有了鐵證,剩下的就是怎么根據(jù)標準解讀協(xié)議了性雄。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末没卸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子秒旋,更是在濱河造成了極大的恐慌约计,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迁筛,死亡現(xiàn)場離奇詭異病蛉,居然都是意外死亡,警方通過查閱死者的電腦和手機瑰煎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來俗孝,“玉大人酒甸,你說我怎么就攤上這事「陈粒” “怎么了插勤?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長革骨。 經(jīng)常有香客問我农尖,道長,這世上最難降的妖魔是什么良哲? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任盛卡,我火速辦了婚禮,結(jié)果婚禮上筑凫,老公的妹妹穿的比我還像新娘滑沧。我一直安慰自己并村,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布滓技。 她就那樣靜靜地躺著哩牍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪令漂。 梳的紋絲不亂的頭發(fā)上膝昆,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音叠必,去河邊找鬼荚孵。 笑死,一個胖子當(dāng)著我的面吹牛挠唆,可吹牛的內(nèi)容都是我干的处窥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼玄组,長吁一口氣:“原來是場噩夢啊……” “哼滔驾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起俄讹,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤哆致,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后患膛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摊阀,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年踪蹬,在試婚紗的時候發(fā)現(xiàn)自己被綠了胞此。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡跃捣,死狀恐怖漱牵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情疚漆,我是刑警寧澤酣胀,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站娶聘,受9級特大地震影響闻镶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丸升,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一铆农、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狡耻,春花似錦顿涣、人聲如沸波闹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽精堕。三九已至,卻和暖如春蒲障,著一層夾襖步出監(jiān)牢的瞬間歹篓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工揉阎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留庄撮,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓毙籽,卻偏偏與公主長得像洞斯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子坑赡,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理烙如,服務(wù)發(fā)現(xiàn),斷路器毅否,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 第一章 Nginx簡介 Nginx是什么 沒有聽過Nginx亚铁?那么一定聽過它的“同行”Apache吧!Ngi...
    JokerW閱讀 32,650評論 24 1,002
  • 大多數(shù) Nginx 新手都會頻繁遇到這樣一個困惑螟加,那就是當(dāng)同一個location配置塊使用了多個 Nginx 模塊...
    SkTj閱讀 7,610評論 0 12
  • 用戶界面的設(shè)計很容易出現(xiàn)一團亂徘溢。這是因為在整個項目中你可以從某個樣式開始,而以另一個樣式收尾捆探。不管你是專業(yè)導(dǎo)向設(shè)計...
    Jessie閱讀 3,718評論 1 46
  • GCD 概念 GCD 全稱Grand Central Dispatch然爆,是一套C語言API,提供了?種新的方法來進...
    癡人會說夢閱讀 276評論 0 0