一个基于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;更不是设置长连接池最大连接数。官方解释:
我们先假设一个场景: 有一个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:
C、同样,如果假设相应不均衡也会出现上面的连接数波动情况。
造成连接数量反复震荡的一个推手,就是这个keepalive 这个最大空闲连接数。毕竟连接池中的1000个连接在频繁利用时,出现短时间内多余10个空闲连接的概率实在太高。因此为了避免出现上面的连接震荡,必须考虑加大这个参数,比如上面的场景如果将keepalive设置为100或者200,就可以非常有效的缓冲请求和应答不均匀。
题外话引自 https://lanjingling.github.io/2016/06/11/nginx-https-keepalived-youhua/