TCP連接不釋放承绸,應(yīng)用產(chǎn)生大量CLOSE_WAIT狀態(tài)TCP

一裸影、起源

23年元旦期間,大家都沉浸在一片祥和的過(guò)節(jié)氣氛當(dāng)中军熏。

“滴滴滴”轩猩,這頭同事的電話響起,具體說(shuō)些什么我也沒(méi)太在意荡澎,但見(jiàn)同事接完電話之后展現(xiàn)出了一副懊惱夾雜著些許不耐煩的表情均践。

我不解問(wèn)道:“怎么了?”

同事:“剛剛運(yùn)營(yíng)反饋系統(tǒng)開(kāi)始刷白屏了摩幔,所有的請(qǐng)求一直處于pending狀態(tài)彤委。”

此刻我的大腦開(kāi)始飛速旋轉(zhuǎn)或衡,界面白屏焦影?前端是不是有一個(gè)初始化的請(qǐng)求?這個(gè)初始化的請(qǐng)求有問(wèn)題導(dǎo)致前端沒(méi)有往下運(yùn)行并渲染頁(yè)面封断?

我:“已經(jīng)登陸進(jìn)系統(tǒng)的用戶可以正常使用嗎斯辰?”

同事:“也不可以”。

那就不是初始化請(qǐng)求失敗的問(wèn)題了坡疼。

再往下分析椒涯,應(yīng)用進(jìn)程應(yīng)該沒(méi)有宕機(jī),否則接口會(huì)快速響應(yīng)404回梧。

我:“所有接口都阻塞住了還是單個(gè)接口的問(wèn)題废岂,最近有沒(méi)有項(xiàng)目發(fā)版?”

同事:“運(yùn)營(yíng)反饋是所有接口都有阻塞情況狱意,這段時(shí)間一直沒(méi)有發(fā)版”湖苞。

那就怪了,近期沒(méi)有發(fā)版详囤,而且所有接口均有問(wèn)題财骨。

到此,基本可以判定出藏姐,這個(gè)問(wèn)題與業(yè)務(wù)無(wú)關(guān)隆箩,而是類(lèi)似基礎(chǔ)設(shè)施出現(xiàn)問(wèn)題導(dǎo)致了,比如網(wǎng)絡(luò)問(wèn)題羔杨、Java GC問(wèn)題捌臊、線程池線程短缺出現(xiàn)業(yè)務(wù)饑餓問(wèn)題等。

這種問(wèn)題勾起了我的興趣兜材,我決定自己上手問(wèn)題理澎,開(kāi)始排查…

二、表象

打開(kāi)系統(tǒng)曙寡,發(fā)現(xiàn)瀏覽器發(fā)出的接口確實(shí)都處于“pending”狀態(tài)糠爬。(Ps:pending是瀏覽器發(fā)出請(qǐng)求之后的一種狀態(tài),代表該請(qǐng)求還未得到服務(wù)器響應(yīng)举庶,如果接口RT延遲高执隧,你就有可能經(jīng)常看到這種狀態(tài))户侥;

接口處于pending狀態(tài)镀琉,我在本地使用telnet ip port命令連接應(yīng)用服務(wù)器端口也是正常的,到此說(shuō)明網(wǎng)絡(luò)與應(yīng)用進(jìn)程都是完好的添祸。

三滚粟、排查

打開(kāi)監(jiān)控系統(tǒng)查看了Java GC耗時(shí)、次數(shù)兩項(xiàng)指標(biāo)刃泌,發(fā)現(xiàn)和平時(shí)也沒(méi)什么不一樣凡壤。

查看了系統(tǒng)調(diào)用量發(fā)現(xiàn)與往常相似,沒(méi)有什么流量突刺耙替。

同事查看了那段時(shí)間的系統(tǒng)日志也未發(fā)現(xiàn)明顯的錯(cuò)誤亚侠。

我手里的鼠標(biāo)一直向下滑動(dòng)著,我和同事緊張的看著電腦俗扇,緊盯著各項(xiàng)指標(biāo)硝烂,希望能看出些端倪,并能以此為突破口铜幽。

果然滞谢,還是看到了…

下圖是近幾天的TCP連接數(shù)曲線圖:


TCP漲勢(shì)圖.jpeg

發(fā)現(xiàn)TCP連接一直處于只漲不降的趨勢(shì)(除了中間幾次的人為重啟機(jī)器)串稀。

我進(jìn)入服務(wù)器,熟練的敲起Linux命令:

[root] $ **netstat -na | grep '8089' | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
LISTEN 8
CLOSE_WAIT 35
ESTABLISHED 108
SYN_SENT 1
TIME_WAIT 9**

[root] $ netstat -ntp | grep 'CLOSE_WAIT'
tcp 1 0 本機(jī)ip:端口  nginx機(jī)器  CLOSE_WAIT  pid/java
....

觀察了十分鐘狮杨,發(fā)現(xiàn)CLOSE_WAT狀態(tài)的TCP連接數(shù)一直處于增長(zhǎng)的狀態(tài)母截。并且觀察到所有的CLOSE_WAIT狀態(tài)的TCP都是與Nginx建立的,問(wèn)了運(yùn)維同學(xué)橄教,這臺(tái)Nginx是這臺(tái)后端應(yīng)用的代理清寇。

同事:“啊,原來(lái)是TCP連接數(shù)搗鬼护蝶,一直上漲但不釋放”华烟。

這一點(diǎn)和瀏覽器發(fā)出的接口一直處于Pending狀態(tài)剛好吻合起來(lái)了。一定是代碼出現(xiàn)了什么問(wèn)題持灰,導(dǎo)致接口延遲上升盔夜,所有的HTTP請(qǐng)求映射到底層就是TCP連接,應(yīng)用服務(wù)器一直沒(méi)有返回內(nèi)容搅方,那么TCP自然也不會(huì)釋放了(這里我們先不考慮現(xiàn)代HTTP協(xié)議的KeepAlive機(jī)制)比吭。

只見(jiàn)同事為了止血抓緊去重啟機(jī)器了,好讓這些TCP連接被強(qiáng)制關(guān)閉姨涡。

但我還有一點(diǎn)疑問(wèn)衩藤,TCP未能及時(shí)釋放確實(shí)有問(wèn)題不假,但即便是這樣涛漂,區(qū)區(qū)500個(gè)TCP連接赏表,也不至于達(dá)到操作系統(tǒng)的閾值,要知道操作系統(tǒng)對(duì)端口的限制理論上可以達(dá)到65535個(gè)匈仗。

那一定是有一個(gè)短板的中間件因達(dá)到了閾值而限制了整個(gè)系統(tǒng)瓢剿,很快我便想到了Tomcat,因?yàn)門(mén)omcat一定是每個(gè)業(yè)務(wù)接口都繞不過(guò)的中間件悠轩。

雖然機(jī)器已經(jīng)重啟了间狂,但我們有服務(wù)治理相關(guān)的平臺(tái)可以調(diào)出任意一個(gè)時(shí)間點(diǎn)的線程狀態(tài),不看不知道火架,一看嚇一跳鉴象,所有以“http-nio-xxx”為前綴的線程全部阻塞在方法:SocketInputStream.read0方法中。熟悉這個(gè)前綴線程的人都知道何鸡,這是Tomcat線程池纺弊。

我:”查一下日志,看看接口AOP有沒(méi)有捕捉到請(qǐng)求信息骡男?“

同事:“捕捉到了”

我:“隨機(jī)抽取幾個(gè)請(qǐng)求淆游,看看接口AOP是否捕捉到了接口響應(yīng)信息”

同事抓緊去查了,不一會(huì)兒驚訝的說(shuō)道:“老朱,還真有很多請(qǐng)求接口響應(yīng)AOP沒(méi)有捕獲到犹菱,那就是說(shuō)這些請(qǐng)求一直沒(méi)有處理完”拾稳。

同事又說(shuō)道:“老朱,這個(gè)項(xiàng)目比較特殊已亥,項(xiàng)目中所有的接口基本只做了透?jìng)鞯氖虑樾芾担?xiàng)目只是做簡(jiǎn)單的數(shù)據(jù)加工,然后請(qǐng)求其他系統(tǒng)虑椎,最后針對(duì)結(jié)果集再進(jìn)行過(guò)濾、轉(zhuǎn)換返回給前端”

我:“啊俱笛,原來(lái)是這樣捆姜,目前使用的HTTP客戶端是什么?”

同事:“Apache HttpClient”

我:“Apache HttpClient迎膜?三個(gè)超時(shí)時(shí)間都設(shè)置了吧泥技?“

同事:“嗯,都設(shè)置了磕仅,connectTimeout珊豹、socketTimeout、connectionRequestTimeout都根據(jù)項(xiàng)目的性質(zhì)調(diào)試過(guò)了榕订,并且數(shù)據(jù)流也都妥善處理了”

我有些不解店茶,所有的超時(shí)時(shí)間都設(shè)置好了,那為什么還會(huì)有大量的Tomcat線程阻塞在SocketInputstream.read0方法中呢劫恒,最終導(dǎo)致TCP持續(xù)飆高贩幻。

我和同事都陷入了困境,進(jìn)入兩難處境两嘴,不知道如何繼續(xù)排查丛楚,機(jī)器重啟確實(shí)可以強(qiáng)制釋放TCP但是這方法治標(biāo)不治本。

同事:“啊憔辫,這趣些,怎么會(huì)這樣”

我:“怎么了?贰您!吞吞吐吐”

同事:“我記得之前明明在這些應(yīng)用里都加了接口AOP日志坏平,剛剛看到我們項(xiàng)目有日志,但是其他系統(tǒng)中居然沒(méi)有日志…

至此我有個(gè)大膽的猜測(cè)枉圃,請(qǐng)求進(jìn)入項(xiàng)目之后功茴,項(xiàng)目加工請(qǐng)求并轉(zhuǎn)發(fā)給其他項(xiàng)目,但轉(zhuǎn)發(fā)給系統(tǒng)項(xiàng)目的這段網(wǎng)絡(luò)出現(xiàn)了什么問(wèn)題(因?yàn)槠渌到y(tǒng)并未收到請(qǐng)求)孽亲,又因?yàn)榉N種原因?qū)е鲁瑫r(shí)時(shí)間并未生效坎穿,慢慢的項(xiàng)目中所有Tomcat線程都阻塞在了SocketInputstream中的read0方法中,最后Tomcat無(wú)空閑線程可去處理用戶提交的新的請(qǐng)求。

但是什么情況下Apache HttpClient中的超時(shí)時(shí)間不生效呢玲昧?

直到我看到了一篇JDK官網(wǎng)說(shuō)明:

Bug ID: JDK-8172578 SocketInputStream.socketRead0 can hang even with soTimeout set

SocketInputStream.socketRead0 can hang even with soTimeout set

在第二篇文章中有詳細(xì)描述了SocketTimeOut參數(shù)失效的原因栖茉,大致意思是:因?yàn)镺S select方法存在虛假喚醒的問(wèn)題導(dǎo)致線程提前進(jìn)行讀取,但又因?yàn)椴](méi)有實(shí)際數(shù)據(jù)因此造成了讀取阻塞孵延。

而我們的JDK版本是1.8.0_91吕漂,也就是低于JDK修復(fù)版本,當(dāng)OS發(fā)出虛假喚醒之后尘应,導(dǎo)致Java線程進(jìn)入阻塞讀取狀態(tài)惶凝,最終導(dǎo)致socketTimeout失效。

事情到這就解釋的通了犬钢,我再來(lái)根據(jù)現(xiàn)象與各項(xiàng)指標(biāo)結(jié)合起來(lái)從頭到尾的闡述一波苍鲜。

應(yīng)用拓?fù)鋱D.png

這里先祭出項(xiàng)目的拓?fù)鋱D。

應(yīng)用A是這次案發(fā)地點(diǎn)玷犹,應(yīng)用A的大多數(shù)功能只是做請(qǐng)求透?jìng)鞴ぷ骰焯希瑢⒂脩粽?qǐng)求的參數(shù)做過(guò)濾、篩選歹颓、添油加醋發(fā)送給應(yīng)用B坯屿。

根據(jù)上述的拓?fù)湫再|(zhì),以應(yīng)用A為中心會(huì)有兩段請(qǐng)求巍扛,這里標(biāo)識(shí)為A段领跛、B段。

案發(fā)時(shí)間前幾個(gè)小時(shí)电湘,起初系統(tǒng)穩(wěn)如老狗隔节。應(yīng)用A穩(wěn)定的處理著Nginx轉(zhuǎn)發(fā)來(lái)的前端請(qǐng)求,并包裝發(fā)送給應(yīng)用B寂呛,然后將應(yīng)用B返回來(lái)的數(shù)據(jù)進(jìn)行篩選怎诫、處理然后返回給用戶。

然而好景不長(zhǎng)贷痪,B段出現(xiàn)了一些網(wǎng)絡(luò)丟包幻妓、抖動(dòng)等情況變得開(kāi)始不穩(wěn)定了,搭配上JDK8U152版本之前可能會(huì)受到操作系統(tǒng)的select虛假喚醒的情況劫拢,慢慢的Tomcat很多線程都阻塞在了SocketInputstreamread0方法上肉津,這一點(diǎn)和之前看到的線程Dump信息一致。

看了眼項(xiàng)目對(duì)Tomcat的線程數(shù)配置:server.tomcat.max-threads=**400**舱沧,而線程Dump出的信息中剛好有400個(gè)線程阻塞在了read0方法中妹沙。

嗯,沒(méi)錯(cuò)熟吏,就是這樣距糖,最后Tomcat成為了整個(gè)項(xiàng)目的瓶頸玄窝。

四、解決

知道了悍引,問(wèn)題就非常方便解決了恩脂,我主要采取了三個(gè)措施:

  1. 將項(xiàng)目中使用的BIO HttpClient化成NIO的方式:HttpAsyncClient
  2. 升級(jí)JDK為JDK8u152版本
  3. 添加耗時(shí)兜底方案,若超過(guò)SocketTimeout時(shí)間趣斤,應(yīng)用A請(qǐng)求B應(yīng)用還未結(jié)束俩块,則手動(dòng)關(guān)閉TCP連接,Tomcat主線程繼續(xù)向下運(yùn)行

其中第二點(diǎn)不用說(shuō)浓领,我通過(guò)一段偽代碼來(lái)說(shuō)明一下1和3.

public static String getHttpClientResult(CloseableHttpAsyncClient asyncHttpClient, HttpRequestBase httpMethod) throws IOException {
                String content = "";        
                // TCP強(qiáng)制斷開(kāi)標(biāo)志
        boolean abortFlag = true;
                HttpResponse response = null;
        // 執(zhí)行請(qǐng)求
        Future<HttpResponse> httpResponseFuture = asyncHttpClient.execute(httpMethod, null);

        try {
            httpResponse = httpResponseFuture.get(SOCKET_TIMEOUT, TimeUnit.MILLISECONDS);
            abortFlag = false;
                        // 獲取返回結(jié)果
                        if (httpResponse.getEntity() != null) {
                content = EntityUtils.toString(httpResponse.getEntity(), ENCODING);
            }

        } catch (InterruptedException e) {
            // 線程被打斷玉凯,重新設(shè)置標(biāo)志并且向下運(yùn)行
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            // http執(zhí)行異常,包括readTimeOut联贩、connectTimeOut
            log.error("當(dāng)前線程:{},請(qǐng)求異常,發(fā)送TCP RST包強(qiáng)制斷開(kāi)", Thread.currentThread().getName());
        } catch (TimeoutException e) {
            /**
             * future get 超時(shí) 兜底方案壮啊,避免主線程阻塞在read方法中
             * @see java.net.SocketInputStream#socketRead0(java.io.FileDescriptor, byte[], int, int, int)
             * 若超時(shí)則發(fā)送RST包以強(qiáng)制連接雙方斷開(kāi)TCP
             */
            log.error("當(dāng)前線程:{},超時(shí):{}秒,仍未獲取到數(shù)據(jù),發(fā)送RST包", Thread.currentThread().getName(), SOCKET_TIMEOUT);
        } finally {
            if (abortFlag) {
                httpMethod.abort();
            }
              // 釋放資源
              if (!Objects.isNull(httpResponse)
                && !Objects.isNull(httpResponse.getEntity())
                && !Objects.isNull(httpResponse.getEntity().getContent())) {
                   httpResponse.getEntity().getContent().close();
                }
        }
                
        return abortFlag ? null : content;
    }

代碼優(yōu)化完畢,趕緊發(fā)版撑蒜,目前我的這位同事在也沒(méi)有被運(yùn)維‘騷擾’過(guò)了,目前TCP升降趨勢(shì)穩(wěn)定喜人 玄渗,如下:


修復(fù)后的TCP漲勢(shì)圖.jpeg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末座菠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子藤树,更是在濱河造成了極大的恐慌浴滴,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岁钓,死亡現(xiàn)場(chǎng)離奇詭異升略,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)屡限,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)品嚣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人钧大,你說(shuō)我怎么就攤上這事翰撑。” “怎么了啊央?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵眶诈,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我瓜饥,道長(zhǎng)逝撬,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任乓土,我火速辦了婚禮宪潮,結(jié)果婚禮上溯警,老公的妹妹穿的比我還像新娘。我一直安慰自己坎炼,他們只是感情好愧膀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谣光,像睡著了一般檩淋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上萄金,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天蟀悦,我揣著相機(jī)與錄音,去河邊找鬼氧敢。 笑死日戈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孙乖。 我是一名探鬼主播浙炼,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼唯袄!你這毒婦竟也來(lái)了弯屈?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恋拷,失蹤者是張志新(化名)和其女友劉穎资厉,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體蔬顾,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宴偿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诀豁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窄刘。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖且叁,靈堂內(nèi)的尸體忽然破棺而出都哭,到底是詐尸還是另有隱情,我是刑警寧澤逞带,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布欺矫,位于F島的核電站,受9級(jí)特大地震影響展氓,放射性物質(zhì)發(fā)生泄漏穆趴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一遇汞、第九天 我趴在偏房一處隱蔽的房頂上張望未妹。 院中可真熱鬧簿废,春花似錦、人聲如沸络它。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)化戳。三九已至单料,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間点楼,已是汗流浹背扫尖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掠廓,地道東北人换怖。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蟀瞧,于是被迫代替她去往敵國(guó)和親沉颂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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