一次httpclient长连接高并发问题的解决历程与研究总结

2023-04-10 17:06:38

相关背景

  一个基于es的搜索项目,生产环境中目前qps为4w多,所有的业务请求通过soa落在搜索微服务集群,微服务集群中每台机器底层通过httpclient请求SLB(service load balance),SLB核心为Nginx,最终Nginx将请求分发至es物理机集群计算并返回

问题描述

  生产搜索微服务间歇性告警,主要为NoHttpResponseException: *** failed to respond和java.net.SocketException: Connection reset异常。

  微服务有自开发的http请求重试机制,设置为2次,日志反映重试机制触发规模很大,并不足以覆盖问题

  问题规模在搜索微服务与slb之间,为方便,后文称前者为客户端,后者为服务端

问题原因

  客户端与服务端之间使用的是http长连接,客户端通过PoolingHttpClientConnectionManager连接池来管理连接。在客户端不能感知的情况下,服务端对长连接进行了关闭,在临界状态会触发以下三种错误:

1、java.net.SocketException: Connection reset

2、java.net.SocketException: Broken pipe 

3、org.apache.http.NoHttpResponseException: *** failed to respond 

  qps越高,错误量越大

  最简单有效的处理方式是设置重试机制(已有),但错误量已经超过了重试的处理能力。

根本原因

    客户端没有及时回收http连接,且丢入连接池导致复用已关闭连接,即服务端关闭连接,会引起客户端错误,在量不够大的情况下,重试机制完全可以支撑,但随着错误量上升,重试带来的消耗扩大,且逐渐超出能力范围

主要原因

  目前我所知道相关的造成服务端(nginx)主动关闭长连接的情况有以下:

 1、keepalive 用于在qps变化较大的情况下nginx及时回收长连接(详见.题外话)。项目的qps相对稳定,即使在变化时,重试机制也可以覆盖错误量,所以这不是主要原因

 2、keepalive_requests 单个长连接最多可处理的请求数。

 3、keepalive_timeout 长连接最久存活时间。  

  主要原因是nginx的keepalive_timeout可选配置项没有开启,同时客户端连接池回收器是自开发,只做了空闲超时连接回收(closeIdleConnections),没有生命期回收(closeExpiredConnections)

解决方案

两种解决方案,一种是客户端修改程序同时服务端增加nginx可选配置,一种是只有客户端修改程序

首先两种方案都需要客户端支持http长连接生命期回收(closeExpiredConnections)。在使用PoolingHttpClientConnectionManager管理连接池的同时,需要独立的运行一个回收器来帮助PoolingHttpClientConnectionManager回收http连接,在PoolingHttpClientConnectionManager中有两种回收方式,closeIdleConnections是回收空闲超时连接,即调用时会回收可用连接池内空闲太久(具体阈值可设置)的连接,这种回收已有;而closeExpiredConnections调用时会根据生命期回收,这种回收欠缺。回收器需要自定策略并显示调用PoolingHttpClientConnectionManager的两种回收方法。

http连接的生命期(expiry)主要受两方面影响,分别对应了这个问题的两种解决方案

1、连接创建时限定的最大存活时间(validityDeadline),expiry会初始化为这个字段值。对应解决方案就是将相关构造器参数逐层暴露到外层进行控制,使它小于nginx的keepalive_timeout值

2、使用服务端响应的Keep-Alive头,由服务端控制更新客户端连接的expiry,这需要nginx的可选配置支持(见图官方说明)

同时客户端需要开发一个实现ConnectionKeepAliveStrategy的类,实例化并set进HttpClientBuilder中,可参照DefaultConnectionKeepAliveStrategy类。

httpclient的调用过程应用了责任链模式,之前提到的重试机制,是通过HttpClientBuilder将重试机制嵌入了链条中的一环,而在链条的末尾主要由MainClientExec来做很多底层工作,http连接的生命期更新就在其中,当响应到达后,MainClientExec会获取ConnectionKeepAliveStrategy的值来进行更新,而每个连接自身在更新的时候会内部取min(newExpiry, this.validityDeadline)。此时配合回收器就实现了服务端对客户端连接生命期的控制。

 

 第二种解决方案比较流行且效果较好,但需要服务端配合,而我当下的情况nginx并没做可选配置也不由我可控,询问时被拒绝更改,所以采取了第一种方案。

具体实施

在项目中,服务应用依赖了自开发的es框架,es框架依赖了自开发的http包,es框架升级严格

开发测试发布过程

1、对http包修改,在代码中写死生命周期,生命周期值小于服务端,并升级版本只安装到maven本地仓。在服务应用通过maven的依赖manager手动控制底层http包版本,并本地进行自测。功能没问题

2、将http包提交并发布到远程仓库,将服务应用发布测试环境。查看日志没问题并且重试规模减少

3、将服务应用灰度测试后发生产环境其中一个集群。对比后效果明显。

4、对http再次修改,将写死的生命周期通过扩展构造器交由外层控制,升级版本;修改es框架,通过对配置扩展使用远程配置中心控制http连接生命期,升级版本;更改服务依赖的es框架版本,去除之前的manager版本控制。然后跟1相同方法本地自测。无bug

5、将http包发布远程仓库,将es框架提交并等待大佬review后发布远程仓库,然后将服务应用发布测试生产。

效果

1、日常不再有上述间歇性告警

2、重试规模大大减少

题外话

upstream中的keepalive设置:
此处keepalive的含义不是开启、关闭长连接的开关;也不是用来设置超时的timeout;更不是设置长连接池最大连接数。官方解释:

  1. The connections parameter sets the maximum number of idle keepalive connections to upstream servers connections(设置到upstream服务器的空闲keepalive连接的最大数量
  2. When this number is exceeded, the least recently used connections are closed. (当这个数量被突破时,最近使用最少的连接将被关闭
  3. It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.(特别提醒:keepalive指令不会限制一个nginx worker进程到upstream服务器连接的总数量

我们先假设一个场景: 有一个HTTP服务,作为upstream服务器接收请求,响应时间为100毫秒。如果要达到10000 QPS的性能,就需要在nginx和upstream服务器之间建立大约1000条HTTP连接。nginx为此建立连接池,然后请求过来时为每个请求分配一个连接,请求结束时回收连接放入连接池中,连接的状态也就更改为idle。我们再假设这个upstream服务器的keepalive参数设置比较小,比如常见的10.

A、假设请求和响应是均匀而平稳的,那么这1000条连接应该都是一放回连接池就立即被后续请求申请使用,线程池中的idle线程会非常的少,趋进于零,不会造成连接数量反复震荡。

B、显示中请求和响应不可能平稳,我们以10毫秒为一个单位,来看连接的情况(注意场景是1000个线程+100毫秒响应时间,每秒有10000个请求完成),我们假设应答始终都是平稳的,只是请求不平稳,第一个10毫秒只有50,第二个10毫秒有150:

  1. 下一个10毫秒,有100个连接结束请求回收连接到连接池,但是假设此时请求不均匀10毫秒内没有预计的100个请求进来,而是只有50个请求。注意此时连接池回收了100个连接又分配出去50个连接,因此连接池内有50个空闲连接。
  2. 然后注意看keepalive=10的设置,这意味着连接池中最多容许保留有10个空闲连接。因此nginx不得不将这50个空闲连接中的40个关闭,只留下10个。
  3. 再下一个10个毫秒,有150个请求进来,有100个请求结束任务释放连接。150 - 100 = 50,空缺了50个连接,减掉前面连接池保留的10个空闲连接,nginx不得不新建40个新连接来满足要求。

C、同样,如果假设相应不均衡也会出现上面的连接数波动情况。

造成连接数量反复震荡的一个推手,就是这个keepalive 这个最大空闲连接数。毕竟连接池中的1000个连接在频繁利用时,出现短时间内多余10个空闲连接的概率实在太高。因此为了避免出现上面的连接震荡,必须考虑加大这个参数,比如上面的场景如果将keepalive设置为100或者200,就可以非常有效的缓冲请求和应答不均匀。

题外话引自 https://lanjingling.github.io/2016/06/11/nginx-https-keepalived-youhua/

  • 作者:阿西吧ckia
  • 原文链接:https://blog.csdn.net/qq_36261538/article/details/103543827
    更新时间:2023-04-10 17:06:38