一、問題表現(xiàn)
系統(tǒng)自9月底正式運(yùn)行,運(yùn)行至今近半年時(shí)間,于2018年1月15日第一次出現(xiàn)HttpClient請(qǐng)求夯住現(xiàn)象,造成整個(gè)系統(tǒng)訪問非常慢,甚至不可訪問,如下為某一時(shí)刻截圖:

在這也解釋一下何為http請(qǐng)求夯住,夯住(Hang)是指程序仍在運(yùn)行,卡在某個(gè)方法調(diào)用上,沒有返回也沒有異常拋出;卡住時(shí)間從幾秒到幾小時(shí)不等。
另外系統(tǒng)使用Tomcat中間件,對(duì)于Tomcat來說,每一個(gè)進(jìn)來的請(qǐng)求(request)都需要一個(gè)線程,直到該請(qǐng)求結(jié)束。如果同時(shí)進(jìn)來的請(qǐng)求多于當(dāng)前可用的請(qǐng)求處理線程數(shù),額外的線程就會(huì)被創(chuàng)建,直到到達(dá)配置的最大線程數(shù)(maxThreads屬性值)。如果仍就同時(shí)接收到更多請(qǐng)求,這些來不及處理的請(qǐng)求就會(huì)在Connector創(chuàng)建的ServerSocket中堆積起來,直到到達(dá)最大的配置值(acceptCount屬性值)。至此,任何再來的請(qǐng)求將會(huì)收到connection refused錯(cuò)誤,直到有可用的資源來處理它們。
這里我們使用的Tomcat并沒有做調(diào)優(yōu),所以maxThreads默認(rèn)值為200,acceptCount默認(rèn)值為100。(為什么不做優(yōu)化?因?yàn)槟J(rèn)的配置足夠應(yīng)對(duì)生產(chǎn)環(huán)境的情況)
二、問題分析
造成在生產(chǎn)環(huán)境上系統(tǒng)緩慢,甚至不能使用是非常著急的,第一反應(yīng)最短的時(shí)間恢復(fù)用戶的正常使用,最簡單粗暴的方式——重啟應(yīng)用。事實(shí)上這種簡單粗暴的方式并沒有解決問題,在系統(tǒng)重啟短短十幾秒內(nèi)http請(qǐng)求的線程瞬間暴增,系統(tǒng)依然緩慢。
接下來以我當(dāng)時(shí)幾點(diǎn)思考,來闡述分析過程。面對(duì)如此多的request請(qǐng)求并且沒有一個(gè)回收釋放肯定是不正常的。
思考點(diǎn)1:這些請(qǐng)求都是從哪里來的?
(1) 面對(duì)瞬間生成的100多個(gè)http請(qǐng)求線程,是不是受到外部攻擊(因?yàn)檫@臺(tái)服務(wù)器早些時(shí)候受到過攻擊)
(2) 與數(shù)字城管系統(tǒng)有交互的其它系統(tǒng)和內(nèi)部HttpClient請(qǐng)求,是否出現(xiàn)多并發(fā)請(qǐng)求或者有輪詢操作請(qǐng)求我們,其它系統(tǒng)主要有市級(jí)數(shù)字城管系統(tǒng)、前置交換系統(tǒng)、城管通服務(wù)端系統(tǒng),其中市級(jí)數(shù)字城管系統(tǒng)是第三方排查不便,另外兩個(gè)是我們自己維護(hù)方便排查。
思考點(diǎn)2:這些請(qǐng)求狀態(tài)都是什么?
Java程序發(fā)生夯時(shí),應(yīng)該首先使用 jstack 把java進(jìn)程的堆棧信息保存下來 ,供后繼分析使用,jstack -l <pid> > js.txt 可以把pid的堆棧信息保存到文件js.txt中,如下圖某時(shí)刻的堆棧信息:

發(fā)現(xiàn)線程得狀態(tài)都是TIMED_WAITING(線程得各種狀態(tài)在此不詳細(xì)說,網(wǎng)上很多),Java文檔官方定義TIMED_WAITING狀態(tài)為:“一個(gè)線程在一個(gè)特定的等待時(shí)間內(nèi)等待另一個(gè)線程完成一個(gè)動(dòng)作會(huì)在這個(gè)狀態(tài)”。
真實(shí)生活例子:盡管充滿戲劇性,你在面試中做的非常好,驚艷了所有人并獲得了高薪工作。(祝賀你?。┠慊丶腋嬖V你的鄰居你的新工作并表達(dá)你激動(dòng)的心情。你的朋友告訴你他也在同一個(gè)辦公樓里工作。他建議你坐他的車去上班。你想這不錯(cuò)。所以第一天,你走到他的房子。在他的房子前停好你的車。你等了10分鐘,但你的鄰居沒有出現(xiàn)。你繼續(xù)開自己的車去上班,這樣你不會(huì)在第一天就遲到。這就是TIMED_WAITING。
三、 進(jìn)一步分析
接下來怎么辦呢?
(1) 市級(jí)數(shù)字城管系統(tǒng)有多并發(fā)或者輪詢操作?由于第三方系統(tǒng),無法具體斷言,而且運(yùn)行一段時(shí)間以來,也未出現(xiàn),基本上可以排除,也就只能調(diào)侃一下說是他們的原因。
(2) 受外部網(wǎng)絡(luò)攻擊,不斷有請(qǐng)求進(jìn)來?網(wǎng)絡(luò)技術(shù)并不是我們的強(qiáng)項(xiàng),往這方面走明顯得不到根本解決,也可以借助一些殺毒軟件查看網(wǎng)絡(luò)請(qǐng)求來源。
(3) 接下來也就剩下內(nèi)部的HttpClient請(qǐng)求和前置交換系統(tǒng)、城管通服務(wù)端系統(tǒng),會(huì)發(fā)送請(qǐng)求進(jìn)來,由于這些都是我們自己維護(hù),排查起來自由方便。
由于代碼也有些年頭,前置交換系統(tǒng)與城管通系統(tǒng)與數(shù)字城管系統(tǒng)交換使用Apache HttpClient,HttpClient使用3.1版本,也并未使用HttpClient連接池,以及未關(guān)閉無效的連接。大部分人使用HttpClient都是使用類似下面的事例代碼,包括Apache官方的例子也是類似如此。在性能測試過程中,使用HttpClient一次循環(huán)發(fā)起大量請(qǐng)求到服務(wù)器會(huì)使TCP連接被大量占用。

于是,我將HttpClient升級(jí)為4.5版本,并使用PoolingClientConnectionManager連接池管理器,HttpClient可以通過多個(gè)執(zhí)行線程同時(shí)執(zhí)行多個(gè)請(qǐng)求。PoolingClientConnectionManager將會(huì)根據(jù)其配置分配連接,如果某個(gè)路由的所有連接都已經(jīng)被分配出去了,新進(jìn)來的請(qǐng)求將會(huì)阻塞直到某個(gè)連接被釋放回連接池,你可以通過配置http.conn-manager.timeout 這個(gè)參數(shù)來配置新的請(qǐng)求進(jìn)來時(shí)阻塞的超時(shí)時(shí)間,從而避免無限期等待,如果在給定時(shí)間內(nèi)連接沒有被獲取到,那么將會(huì)拋出ConnectionPoolTimeoutException異常。


將所有老代碼的HttpClient請(qǐng)求升級(jí)換成PoolingClientConnectionManager連接池來管理時(shí),初步感覺已經(jīng)解決了HttpClient夯的問題,一波三折系統(tǒng)重啟之后,問題依舊存在。這時(shí)候已經(jīng)很抓狂?。?!
這個(gè)時(shí)候問題沒有得到根本解決是很容易否定之前所做的工作,往往回歸到問題本身又是一個(gè)突破口。本著這個(gè)理念花了點(diǎn)時(shí)間理解了HttpClient連接回收策略。
HttpClient連接回收策略
經(jīng)典阻塞I/O模型的一個(gè)主要缺點(diǎn)就是網(wǎng)絡(luò)socket只有在I/O操作阻塞的情況下才會(huì)對(duì)I/O事件作出反應(yīng)。當(dāng)連接釋放回管理器時(shí),它雖然能夠保持存活,但是它無法監(jiān)控socket的狀態(tài)也無法對(duì)任何I/O事件作出反應(yīng)。如果連接在Server端被關(guān)閉,client端連接無法偵測到連接狀態(tài)的改變并且作出適當(dāng)?shù)幕貞?yīng)。
HttpClient嘗試通過測試連接是否'stale'來緩解這個(gè)問題,stale的意思是連接不再有效因?yàn)樵趫?zhí)行Http請(qǐng)求之前其已經(jīng)被服務(wù)端關(guān)閉。過期連接檢查不是100%可靠的,唯一可行解決方案是提供一個(gè)專用監(jiān)控線程用于回收那些長時(shí)間內(nèi)不活動(dòng)的連接,而該解決方案不會(huì)影響到一個(gè)socket一個(gè)線程的空閑連接模型。監(jiān)控線程可以定期調(diào)用 ClientConnectionManager#closeExpiredConnections()方法來關(guān)閉所有過期的連接,同時(shí)從連接池中回收已經(jīng)被關(guān)閉的連接,也可以有選擇性的調(diào)用 ClientConnectionManager#closeIdleConnection()方法去關(guān)閉那些在給定時(shí)間范圍內(nèi)空閑的連接。
通過增加一個(gè)獨(dú)立線程專門回收不活動(dòng)的連接,如下示例代碼:


至此,系統(tǒng)HttpClient夯住問題基本解決。
四、 總結(jié)
a) 理解一下HttpClient這樣設(shè)計(jì)的理由: socket重用,keepAlive協(xié)議的支持等,保證上一次數(shù)據(jù)不會(huì)對(duì)新的請(qǐng)求有影響。
b) Thread.interrpt()處理,只會(huì)在Thread處于sleep或者wait狀態(tài)才會(huì)被喚醒(api的描述)。而且該方法的調(diào)用并不自動(dòng)產(chǎn)生InterruptedException異常,一般是需要自己判斷Thread.isInterrupted(),然后throw異常。 我們目前使用的一些jdk cocurrent類比如future.cancel也是類似處理。
c) OkHttp也許逐步替代Apache的HttpClient,未來可以嘗試使用。
優(yōu)點(diǎn)
支持SPDY, 可以合并多個(gè)到同一個(gè)主機(jī)的請(qǐng),使用連接池技術(shù)減少請(qǐng)求的延遲(如果SPDY是可用的話) ,
使用GZIP壓縮減少傳輸?shù)臄?shù)據(jù)量,緩存響應(yīng)避免重復(fù)的網(wǎng)絡(luò)請(qǐng)求、攔截器等等。
缺點(diǎn)
第一缺點(diǎn)是消息回來需要切到主線程,主線程要自己去寫,第二傳入調(diào)用比較復(fù)雜。