HttpClient 在vivo内销浏览器的高并发实践优化

网友投稿 631 2022-10-19

HttpClient 在vivo内销浏览器的高并发实践优化

HttpClient 在vivo内销浏览器的高并发实践优化

作者:vivo 互联网服务器团队- Zhi Guangquan

HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些优化空间。

一、问题现象

北京时间X月X日,浏览器信息流服务监控出现异常,主要表现在以下三个方面:

从某个时间点开始,云监控显示部分Http接口的熔断器被打开,而且从明细列表可以发现问题机器:

2\. 从PAAS平台Hystrix熔断管理界面中可以进一步确认问题机器的所有Http接口调用均出现了熔断:

3\. 日志中心有大量从Http连接池获取连接的异常:org.apache.Request aborted。

二、问题定位

综合以上三个现象,大概可以推测出问题机器的TCP连接管理出了问题,可能是虚拟机问题,也可能是物理机问题;与运维与系统侧沟通后,发现虚拟机与物理机均无明显异常,第一时间联系运维重启了问题机器,线上问题得到解决。

2.1 临时解决方案

几天以后,线上部分其他机器也陆续出现了上述现象,此时基本可以确认是服务本身有问题;既然问题与TCP连接相关,于是联系运维在问题机器上建立了一个作业查看TCP连接的状态分布:

netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'

结果如下:

如上图,问题机器的CLOSE\_WAIT状态的连接数已经接近200左右(该服务Http连接池最大连接数设置的250),那问题直接原因基本可以确认是CLOSE\_WAIT状态的连接过多导致的;本着第一时间先解决线上问题的原则,先把连接池调整到500,然后让运维重启了机器,线上问题暂时得到解决。

2.2 原因分析

调整连接池大小只是暂时解决了线上问题,但是具体原因还不确定,按照以往经验,出现连接无法正常释放基本都是开发者使用不当,在使用完成后没有及时关闭连接;但很快这个想法就被否定了,原因显而易见:当前的服务已经在线上运行了一周左右,中间没有经历过发版,以浏览器的业务量,如果是连接使用完没有及时关。

闭,250的连接数连一分钟都撑不到就会被打爆。那么问题就只能是一些异常场景导致的连接没有释放;于是,重点排查了下近期上线的业务接口,尤其是那种数据包体较大,响应时间较长的接口,最终把目标锁定在了某个详情页优化接口上;先查看处于CLOSE_WAIT状态的IP与端口连接对,确认对方服务器IP地址。

netstat-tulnap|grep CLOSE_WAIT

经过与合作方确认,目标IP均来自该合作方,与我们的推测是相符的。

2.3 TCP抓包

在定位问题的同时,也让运维同事帮忙抓取了TCP的数据包,结果表明确实是客户端(浏览器服务端)没返回ACK结束握手,导致挥手失败,客户端处于了CLOSE_WAIT状态,数据包的大小也与怀疑的问题接口相符。

为了方便大家理解,我从网上找了一张图,大家可以作为参考:

CLOSE\_WAIT是一种被动关闭状态,如果是SERVER主动断开的连接,那么就会在CLIENT出现CLOSE\_WAIT的状态,反之同理;

通常情况下,如果客户端在一次独立连接池

为了不影响其他业务场景,防止出现系统性风险,我们先把问题接口连接池进行了独立管理。

2.5 深入分析

带着2.3的疑问我们仔细查看一下业务调用代码:

try { = HttpsClientUtil.getHttpClient().execute(request); HttpEntity = is = }catch (Exception e){ log.error(""); }finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly( }

这段代码存在一个明显的问题:既关闭了数据传输流( IOUtils.closeQuietly(is)),也关闭了整个连接(IOUtils.closeQuietly(源码分析

开始分析之前,我们需要了解一个基础知识:Http的长连接和短连接。所谓长连接就是建立起连接之后,可以复用连接多次进行数据传输;而短连接则是每次都需要重新建立连接再进行数据传输。

而通过对接口的抓包我们发现,响应头里有Connection:keep-live字样,那我们就可以重点从HttpClient对长连接的管理入手来进行代码分析。

2.6.1 连接池初始化

初始化方法:

进入PoolingHttpClientConnectionManager这个类,有一个重载构造方法里包含连接存活时间参数:

顺着继续向下查看

manager的构造方法到此结束,我们不难发现validityDeadline会被赋值给expiry变量,那我们接下来就要看下HttpClient是在哪里使用expiry这个参数的;

通常情况下,实例对象被构建出来的时候会初始化一些策略参数,此时我们需要查看构建HttpClient实例的方法来寻找答案:

此方法包含一系列的初始化操作,包括构建连接池,给连接池设置最大连接数,指定重用策略和长连接策略等,这里我们还注意到,HttpClient创建了一个异步线程,去监听清理空闲连接。

当然,前提是你打开了自动清理空闲连接的配置,默认是关闭的。

接着我们就看到了HttpClient关闭空闲连接的具体实现,里面有我们想要看到的内容:

此时,我们可以得出第一个结论:可以在初始化连接池的时候,通过实现带参的PoolingHttpClientConnectionManager构造方法,修改validityDeadline的值,从而影响HttpClient对长连接的管理策略。

2.6.2 执行方法入口

先找到执行入口方法:org.apache.前言

开始分析之前,先简单介绍下几个核心类:

【PoolingHttpClientConnectionManager】:连接池管理器类,主要作用是管理连接和连接池,封装连接的创建、状态流转以及连接池的相关操作,是操作连接和连接池的入口方法;【CPool】:连接池的具体实现类,连接和连接池的具体实现均在CPool以及抽象类AbstractConnPool中实现,也是分析的重点;【CPoolEntry】:具体的连接封装类,包含连接的一些基础属性和基础操作,比如连接id,创建时间,有效期等;【HttpClientBuilder】:HttpClient的构造器,重点关注build方法;【MainClientExec】:客户端请求的执行类,是执行的入口,重点关注execute方法;【ConnectionHolder】:主要封装释放连接的方法,是在PoolingHttpClientConnectionManager的基础上进行了封装。

3.2 两个连接

最大连接数(maxTotal)最大单路由连接数(maxPerRoute)最大连接数,顾名思义,就是连接池允许创建的最大连接数量;最大单路由连接数可以理解为同一个域名允许的最大连接数,且所有maxPerRoute的总和不能超过maxTotal。以浏览器为例,浏览器对接了头条和一点,为了做到业务隔离,不相互影响,可以把maxTotal设置成500,而defaultMaxPerRoute设置成400,主要是因为头条的业务接口量远大于一点,defaultMaxPerRoute需要满足调用量较大的一方。

3.3 三个超时

connectionRequestTimoutconnetionTimeoutsocketTimeout

**【connectionRequestTimout】:**指从连接池获取连接的超时时间;【connetionTimeout】:指客户端和服务器建立连接的超时时间,超时后会报ConnectionTimeOutException异常;【socketTimeout】:指客户端和服务器建立连接后,数据传输过程中数据包之间间隔的最大时间,超出后会抛出SocketTimeOutException。

一定要注意:这里的超时不是数据传输完成,而只是接收到两个数据包的间隔时间,这也是很多线上诡异问题发生的根本原因。

3.4 四个容器

freeleasedpendingavailable

**【free】:**空闲连接的容器,连接还没有建立,理论上freeSize=maxTotal -leasedSize\- availableSize(其实HttpClient中并没有该容器,只是为了描述方便,特意引入的一个容器)。【leased】:租赁连接的容器,连接创建后,会从free容器转移到leased容器;也可以直接从available容器租赁连接,租赁成功后连接被放在leased容器中,此种场景主要是连接的复用,也是连接池的一个很重要的能力。【pending】:等待连接的容器,其实该容器只是在等待连接释放的时候用作阻塞线程,下文也不会再提到,感兴趣的可以参考具体实现代码,其与connectionRequestTimout相关。【available】:可复用连接的容器,通常直接从leased容器转移过来,长连接的情况下完成通信后,会把连接放到available列表,一些对连接的管理和释放通常都是围绕该容器进行的。

注:由于存在maxTotal和maxPerRoute两个连接数限制,下文在提到这四种容器时,如果没有带前缀,都代表是总连接数,如果是r.xxxx则代表是路由连接里的某个容器大小。

maxTotal的组成

3.5 连接的产生与管理

循环从available容器中获取连接,如果该连接未失效(根据上文提到的expiry字段判断),则把该连接从available容器中删除,并添加到leased容器,并返回该连接;如果在第一步中没有获取到可用连接,则判断r.available + r.leased是否大于maxPerRoute,其实就是判断是否还有free连接;如果不存在,则需要把多余分配的连接释放掉(r. available + r.leased - maxPerRoute),来保证真实的连接数受maxPerRoute控制(至于为什么会出现r.leased+r.available>maxPerRoute的情况其实也很好理解,虽然在整个状态流转过程都加了锁,但是状态的流转并不是原子操作,存在一些异常的场景都会导致状态短时间不正确);所以我们可以得出结论,maxPerRoute只是一个理论上的最大数值,其实真实产生的连接数在短时间内是可能大于这个值的;在真实的连接数(r .leased+ r .available)小于maxPerRoute且maxTotal>leased的情况下:如果free>0,则重新创建一个连接;如果free=0,则把available容器里的最早创建的一个连接关闭掉,然后再重新创建一个连接;看起来有点绕,其实就是优先使用free容器里的连接,获取不到再释放available容器里的连接;如果经过上述过程仍然没有获取到可用连接,那就只能等待一个connectionRequestTimout时间,或者有其他线程的信号通知来结束整个获取连接的过程。

3.6 连接的释放

如果是长连接(reusable),则把该连接从leased容器中删除,然后添加到available容器的头部,设置有效期为expiry;如果是短连接(non-reusable),则直接关闭该连接,并且从released容器中删除,此时的连接被释放,处于free容器中;最后,唤醒“连接的产生与管理“第四部中的等待线程。

整个过程分析完,了解了new HttpResponseProxy(response, null)**返回的ConnectionHolder是null,结合IOUtils.closeQuietly(“连接的产生与管理”的步骤3的描述,在free容器为空的时候TimeUnit.SECONDS)构造函数,尤其是调用量较大的情况,防止发生不可预知的问题;可以通过设置evictIdleConnections(5, TimeUnit.SECONDS)定时清理空闲连接,尤其是http接口响应时间短,并发量大的情况下,及时清理空闲连接,避免从连接池获取连接的时候发现连接过期再去关闭连接,能在一定程度上提高接口性能。

五、写在最后

HttpClient作为当前使用最广泛的基于Java语言的Http调用框架,在笔者看来其存在两点明显不足:

没有提供监控连接状态的入口,也没有提供能外部介入动态影响连接生命周期的扩展点,一旦线上出现问题可能就是致命的;此外,其获取连接的方式是采用同步锁的方式,在并发较高的情况下存在一定的性能瓶颈,而且其对长连接的管理方式存在问题,稍不注意就会导致建立大量异常长连接而无法及时释放,造成系统性灾难。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:EdoJS- Web界面开发框架
下一篇:Springboot @Value注入boolean设置默认值方式
相关文章

 发表评论

暂时没有评论,来抢沙发吧~