記一次線程掛死的排查過程(附 HttpClient 配置建議)

一、事發

咱們有個視頻處理程序,基於 SpringBoot,會啓動幾個線程來跑。要退出程序時,會發送一個信號給程序,每一個線程收到信號後會平滑退出,等所有線程都退出後,整個進程再平滑退出。html

整個程序平時運行都正常,而後有一天,咱們發送了退出信號給程序後,發現程序沒法自動退出了!腫麼回事呢,grep 一下日誌看到是這樣的。java

# grep 'receive exit signal' /PATH/TO/LOG

[2019-02-22 09:49:28,884][INFO ][Thread-75][n.p.j.e.l.l.PolyvQueueVideo:83] - receive exit signal ... exit current thread
[2019-02-22 09:49:56,271][INFO ][Thread-78][n.p.j.e.l.l.PolyvQueueVideo:83] - receive exit signal ... exit current thread
[2019-02-22 09:53:24,943][INFO ][Thread-74][n.p.j.e.l.l.PolyvQueueVideo:83] - receive exit signal ... exit current thread
[2019-02-22 09:55:23,317][INFO ][Thread-79][n.p.j.e.l.l.PolyvQueueVideo:83] - receive exit signal ... exit current thread
[2019-02-22 09:57:00,196][INFO ][Thread-77][n.p.j.e.l.l.PolyvQueueVideo:83] - receive exit signal ... exit current thread

這裏的程序總共啓動了6個線程的,但上面看到只有5個線程退出了,還有一個哪兒去了?不願輕易就義麼?好頑強的線程。。。apache

二、排查

有點小幸運的是,從上面這5個線程的名稱,咱們能夠推斷出那個頑強的線程的名稱,從74到79,中間惟獨缺了76!那就是你啦 Thread-76!服務器

再查 Thread-76 的日誌,確實是咱們想找的那個線程,而後發現原來在好幾天前它 好像就中止了運行 ,再也不有日誌輸出。也沒有任何異常信息!Thread-76 就這樣悄悄的離開,不帶走一片雲彩。我對着日誌和代碼大眼瞪小眼看了半個小時,束手無策。網絡

此時我想到了福爾摩斯說過的一句話:多線程

「當你排除掉各類不可能出現的狀況以後,剩下的狀況不管多麼難以置信,都是真相。」 -- 福爾摩斯

冷靜下來想想,Thread-76 這個線程,有可能靜悄悄地退出了嗎,沒留下半點異常日誌?從理論上來講,不可能。一個線程,要麼順利地執行直到結束,要麼中途出錯退出了,若是這樣確定有異常信息,但咱們並沒看到有異常日誌。排除掉 「Thread-76 已經退出了」 這個可能性以後,我有個大膽的想法:這個線程還一直運行着!安安靜靜地運行着,持續着好幾天,沒有半點日誌輸出!併發

是出現了死鎖嗎?不肯定,但咱們能夠驗證一下這個線程是否是真的還存活着。祭出 jstack,把線程信息 dump 出來,一查,果真見到了 Thread-76!socket

"Thread-76" #141 prio=5 os_prio=0 tid=0x00007f812d7d9800 nid=0x12848 runnable [0x00007f8227cfa000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:170)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
        at java.io.BufferedInputStream.read(BufferedInputStream.java:265)
        - locked <0x00000005e64cad10> (a java.io.BufferedInputStream)
        at org.apache.commons.httpclient.HttpParser.readRawLine(HttpParser.java:77)
        at org.apache.commons.httpclient.HttpParser.readLine(HttpParser.java:105)
        at org.apache.commons.httpclient.HttpConnection.readLine(HttpConnection.java:1115)
        at org.apache.commons.httpclient.HttpMethodBase.readStatusLine(HttpMethodBase.java:1832)
        at org.apache.commons.httpclient.HttpMethodBase.readResponse(HttpMethodBase.java:1590)
        at org.apache.commons.httpclient.HttpMethodBase.execute(HttpMethodBase.java:995)
        at org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:397)
        at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:170)
        at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:396)
        at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:324)
        at net.polyv.jet.encoding.legacy.util.IPUtil.delRemoteServerCacheFile(IPUtil.java:175)
        ***

能夠看到,這個線程並無發生死鎖,但卡在了發送 HTTP 請求這一步。多是網絡有問題,或者是服務端除了問題,反正咱們沒收到響應,而後線程就一直停在這了。怎麼會這樣呢,難道發送 HTTP 請求時沒有設置超時時間嗎?我一查代碼,還真的沒設置。。。這是個低級錯誤啊。ide

三、總結

弄清楚了緣由以後,問題就迎刃而解了。總結一下,有幾個地方能夠改進:oop

  1. 客戶端發送 HTTP 請求時,必定要設置超時時間,避免出現問題致使請求卡死。
  2. 接收 HTTP 請求的服務端,各級服務器(例如 Nginx、Tomcat)也都要設置超時時間,理由同上。
  3. 多線程的程序,出問題時進行排查的難度會相對大一些。因此,對於手工啓動、維護的線程,能夠的話自定義個線程名稱吧,出問題時也有跡可循。

四、HttpClient 配置建議

最後,附上一份 HttpClient 配置建議。

因爲各類緣由,HttpClient 經歷過好幾回版本變動,且這幾回變動致使其 API 用法都不同,不瞭解狀況的人每每會以爲懵逼,我到底該用哪一個版本呢?到底該用哪一種方法作配置呢?到底該配置哪幾種超時時間呢?下面這個例子,應該基本上涵蓋了大多數的應用場景了,拿走不謝。對應的版本是 HttpClient 4.5.* 。

public static CloseableHttpClient buildHttpClient() throws KeyStoreException, NoSuchAlgorithmException,
        KeyManagementException {
    HttpClientBuilder builder = HttpClientBuilder.create();
    
    // 信任所有 HTTPS 證書,避免 HTTPS 請求由於證書問題而失敗。留意,風險自擔。
    SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build(); // 信任所有證書
    builder.setSSLContext(sslContext);
    HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE; // 也信任所有域名
    SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
    Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
            .register("http", PlainConnectionSocketFactory.getSocketFactory())
            .register("https", sslSocketFactory).build();
    PoolingHttpClientConnectionManager connectionManager =
            new PoolingHttpClientConnectionManager(socketFactoryRegistry);
    
    // 設置併發鏈接數上限
    connectionManager.setMaxTotal(CONNECTION_LIMIT_TOTAL); // 總的併發鏈接數上限
    connectionManager.setDefaultMaxPerRoute(CONNECTION_LIMIT_PER_HOST); // 單個域名的併發鏈接數上限
    builder.setConnectionManager(connectionManager);
    
    // 設置默認的超時時間。具體數值可按需調整。
    RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(CONNECT_TIMEOUT) // 創建鏈接的超時時間
            .setSocketTimeout(SOCKET_TIMEOUT) // 鏈接創建後,傳輸數據時的超時時間
            .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT) // 從鏈接池中獲取鏈接時的超時時間
            .build();
    builder.setDefaultRequestConfig(requestConfig);
    
    // 除了 GET、HEAD 以外,也自動跟隨 POST、PUT 的 30一、302 重定向。按需使用。
    builder.setRedirectStrategy(new LaxRedirectStrategy());
    
    return builder.build();
}

參考:

相關文章
相關標籤/搜索