HttpClient偶爾報NoHttpResponseException: xxx failed to respond
背景描述
調(diào)用底層服務(wù)偶爾會報以下錯誤
org.apache.http.NoHttpResponseException: submit.10690221.com:9012 failed to respond
at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:141)
....
第一次碰到拒课,先google一下切揭,發(fā)現(xiàn)不少相同的情況苇倡,講的也很不錯放刨,但是呢熔号,我想自己復(fù)現(xiàn)一下秕磷,并且自己去分析并解決簸州,這樣能更好的去理解 網(wǎng)絡(luò) 這東西
復(fù)現(xiàn)方法
這個怎么復(fù)現(xiàn)呢缭付,通過google得知柿估,這個只會在服務(wù)器端keep-alive剛好過期的時間我們進(jìn)行訪問才能大概率復(fù)現(xiàn),方法如下:
wireshark進(jìn)行抓包得出底層服務(wù)器的keep-alive時間
寫一段程序陷猫,用于探測底層服務(wù)器的keep-alive秫舌,代碼如下:
@Test
public void test121() throws Exception {
String url = "http://xxxxxxx:9012/hy/json";
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost request = new HttpPost(url);
httpClient.execute(request, response -> {
String content = EntityUtils.toString(response.getEntity());
System.out.println(content);
return content;
});
Thread.sleep(1000000);
}
開啟wireshark進(jìn)行抓包,執(zhí)行程序直到下圖出現(xiàn)即可停止
重點看左下角的紅色框绣檬,時間相差65秒左右,沒錯從而可以得知底層服務(wù)器的keep-alive 是 65秒娇未,也就是當(dāng)一個連接socket 65秒內(nèi)沒有數(shù)據(jù)交互墨缘,底層服務(wù)器就會認(rèn)為這個連接可以關(guān)閉了,因此才會在3分36秒進(jìn)行揮手操作發(fā)送一個FIN包,這時我們稍微改造一下這個程序镊讼,如下:
@Test
public void test121() throws Exception {
String url = "http://xxxxxxx:9012/hy/json";
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost request = new HttpPost(url);
while (true) {//加了一個死循環(huán) ^_^
httpClient.execute(request, response -> {
String content = EntityUtils.toString(response.getEntity());
System.out.println(content);
return content;
});
Thread.sleep(65000); //關(guān)鍵是這里平夜,設(shè)置和底層服務(wù)器keep-alive相同
}
}
相比第一個蝶棋,有兩個改動
- 加了一個循環(huán)
- 每次調(diào)用的間隔改成和底層服務(wù)器相同的65秒
我們清空wireshark,運(yùn)行該程序抓包忽妒,結(jié)果如下:
問題分析
首先我們分析一下抓包結(jié)果
- 紅色框1:前3個請求是建立連接的過程玩裙,三次握手,接著4個請求就是client和server的數(shù)據(jù)交互段直,著重看最后四個請求
- 9012 -> 59233 [FIN, ACK]:服務(wù)器主動進(jìn)行關(guān)閉吃溅,給client發(fā)送了FIN包
- 59233 -> 9012 [ACK]:client進(jìn)行回應(yīng)ACK包
- 69233 -> 9012 [FIN, ACK]:按照四次揮手原則,client發(fā)現(xiàn)目前數(shù)據(jù)已經(jīng)發(fā)送完畢了鸯檬,因此也發(fā)出FIN包
- 9012 -> 59233 [RST]:服務(wù)器直接返回一個RST
- 紅色框2:同2
- 紅色框3:前面的7個步驟都是相同的决侈,建立連接,數(shù)據(jù)交互喧务,區(qū)別唯獨在于綠色框
- 9012 -> 59233 POST /hy/json: client認(rèn)為服務(wù)器端可用颜及,因此給服務(wù)器發(fā)送數(shù)據(jù)
- 9012 -> 59233 [FIN, ACK]:服務(wù)器認(rèn)為此連接已經(jīng)失效,因為超過了65的keep-alive時間蹂楣,主動進(jìn)行關(guān)閉,給client發(fā)送了FIN包
- 59233 -> 9012 [ACK]:client進(jìn)行回應(yīng)ACK包
- 69233 -> 9012 [FIN, ACK]:按照四次揮手原則讯蒲,client發(fā)現(xiàn)目前數(shù)據(jù)已經(jīng)發(fā)送完畢了痊土,因此也發(fā)出FIN包
- 9012 -> 59233 [RST]:服務(wù)器直接返回一個RST 通過Seq=188,可判斷這條是給【9012 -> 59233 POST /hy/json】這個請求回的
- 9012 -> 59233 [RST]:服務(wù)器直接返回一個RST 通過Seq=189墨林,可判斷這條是給【69233 -> 9012 [FIN, ACK]】回的
- 9012 -> 59233 [RST]:服務(wù)器直接返回一個RST 通過Seq=189赁酝,同6
通過分析抓包數(shù)據(jù)犯祠,得出結(jié)果是,當(dāng)client客戶端認(rèn)為這條Socket連接有用酌呆,這時服務(wù)器端卻認(rèn)為該Socket連接無用衡载,并主動關(guān)閉,就會報錯,屬于臨界值沒有處理好的
這時有人就說了隙袁,為什么前兩次就沒有問題呢痰娱,原因是HttpClient會進(jìn)行連接過期是否可用的檢查,那么也就能理解這是httpclient的一個bug菩收,即使httpclient有做這么一件事情梨睁,但是由于網(wǎng)絡(luò)I/O原因,導(dǎo)致httpclient認(rèn)為一個關(guān)閉了的連接是有效的娜饵,才報了這個錯誤
接下來我們看看HttpClient為什么會復(fù)用一個已經(jīng)被關(guān)閉的連接
由于HttpClient代碼有點多坡贺,為了方便快速定位縮小范圍, 我這邊開啟了debug箱舞,并對兩者的日志進(jìn)行了分析
左邊日志是正常交互遍坟、右邊是報錯了
我這邊簡化了一下日志,通過仔細(xì)分析HttpClient打印的debug日志,可發(fā)現(xiàn)左邊正常交互日志 打印了一串 "end of stream" 后進(jìn)行了連接的重新建立晴股, connection established 愿伴,而右邊錯誤日志打印了一串 "[read] I/O error: Read timed out" 后沒有進(jìn)行連接的重新建立,因此就報錯了
那么可以通過打印 "[read] I/O error: Read timed out"日志的上下文日志縮小 排查代碼的范圍队魏,上文日志 Connection request公般,下文日志 Connection leased,進(jìn)行代碼定位
基本上定位到了PooingHttpClientConnectionManager.java這個類胡桨,那么進(jìn)行代碼跟蹤吧
追蹤到了 AbstractConnPool.java類官帘,那么這段代碼什么意思呢,這個就是進(jìn)行連接是否能夠復(fù)用的檢查代碼
對validateAfterInactivity進(jìn)行判斷昧谊,這個是服務(wù)器keep-alive的值
- leasedEntry.getUpdated() + validateAfterInactivity <= System.currentTimeMillis():如果連接的最后一次使用時間 + 服務(wù)器keep-alive的時間 小于等于當(dāng)前時間刽虹,那么就認(rèn)為該連接可能已經(jīng)失效了
- !validate(leasedEntry): 因此會進(jìn)行連接是否失效的檢查
跟進(jìn)去看看
最終找到"end of stream" and "[read] I/O error: Read timed out" 打印的地方
然后回到如下圖代碼:
可以看到
- 當(dāng)bytesRead 值為 -1 時,返回true,那么HttpClient就會認(rèn)為該連接失效了呢诬,不能夠復(fù)用涌哲,并進(jìn)行清理操作,
- 當(dāng)拋出異常是ShockTimeoutException時會返回false尚镰, 那么HttpClient就會認(rèn)為該連接可復(fù)用
分析到這阀圾,相信大部分人都已經(jīng)知道為什么會保證錯了,不過還是強(qiáng)烈建議自己動手分析一下狗唉,另外大家可去了解一下初烘,為什么會輸出"end of stream" and "[read] I/O error: Read timed out"兩種不同的結(jié)果,快去暢游底層Socket編程相關(guān)的原理吧,這有助于你更加理解
解決方案
其實當(dāng)你知道原因后肾筐,也能想出對應(yīng)的解決方案哆料,不過我這邊還是收集列出來了一些
- 禁用HttpClient的連接復(fù)用(有點扯淡)
- 重試方案:http請求使用重發(fā)機(jī)制,捕獲NohttpResponseException的異常吗铐,重新發(fā)送請求东亦,重發(fā)3次后還是失敗才停止
- 根據(jù)keep Alive時間,調(diào)整validateAfterInactivity小于keepAlive Time,但這種方法依舊不能避免同時關(guān)閉
- 系統(tǒng)主動檢查每個連接的空閑時間唬渗,并提前自動關(guān)閉連接典阵,避免服務(wù)端主動斷開
推薦使用重試方案