记一次线程挂死的排查过程(附 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();
}

参考:

相关文章
相关标签/搜索