HttpClient偶尔报NoHttpResponseException: xxx failed to respond 问题分析

HttpClient偶尔报NoHttpResponseException: xxx failed to respond

背景描述

调用底层服务偶尔会报以下错误

org.apache.http.NoHttpResponseException: submit.10690221.com:9012 failed to respond

    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:141)
    ....

第一次碰到,先google一下,发现不少相同的情况,讲的也很不错,但是呢,我想自己复现一下,并且自己去分析并解决,这样能更好的去理解 网络 这东西

复现方法

这个怎么复现呢,通过google得知,这个只会在服务器端keep-alive刚好过期的时间我们进行访问才能大概率复现,方法如下:

wireshark进行抓包得出底层服务器的keep-alive时间

写一段程序,用于探测底层服务器的keep-alive,代码如下:

@Test
public void test121() throws Exception {
    String url = "http://xxxxxxx:9012/hy/json";
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpPost request = new HttpPost(url);

    httpClient.execute(request, response -> {
        String content = EntityUtils.toString(response.getEntity());
        System.out.println(content);
        return content;
    });

    Thread.sleep(1000000);

}

开启wireshark进行抓包,执行程序直到下图出现即可停止


重点看左下角的红色框,时间相差65秒左右,没错从而可以得知底层服务器的keep-alive 是 65秒,也就是当一个连接socket 65秒内没有数据交互,底层服务器就会认为这个连接可以关闭了,因此才会在3分36秒进行挥手操作发送一个FIN包,这时我们稍微改造一下这个程序,如下:

@Test
public void test121() throws Exception {
    String url = "http://xxxxxxx:9012/hy/json";
    CloseableHttpClient httpClient = HttpClients.createDefault();
    HttpPost request = new HttpPost(url);
    while (true) {//加了一个死循环 ^_^
        httpClient.execute(request, response -> {
            String content = EntityUtils.toString(response.getEntity());
            System.out.println(content);
            return content;
        });

        Thread.sleep(65000); //关键是这里,设置和底层服务器keep-alive相同
    }
}

相比第一个,有两个改动

  1. 加了一个循环
  2. 每次调用的间隔改成和底层服务器相同的65秒

我们清空wireshark,运行该程序抓包,结果如下:


问题分析

首先我们分析一下抓包结果


  1. 红色框1:前3个请求是建立连接的过程,三次握手,接着4个请求就是client和server的数据交互,着重看最后四个请求
    1. 9012 -> 59233 [FIN, ACK]:服务器主动进行关闭,给client发送了FIN包
    2. 59233 -> 9012 [ACK]:client进行回应ACK包
    3. 69233 -> 9012 [FIN, ACK]:按照四次挥手原则,client发现目前数据已经发送完毕了,因此也发出FIN包
    4. 9012 -> 59233 [RST]:服务器直接返回一个RST
  2. 红色框2:同2
  3. 红色框3:前面的7个步骤都是相同的,建立连接,数据交互,区别唯独在于绿色框
    1. 9012 -> 59233 POST /hy/json: client认为服务器端可用,因此给服务器发送数据
    2. 9012 -> 59233 [FIN, ACK]:服务器认为此连接已经失效,因为超过了65的keep-alive时间,主动进行关闭,给client发送了FIN包
    3. 59233 -> 9012 [ACK]:client进行回应ACK包
    4. 69233 -> 9012 [FIN, ACK]:按照四次挥手原则,client发现目前数据已经发送完毕了,因此也发出FIN包
    5. 9012 -> 59233 [RST]:服务器直接返回一个RST 通过Seq=188,可判断这条是给【9012 -> 59233 POST /hy/json】这个请求回的
    6. 9012 -> 59233 [RST]:服务器直接返回一个RST 通过Seq=189,可判断这条是给【69233 -> 9012 [FIN, ACK]】回的
    7. 9012 -> 59233 [RST]:服务器直接返回一个RST 通过Seq=189,同6

通过分析抓包数据,得出结果是,当client客户端认为这条Socket连接有用,这时服务器端却认为该Socket连接无用,并主动关闭,就会报错,属于临界值没有处理好的

这时有人就说了,为什么前两次就没有问题呢,原因是HttpClient会进行连接过期是否可用的检查,那么也就能理解这是httpclient的一个bug,即使httpclient有做这么一件事情,但是由于网络I/O原因,导致httpclient认为一个关闭了的连接是有效的,才报了这个错误

接下来我们看看HttpClient为什么会复用一个已经被关闭的连接

由于HttpClient代码有点多,为了方便快速定位缩小范围, 我这边开启了debug,并对两者的日志进行了分析
左边日志是正常交互、右边是报错了


我这边简化了一下日志,通过仔细分析HttpClient打印的debug日志,可发现左边正常交互日志 打印了一串 "end of stream" 后进行了连接的重新建立, connection established ,而右边错误日志打印了一串 "[read] I/O error: Read timed out" 后没有进行连接的重新建立,因此就报错了

那么可以通过打印 "[read] I/O error: Read timed out"日志的上下文日志缩小 排查代码的范围,上文日志 Connection request,下文日志 Connection leased,进行代码定位


基本上定位到了PooingHttpClientConnectionManager.java这个类,那么进行代码跟踪吧


追踪到了 AbstractConnPool.java类,那么这段代码什么意思呢,这个就是进行连接是否能够复用的检查代码

对validateAfterInactivity进行判断,这个是服务器keep-alive的值

  1. leasedEntry.getUpdated() + validateAfterInactivity <= System.currentTimeMillis():如果连接的最后一次使用时间 + 服务器keep-alive的时间 小于等于当前时间,那么就认为该连接可能已经失效了
  2. !validate(leasedEntry): 因此会进行连接是否失效的检查

跟进去看看


最终找到"end of stream" and "[read] I/O error: Read timed out" 打印的地方
然后回到如下图代码:


可以看到

  • 当bytesRead 值为 -1 时,返回true,那么HttpClient就会认为该连接失效了,不能够复用,并进行清理操作,
  • 当抛出异常是ShockTimeoutException时会返回false, 那么HttpClient就会认为该连接可复用

分析到这,相信大部分人都已经知道为什么会保证错了,不过还是强烈建议自己动手分析一下,另外大家可去了解一下,为什么会输出"end of stream" and "[read] I/O error: Read timed out"两种不同的结果,快去畅游底层Socket编程相关的原理吧,这有助于你更加理解

解决方案

其实当你知道原因后,也能想出对应的解决方案,不过我这边还是收集列出来了一些

  1. 禁用HttpClient的连接复用(有点扯淡)
  2. 重试方案:http请求使用重发机制,捕获NohttpResponseException的异常,重新发送请求,重发3次后还是失败才停止
  3. 根据keep Alive时间,调整validateAfterInactivity小于keepAlive Time,但这种方法依旧不能避免同时关闭
  4. 系统主动检查每个连接的空闲时间,并提前自动关闭连接,避免服务端主动断开

推荐使用重试方案

你可能感兴趣的:(HttpClient偶尔报NoHttpResponseException: xxx failed to respond 问题分析)