使用apache httpclient 默认连接池导致的系统瓶颈问题

现今的 Java 开发基本都会使用开源的开发框架, 它们都经过很多人的验证, 包含了很多成熟的最佳实践. 另外这些框架中经常包含一些可调的参数, 如果不读官方文档, 调节参数, 默认的参数可能由于不太适应真正的业务需求, 导致应用出现某些瓶颈问题.

症状

有告警显示某些 web 服务器的 Tomcat busy threads 横躺在了最大值上, 成了一条直线段. 如下图:
busyThreadMax.png

诊断

根据上面的截图, 正常的服务器的 Tomcat 服务线程基本处于非常低的并发使用率,基本处于并发3个以下. 然而某些服务器从某些时间点开始, 并发忙碌线程数节节上升, 最终达到配置的最大值(40).

症状分析

考虑这个问题之前, 我们先说下这些 web 服务器前面的 LB. 常使用的 LB 分配请求(连接)的策略有: 轮询(Round Robin) 和 最少连接(Least Connection). 在轮询的情况下, 如果某个服务器变慢, 也不会减少分配给它的请求, 可能最终因为它稍微变慢, 最终压垮这个服务器. 最少连接则可能出现另外一种极端问题, 如果某个服务器出错, 并且在非常短的时间内返回, 比如连不上数据库, 它直接返回5xx, 那么它会变成一个黑洞, 因为它处理的快, 导致大量的请求发给它, 其实它返回的都是5xx, 而其他处理正常的服务器都是返回正常的回应(Response). 当然可以在 LB 或者外挂的程序上监控每台服务器返回的 HTTP state code 的情况, 来动态决定是不是在 LB 上停掉某个服务器. 这里考虑的前提是这些服务器都是标准的返回2xx, 4xx, 5xx, 因为最近很多 rest API 直接把很多服务器出错封装到了 API 的回应内部, 外部看到的都是2xx.

回到出问题的这个应用上, 它默认是使用最少连接. 那么问题就来了, 如果使用最少连接, 那么它为什么还会有大量的请求进来? 在其它服务器最多只有3个忙碌线程的情况下, 它怎么达到了最大值40呢? 其实这里要考虑到正常的 HTTP 请求的超时, 正常设计的请求都是设置超时时间的, 一旦超时时间已过, 要么重试, 要么失败, 即使重试也有最大重试次数限制, 并且下次 LB 如果没有粘性配置, 基本不会仍旧落到这台服务器上. 假设用户超时, 用户(API 客户端一样)断掉连接, 但是 Tomcat 的服务线程并不会因为客户断掉连接而自动终止它在做的事情, 它还会继续做业务逻辑, 直到做完, 准备发回应(Response) 的时候, 发现TCP 连接已经中断. 所以这里即使是最少连接的 LB 策略, 它仍有可能导致服务器忙碌线程冲到最大值, 因为每次客户超时, 原来连接断掉, 从 LB 来看, 它的连接数减少, 继续分配, 从服务器来看, 每次新分配一个请求, 它忙碌线程就增加一个, 如果之前接的客一直不能做完, 那么只能把忙碌线程不断增加.

从服务器忙碌线程数的趋势来看, 它的增加是一个梯田形状, 每次增加几个, 这可能是因为监控数据采集粒度的问题, 实际情况可能是线性慢慢增加. 之后过了大概40多分钟后, 它就慢慢减少, 然后就恢复了.

初步诊断

由于是 Tomcat 线程池用光, 所以首先去看 thread dump. 从下面的 thread dump 可以看到有36个 Tomcat 服务线程都在等待从一个 HTTP 连接池中获取连接. 这个连接池使用的是 Apache HTTPComponents Client 的连接池.
image2.png

看到这里, 下一步就是去查看这个连接池的相关信息, 为什么都卡在这里? 连接池共有多少连接? 都在被谁使用?
image6.png
从这个 heap 的数据来看, 这个连接池最多可是有20个连接, 可要命的是每个 Route 只能有2个, 也就是说不管你要和一个地址建立多少连接, 这个地址上最多只能有2个连接, 多了就排队. 可以查看这个连接池的 pending 字段, 我们就能了解现在到底有多少在等待.

这个最多20个连接, 每个路由最多有2个连接, 这种设置正是 apache httpcomponents 的默认设置. 然而这种设置对于现在很多微服务每个 http client 都拥有自己独立的连接池来说, 非常不合适. 官方文档里有这么一段话:

PoolingHttpClientConnectionManager maintains a maximum limit of
connections on a per route basis and in total. Per default this
implementation will create no more than 2 concurrent connections per
given route and no more 20 connections in total. For many real-world
applications these limits may prove too constraining, especially if
they use HTTP as a transport protocol for their services.

并且给出了如何设置更合适的参数 -> 详见文档: https://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html 章节 2.3.3

所以这个例子中, 不知道最开始是什么原因导致的请求堆积, 等我们去查看它的时候, 发现当时的问题是由于配置的连接池的参数不当, 所引起的处理瓶颈.

思考

  1. 对于线程池, 我们平时最关心的是 Tomcat 服务线程池, 那么对于系统其它各种线程池怎么能比较有效的做监控呢? 是不是也经常出现瓶颈呢?
  2. 很多代码都是拿来主义, 最好使用之前或之后还是要看看官方的说明, 至少是配置说明.

标签: none

添加新评论