一萬八千條線程,線程為啥釋放不了?
一萬八千條線程,線程為啥釋放不了?
大家好,我是魔性的茶葉,今天和大家?guī)淼氖俏以诠纠锩媾挪榈牧硪粋€性能問題的過程和結(jié)果,相當(dāng)有意思,分享給大家,為大家以后有可能的排查增加一些些思路。當(dāng)然,最重要的是排查出來問題,解決問題的成就感和解決問題的快樂,拽句英文,那就是 its all about fun。
噢對了,謝絕沒有同意的轉(zhuǎn)載。
事情發(fā)生在某個艷陽高照的下午,我正在一遍打瞌睡一邊寫無聊的curd。坐在我身邊的鄭網(wǎng)友突然神秘一笑。 "有個你會感興趣的東西,要不要看看",他笑著說,臉上帶著自信揣測掌握我的表情。
我還以為他準(zhǔn)備說啥點杯奶茶,最近有啥有意思的游戲,放在平時我可能確實感興趣,可是昨天晚上我凌晨二點才睡,中午休息時間又被某個無良領(lǐng)導(dǎo)叫去加班,困得想死,現(xiàn)在只想趕緊碼完代回家睡覺。
"沒興趣",我說。他臉上的表情就像被一只臭皮鞋梗住了喉嚨,當(dāng)然那只臭皮鞋大概率是我。
"可是這是之前隔壁部門那個很多線程的問題,隔壁部門來找我們了",他強(qiáng)調(diào)了下。
"噢!是嗎,那我確實有興趣",我一下子來了精神,趴過去看他的屏幕。屏幕上面是他和隔壁部門的聊天,隔壁部門的同事說他們看了比較久時間都找不到問題,找我們部門看看。讓我臊的不行的是這貨居然直接還沒看問題,就開始打包票,說什么"我們部門是排查這種性能問題的行家"這種高斯林看了都會臉紅的話。
"不是說沒興趣嗎?"他嘿嘿一笑。我尬笑了一下,這個問題確實糾結(jié)我很久了,因為一個星期前運維同事把隔壁部門的應(yīng)用告警發(fā)到了公共群,一下子就吸引到了我:
這個實例的線程數(shù)去到差不多兩萬(對,就是兩萬,你沒看錯)的線程數(shù)量,1w9的線程處于runnable狀態(tài)。說實話,這個確實挺吸引我的 ,我還悄悄地地去下載了線程快照,但是這是個棘手的問題,只看線程快照完全看不出來,因為gitlab的權(quán)限問題我沒有隔壁部門的代碼,所以只能作罷。但是這個問題就如我的眼中釘,拉起了我的好奇心,我隔一會就想起這個問題,我整天都在想怎么會導(dǎo)致這么多條線程,還有就是jvm真的扛得住這么多條線程?
正好這次隔壁部門找到我們,那就奉旨除bug,順便解決我的困惑。
等待代碼下拉的過程,我打開skywalking觀察這個應(yīng)用的狀態(tài)。這次倒沒到一萬八千條線程,因為找不到為啥線程數(shù)量這么多的原因,每次jvm快被線程數(shù)量撐破的時候運維就重啟一遍,所以這次只有接近6000條,哈哈。
可以看到應(yīng)用的線程在一天內(nèi)保持增加的狀態(tài),而且是一直增加的趨勢。應(yīng)用沒有fgc,只有ygc,配合服務(wù)的調(diào)用數(shù)量很低,tomcat幾乎沒有繁忙線程來看并不是突發(fā)流量。jvm的cpu居高不下,很正常,因為線程太多,僧多粥少的搶占時間片,不高才怪。
拿下線程快照導(dǎo)入,導(dǎo)入imb analyzer tool查看線程快照。
直接看最可疑的地方,有1w9千條的線程都處于runnbale線程,并且都有相同的堆棧,也就是說,大概率是同一段代碼產(chǎn)生的線程:
這些線程的名字都以I/O dispatcher 開頭,翻譯成中文就是io分配者,說實話出現(xiàn)在dubbo應(yīng)用里面我是一點都不意外,可是我們這是springmvc應(yīng)用,這個代碼堆??瓷先ケ容^像一種io多路輪詢的任務(wù),用人話說就是一種異步任務(wù),不能找到是哪里產(chǎn)生的這種線程。說實話這個線程名也比較大眾,網(wǎng)上一搜一大把,也沒啥一看就能定位到的問題。
這種堆棧全是源碼沒有一點業(yè)務(wù)代碼堆棧的問題最難找了。
我繼續(xù)往下看線程,試圖再找一點線索。接著我找到了大量以pool-命名開頭的線程,雖然沒有1w9千條這么多,也是實打?qū)崕装贄l:
這兩條線程的堆棧很相近,都是一個類里面的東西,直覺告訴我是同一個問題導(dǎo)致的??吹竭@個pool開頭,我第一個反應(yīng)是有人用了類似new fixThreadPool()這種api,這種api新建出來的線程池因為沒有自定義threadFactory,導(dǎo)致建立出來的線程都是pool開頭的名字。
于是我在代碼中全局搜索pool這個單詞,想檢查下項目中的線程池是否設(shè)置有誤:
咦,這不是剛剛看到的堆棧里面的東西嗎。雖然不能非常確定是不是這里,但是點進(jìn)去看看又不會掉塊肉。
這是個工具類,我直接把代碼拷過來:
ini復(fù)制代碼private static class HttpHelperAsyncClient { ?? ? ? ?private CloseableHttpAsyncClient httpClient; ?? ? ? ?private PoolingNHttpClientConnectionManager cm; ?? ? ? ?private HttpHelperAsyncClient() {} ?? ? ? ?private DefaultConnectingIOReactor ioReactor; ?? ? ? ?private static HttpHelperAsyncClient instance; ?? ? ? ?private Logger logger = LoggerFactory.getLogger(HttpHelperAsyncClient.class); ?? ? ? ? ? ? ?public static HttpHelperAsyncClient getInstance() { ? ? ? ? ? ? ? ? ?instance = HttpHelperAsyncClientHolder.instance; ?? ? ? ? ? ? ? ?try { ?? ? ? ? ? ? ? ? ? ?instance.init(); ?? ? ? ? ? ? ? ?} catch (Exception e) { ?? ? ? ? ? ? ? ? ? ? ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ?return instance; ?? ? ? ?} ? ? ? ? ?private void init() throws Exception { ? ? ? ? ? ? ?ioReactor = new DefaultConnectingIOReactor(); ?? ? ? ? ? ?ioReactor.setExceptionHandler(new IOReactorExceptionHandler() { ?? ? ? ? ? ? ? ?public boolean handle(IOException ex) { ?? ? ? ? ? ? ? ? ? ?if (ex instanceof BindException) { ?? ? ? ? ? ? ? ? ? ? ? ?return true; ?? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ?return false; ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?public boolean handle(RuntimeException ex) { ?? ? ? ? ? ? ? ? ? ?if (ex instanceof UnsupportedOperationException) { ?? ? ? ? ? ? ? ? ? ? ? ?return true; ?? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ?return false; ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ?}); ? ? ? ? ? ? ?cm=new PoolingNHttpClientConnectionManager(ioReactor); ?? ? ? ? ? ?cm.setMaxTotal(MAX_TOTEL); ?? ? ? ? ? ?cm.setDefaultMaxPerRoute(MAX_CONNECTION_PER_ROUTE); ?? ? ? ? ? ?httpClient = HttpAsyncClients.custom() ?? ? ? ? ? ? ? ? ? ? ? ? ? ?.addInterceptorFirst(new HttpRequestInterceptor() { ? ? ? ? ? ? ? ? ? ? ? ? ?public void process( ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?final HttpRequest request, ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?final HttpContext context) throws HttpException, IOException { ?? ? ? ? ? ? ? ? ? ? ? ? ? ?if (!request.containsHeader("Accept-Encoding")) { ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?request.addHeader("Accept-Encoding", "gzip"); ?? ? ? ? ? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ? ? ?}}).addInterceptorFirst(new HttpResponseInterceptor() { ? ? ? ? ? ? ? ? ? ? ? ? ?public void process( ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?final HttpResponse response, ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?final HttpContext context) throws HttpException, IOException { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?HttpEntity entity = response.getEntity(); ?? ? ? ? ? ? ? ? ? ? ? ? ? ?if (entity != null) { ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?Header ceheader = entity.getContentEncoding(); ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (ceheader != null) { ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?HeaderElement[] codecs = ceheader.getElements(); ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?for (int i = 0; i < codecs.length; i++) { ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?if (codecs[i].getName().equalsIgnoreCase("gzip")) { ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?response.setEntity( ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?new GzipDecompressingEntity(response.getEntity())); ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?return; ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ?}) ?? ? ? ? ? ? ? ? ? ?.setConnectionManager(cm) ?? ? ? ? ? ? ? ? ? ?.build(); ?? ? ? ? ? ?httpClient.start(); ?? ? ? ?} ? ? ?? ? ? ?private Response execute(HttpUriRequest request, long timeoutmillis) throws Exception { ?? ? ? ? ? ?HttpEntity entity = null; ?? ? ? ? ? ?Future<HttpResponse> rsp = null; ?? ? ? ? ? ?Response respObject=new Response(); ?? ? ? ? ? ?//default error code ?? ? ? ? ? ?respObject.setCode(400); ?? ? ? ? ? ?if (request == null) { ?? ? ? ? ? ? ? ?closeClient(httpClient); ?? ? ? ? ? ? ? ?return respObject; ?? ? ? ? ? ?} ? ? ? ? ? ? ?try{ ?? ? ? ? ? ? ? ?if(httpClient == null){ ?? ? ? ? ? ? ? ? ? ?StringBuilder sbuilder=new StringBuilder(); ?? ? ? ? ? ? ? ? ? ?sbuilder.append("\n{").append(request.getURI().toString()).append("}\nreturn error " ?? ? ? ? ? ? ? ? ? ? ? ? ? ?+ "{HttpHelperAsync.httpClient 獲取異常!}"); ?? ? ? ? ? ? ? ? ? ?System.out.println(sbuilder.toString()); ?? ? ? ? ? ? ? ? ? ?respObject.setError(sbuilder.toString()); ?? ? ? ? ? ? ? ? ? ?return respObject; ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?rsp = httpClient.execute(request, null); ?? ? ? ? ? ? ? ?HttpResponse resp = null; ?? ? ? ? ? ? ? ?if(timeoutmillis > 0){ ?? ? ? ? ? ? ? ? ? ?resp = rsp.get(timeoutmillis,TimeUnit.MILLISECONDS); ?? ? ? ? ? ? ? ?}else{ ?? ? ? ? ? ? ? ? ? ?resp = rsp.get(DEFAULT_ASYNC_TIME_OUT,TimeUnit.MILLISECONDS); ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?System.out.println("獲取返回值的resp----->"+resp); ?? ? ? ? ? ? ? ?entity = resp.getEntity(); ?? ? ? ? ? ? ? ?StatusLine statusLine = resp.getStatusLine(); ?? ? ? ? ? ? ? ?respObject.setCode(statusLine.getStatusCode()); ?? ? ? ? ? ? ? ?System.out.println("Response:"); ?? ? ? ? ? ? ? ?System.out.println(statusLine.toString()); ?? ? ? ? ? ? ? ?headerLog(resp); ?? ? ? ? ? ? ? ?String result = new String(); ?? ? ? ? ? ? ? ?if (respObject.getCode() == 200) { ?? ? ? ? ? ? ? ? ? ?String encoding = ("" + resp.getFirstHeader("Content-Encoding")).toLowerCase(); ?? ? ? ? ? ? ? ? ? ?if (encoding.indexOf("gzip") > 0) { ?? ? ? ? ? ? ? ? ? ? ? ?entity = new GzipDecompressingEntity(entity); ?? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ? ? ?result = new String(EntityUtils.toByteArray(entity),UTF8); ?? ? ? ? ? ? ? ? ? ?respObject.setContent(result); ?? ? ? ? ? ? ? ?} else { ?? ? ? ? ? ? ? ? ? ?StringBuilder sbuilder=new StringBuilder(); ?? ? ? ? ? ? ? ? ? ?sbuilder.append("\n{").append(request.getURI().toString()).append("}\nreturn error " ?? ? ? ? ? ? ? ? ? ? ? ? ? ?+ "{").append(resp.getStatusLine().getStatusCode()).append("}"); ?? ? ? ? ? ? ? ? ? ?System.out.println(sbuilder.toString()); ?? ? ? ? ? ? ? ? ? ?try { ?? ? ? ? ? ? ? ? ? ? ? ?result = new String(EntityUtils.toByteArray(entity),UTF8); ?? ? ? ? ? ? ? ? ? ? ? ?respObject.setError(result); ?? ? ? ? ? ? ? ? ? ?} catch(Exception e) { ?? ? ? ? ? ? ? ? ? ? ? ?logger.error(e.getMessage(), e); ?? ? ? ? ? ? ? ? ? ? ? ?result = e.getMessage(); ?? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?System.out.println(result); ? ? ? ? ? ? ?} catch (Exception e) { ?? ? ? ? ? ? ? ?logger.error("httpClient.execute異常", e); ?? ? ? ? ? ? ? ? ? ?} finally { ?? ? ? ? ? ?EntityUtils.consumeQuietly(entity); ?? ? ? ? ? ?System.out.println("執(zhí)行finally中的 closeClient(httpClient)"); ?? ? ? ? ? ?closeClient(httpClient); ?? ? ? ? ? ?} ?? ? ? ? ? ?return respObject; ?? ? ? ?} ?? ? ?? ? ? ?private static void closeClient(CloseableHttpAsyncClient httpClient) { ? ? ? ? ? ? ?if (httpClient != null) { ?? ? ? ? ? ? ? ?try { ?? ? ? ? ? ? ? ? ? ?httpClient.close(); ?? ? ? ? ? ? ? ?} catch (IOException e) { ?? ? ? ? ? ? ? ? ? ?e.printStackTrace(); ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ?} ?? ? ? ?} ?? ?}
這段代碼里面用到了CloseableHttpAsyncClient的api,我大概的查了下這個玩意,這個應(yīng)該是一個異步的httpClient,作用就是用于執(zhí)行一些不需要立刻收到回復(fù)的http請求,CloseableHttpAsyncClient就是用來幫你管理異步化的這些http的請求的。
代碼里面是這么調(diào)用這個類的:
scss復(fù)制代碼HttpHelperAsyncClient.getInstance().execute(request, timeoutMillis)
捋一下邏輯,就是通過HttpHelperAsyncClient.getInstance()
拿到HttpHelperAsyncClient的實例,然后在excute方法里面執(zhí)行請求并且釋放httpClient對象。按我的理解,其實就是一個httpClient的工具類
我直接把代碼拷貝出來,試圖復(fù)現(xiàn)一下,直接在mian方法進(jìn)行一個無限循環(huán)的調(diào)用
arduino復(fù)制代碼while (true){ ?? ? ? ? ? ?post("https://www.baidu.com",new Headers(),new HashMap<>(),0); ?? ? ? ?}
從idea直接拿一份dump:
耶?怎么和我想的不一樣,只有一條主線程,并沒有復(fù)現(xiàn)上萬線程的壯觀。
就在我懵逼的時候,旁邊的鄭網(wǎng)友開口了:"你要不要試試多線程調(diào)用,這個請求很有可能從tomcat進(jìn)來的"。
有道理,我迅速擼出來一個多線程調(diào)用的demo:
typescript復(fù)制代碼ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,20,100,TimeUnit.DAYS,new ArrayBlockingQueue<>(100)); ?? ? ? ?while (true) { ?? ? ? ? ? ?Thread.sleep(100); ?? ? ? ? ? ?threadPoolExecutor.execute(new Runnable() { ?? ? ? ? ? ? ? ?@Override ?? ? ? ? ? ? ? ?public void run() { ? ? ? ? ? ? ? ? ? ? ?try { ? ? ? ? ? ? ? ? ? ? ? ? ?post("https://www.baidu.com", new Headers(), new JSONObject(), 0); ?? ? ? ? ? ? ? ? ? ?} catch (Exception e) { ?? ? ? ? ? ? ? ? ? ? ? ?throw new RuntimeException(e); ?? ? ? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ?}); ?? ? ? ?}
因為線程漲的太猛,這次idea都沒辦法拿下線程快照,我借助JvisualVM監(jiān)控應(yīng)用狀態(tài),線程數(shù)目如同脫韁的野馬, 迅速的漲了起來,并且確實是I/O dispatcher線程居多
到這里,基本能說明問題就出現(xiàn)在這里。我們再深究一下。
可能有的朋友已經(jīng)發(fā)現(xiàn)了,HttpHelperAsyncClient類中的httpclient是線程不安全的,這個HttpHelperAsyncClient這個類里面有個httpclient的類對象變量,每次請求都會new一個新的httpclient賦值到類對象httpclient中,在excute方法執(zhí)行完會調(diào)用closeClient()方法釋放httpclient對象,但是closeClient的入?yún)⒅苯訌念惖某蓡T對象中取,這就有可能導(dǎo)致并發(fā)問題。
簡單的畫個圖解釋下:
http-1-thread調(diào)用方法init()把類變量httpclient設(shè)置為自己的實例對象,http-1-client
此時緊接著http-2-thread進(jìn)來,調(diào)用方法init()把類變量httpclient設(shè)置為自己的實例對象,http-2-client
接著http-1-thread執(zhí)行完請求,調(diào)用closeHttpclient()方法釋放httpclient,但是因為http-2線程已經(jīng)設(shè)置過類變量,所以它釋放的是http-2-client
http-2-thread執(zhí)行完請求,也去調(diào)用closeHttpClient()方法釋放httpclient,但是大概率會因為http-2-client已經(jīng)釋放過報錯
不管http-2-client如何,http-1-client是完完全全的被忘記了,得不到釋放,于是他們無止境的堆積了起來。
如何解決呢?其實也很簡單,這里httpclient對象其實是屬于逃逸了,我們把它變回成局部變量,就可以解決這個問題,在不影響大部分的代碼情況下,我們把生成httpclient的代碼從
java復(fù)制代碼HttpHelperAsyncClient.getInstance()
移動到execute()
中,并且在釋放資源的地方傳入局部變量而不是類變量:private CloseableHttpAsyncClient init() throws Exception { ? ? ? ? ? ? ?//省略部分代碼 ?? ? ? ? ? ?httpClient.start(); ?? ? ? ? ? ?//現(xiàn)在init方法返回CloseableHttpAsyncClient ?? ? ? ? ? ?return httpClient; ?? ?} private Response execute(HttpUriRequest request, long timeoutmillis) throws Exception { ?? ? ? ? ? ?//省略部分代碼 ?? ? ? ? ? ?//改動在這里 client直接new出來 ?? ? ? ? ?CloseableHttpAsyncClient httpClient = init(); ?? ? ? ? ? ?//省略部分代碼 ?? ? ? ? ?? ? ? ? ? ?closeClient(httpClient); ?? ? ? ? ? ?//省略部分代碼 ?? ? ? ?}
經(jīng)過改造后的代碼升級后登錄skywalking查看效果:
可以看到線程數(shù)量恢復(fù)成了180條,并且三天內(nèi)都沒有增加,比之前一天內(nèi)增加到6000條好多了。也就是區(qū)區(qū)一百倍的優(yōu)化,哈哈。
總結(jié)
其實這個算比較低級的錯誤,很簡單的并發(fā)問題,但是一不注意就容易寫出來。但是排查難度挺高的,因為大量的線程都是沒有我們一點業(yè)務(wù)代碼堆棧,根本不知道線程是從哪里創(chuàng)建出來的,和以往的排查方法算是完全不同。這次是屬于運氣爆棚然后找到的代碼,排查完問題我也想過,有沒有其他的方法來定位這么多相同的線程是從哪里創(chuàng)建出來的呢?我試著用內(nèi)存快照去定位,確實有一點線索,但是這屬于是馬后炮了,是我先讀過源碼才知道內(nèi)存快照可以定位到問題,有點從結(jié)果來推過程的意思,沒啥好說的。
總而言之,在定義這種敏感資源(文件流,各種client)時,我們一定要注意并發(fā)創(chuàng)建及釋放資源的問題,變量能不逃逸就不逃逸,最好是局部變量。