正确配置解决HttpClient Connection Reset异常

作者: 沐风之境

使用HttpClient工具遇到Connection Reset异常。


出现Connection Reset的原因

1.客户端在读取数据,服务端不再发送新数据,即服务器主动关闭了连接

为什么会出现服务端主动关闭连接呢?

经过排查线上服务器配置,发现当一个连接空闲时间超过60s,服务器就会将其关闭。如果刚好客户端在使用该连接则客户端就会收到来自服务端的连接复位标志通知

既然明白了服务端关闭的连接的原因,那为什么客户端会使用空闲时间为60s的连接呢? 

排查了HttpClient的配置后发现,项目中的HttpClient使用连接池,虽然设置了池的最大连接数,但是没有配置空闲连接驱逐器(IdleConnectionEvictor)。到这里原因就已经很明朗了,就是httpClient的配置有问题。

解决思路:

如果说服务端会关闭掉空闲时间超过60s的空闲连接,导致了connection reset 异常,要解决这个问题,那只要客户端在服务器关闭连接之前把连接关闭掉那就不会出现了。修改httpClient的配置。

解决方案1:

为HttpClient添加空闲连接驱逐器配置

新加了evictIdleConnections(40, TimeUnit.SECONDS)配置

HttpClients
  .custom()
  // 默认请求配置
  .setDefaultRequestConfig(customRequestConfig())
  // 自定义连接管理器
  .setConnectionManager(poolingHttpClientConnectionManager())
  // 删除空闲连接时间
  .evictIdleConnections(40, TimeUnit.SECONDS)
  .disableAutomaticRetries(); // 关闭自动重试

正常情况下到这里问题就解决了,但是现实是线上再次出现了Connection Reset异常。继续排查…

思考:虽然更新配置后再次出现"连接重置"异常,不过出现频率相较于没改之前还是要低不少。所以改的配置还有用的,肯定是什么地方没有配好。为了一探究竟,查了HttpClient关于IdleConnectionEvictor驱逐器的源码发现了问题所在。

源码解读:

源码1:

// org.apache.http.impl.client.HttpClientBuilder
public class HttpClientBuilder {
  // .....省略无关代码....
  // 关注build方法,这这个方法里面启动了空闲连接驱逐器
  public CloseableHttpClient build() {
    // 。。。。省略代码。。。。
       if (!this.connManagerShared) {
            if (closeables == null) {
                closeables = new ArrayList<Closeable>(1);
            }
            final HttpClientConnectionManager cm = connManager;

            if (evictExpiredConnections || evictIdleConnections) {
              // 在这里实例化了IdleConnectionEvictor。maxIdleTime和maxIdleTimeUnit就是们在配置httpclient时
              // 传入的 40 和 TimeUnit.SECONDS
                final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
                        maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
                        maxIdleTime, maxIdleTimeUnit);
                closeables.add(new Closeable() {

                    @Override
                    public void close() throws IOException {
                        connectionEvictor.shutdown();
                        try {
                            connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
                        } catch (final InterruptedException interrupted) {
                            Thread.currentThread().interrupt();
                        }
                    }

                });
              // 调用start()发放启动了线程驱逐器
                connectionEvictor.start();
            }
            closeables.add(new Closeable() {

                @Override
                public void close() throws IOException {
                    cm.shutdown();
                }

            });
        }
        // 。。。。省略无关代码。。。。。
  }
}
  1. evictIdleConnections(40, TimeUnit.SECONDS)配置的参数在HttpClientBuilder.builder方法中用于实例化IdleConnectionEvictor对象的构造参数

  2. 调用了connectionEvictor.start()方法启动了线程驱逐器

    源码2:

// org.apache.http.impl.client.IdleConnectionEvictor
public final class IdleConnectionEvictor {
  // 。。。。省略无关代码。。。。
  // HttpClientBuilder.build()内实例化IdleConnectionEvictor调用了该构造方法
    public IdleConnectionEvictor(
            final HttpClientConnectionManager connectionManager,
            final long sleepTime, final TimeUnit sleepTimeUnit,
            final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
        this(connectionManager, null, sleepTime, sleepTimeUnit, maxIdleTime, maxIdleTimeUnit);
    }
  // 。。。。省略无关代码。。。。
}

关键的参数列表

  1. sleepTime:延时检查时间
  2. maxIdleTime:最多空闲时间

结合源码1和源码2,可以看到在构造IdleConnectionEvictorsleepTimemaxIdleTime为同一个值40秒,在这里还看不出什么问题,继续。

源码3:

// org.apache.http.impl.client.IdleConnectionEvictor
public final class IdleConnectionEvictor {
  // 省略无关代码
  // 重载的构造方法
    public IdleConnectionEvictor(
            final HttpClientConnectionManager connectionManager,
            final ThreadFactory threadFactory,
            final long sleepTime, final TimeUnit sleepTimeUnit,
            final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
        this.connectionManager = Args.notNull(connectionManager, "Connection manager");
        this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
        this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
        this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
      // 使用threadFactory线程构造器构造了一个守护线程
        this.thread = this.threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (!Thread.currentThread().isInterrupted()) {
                      // 挂起线程时间是们传入的时间40秒
                        Thread.sleep(sleepTimeMs);
                      // 执行检查代码,关闭过期连接
                        connectionManager.closeExpiredConnections();
                        if (maxIdleTimeMs > 0) {
                          // 关闭超过空闲时间的空闲连接,参数传入们配置的40秒
                            connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
                        }
                    }
                } catch (final Exception ex) {
                    exception = ex;
                }

            }
        });
    }

  // HttpClientBuilder中调用的start()方法
    public void start() {
        thread.start();
    }
}

通过源码3 可以看到,检查线程的执行周期时间和最大过期时间都是们传入的40秒。在这里停顿一下思考一下,服务器的空闲连接关闭时间是60s,们配置的时间是40s,那这样配置会不有出现什么问题?

线程相隔40s执行一下回收任务,相当于80秒的的周期内会做两次回收动作。但是60s在其中最多只能回收掉一次,还是可能存在回收不掉的情况,在不执行回收任务停止的40秒里面出了connection reset异常了怎么吧?问题就明了了。

问题复现时序:

  1. 00:00:00 — 启动IdleConnectionEvictor.start(),挂起检查线程,不执行检查代码

  2. 00:00:10 — 10秒后的连接池新建了一个连接

  3. 00:00:12 — 连接耗时2s,用完后返回线程池,假设之后都没有再被使用了

  4. 00:00:40 — 第一次sleep挂起时间到期,执行检查任务。发现没有过期连接,下一次回收任务发生在 00:01:20

  5. 00:01:12 — 这时恰好客户端使用那个空闲的连接,服务端关闭了该连接。在这里发生了connection reset 异常

  6. 00:01:20 — 第二次sleep挂起时间到期,执行检查任务。

    结论:

服务端空闲连接关闭时间是60s,们客户端配置的最大空闲时间值应该小于30s才能避免这个问题

解决方案2:

在解决方案1的基础上,把40s时间改为20s,顺利解决了该问题。

原文创作:沐风之境

原文链接:https://www.cnblogs.com/mufeng3421/p/15387044.html

更多推荐

更多
  • Docker常见问题-MAC电脑运行docker-compose up-d报File "docker/transport/unixconn py", line 43, 在本地有一个 docker-compose.yml 文件,要运行它docker-compose up -d结果报错了,File "docker/transport/unixconn.py", line 43, in connect ...
  • Python 常见问题-pip install报错ERROR: In--require-hashes mode, all requirements poetry 1.1.8 执行了命令,导出 requirements.txt,poetry export,每个库都有 hash 加密字段 执行 pip install 命令,Collecting cffi>=1.1 8 29.38 ...
  • Docker-解决 docker push 上传镜像报:denied: requested access to the resource is denied 的问题 ,Docker - 解决 docker push 上传镜像报:denied: requested access to the resource is denied 的问题,,问题背景,解决方案, ``` ![](https://sta
  • Docker-解决 Error response from daemon: driver failed programming external connectivity on endpoint tomcat9999 ,Docker - 解决 Error response from daemon: driver failed programming external connectivity on endpoint tomcat9999,,问题背景,
  • Docker-解决在容器内删除和主机映射的目录而报错 rm: cannot remove 'webapps': Device or resource busy 的问题 ,Docker - 解决在容器内删除和主机映射的目录而报错 rm: cannot remove 'webapps': Device or resource busy 的问题,,问题背景,问题排查,解决问题,local/tomcat/we
  • Docker-解决 gitlab 容器上的项目进行 clone 时,IP 地址显示一串数字而不是正常 IP 地址的问题 ,Docker - 解决 gitlab 容器上的项目进行 clone 时,IP 地址显示一串数字而不是正常 IP 地址的问题,,问题背景,问题排查,解决方案,atic.oomspot.com/image/bost/2021/189687
  • Docker-解决同步容器与主机时间报错:Error response from daemon: Error processing tar fileexit status 1: invalid symlink "/usr/share/zoneinfo/UTC"-> " /usr/share/zoneinfo/Asia/Shanghai" ,Docker - 解决同步容器与主机时间报错:Error response from daemon: Error processing tar file(exit status 1): invalid symlink "/usr/sh
  • Docker-解决容器内获取的时间和主机的时间不一样的问题 ,Docker - 解决容器内获取的时间和主机的时间不一样的问题,,问题背景,解决方案,1/1896874-20201112213159066-2047342389.png) 可以看到,时间是完全不一样的 解决方案 ---- 在运
  • Docker-解决重新进入容器后,环境变量失效的问题 ,Docker - 解决重新进入容器后,环境变量失效的问题,,问题背景,解决办法,扩展,设置的环境变量失效了 解决办法 ---- 将环境变量设置在 /root/.bashrc 优点 重启容器之后,文件内的环境变量仍然生效 缺点
  • Docker-解决运行容器报 WARNING: IPv4 forwarding is disabled Networking will not work 的问题 ,Docker - 解决运行容器报 WARNING: IPv4 forwarding is disabled. Networking will not work. 的问题,,问题背景,解决方案,351509-984821467.png)
  • 近期文章

    更多
    文章目录

      推荐作者

      更多