美文网首页
HttpClient偶尔报NoHttpResponseExcep

HttpClient偶尔报NoHttpResponseExcep

作者: 漫步无法人生 | 来源:发表于2019-11-26 09:25 被阅读0次

    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偶尔报NoHttpResponseExcep

          本文链接:https://www.haomeiwen.com/subject/tttewctx.html