2025 08 24 09:18:04 | 来源: 互联网整理
HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些优化空间。
北京时间X月X日,浏览器信息流服务监控出现异常,主要表现在以下三个方面:
2. 从PAAS平台Hystrix熔断管理界面中可以进一步确认问题机器的所有Http接口调用均出现了熔断:
3. 日志中心有大量从Http连接池获取连接的异常:org.apache.http.impl.execchain.RequestAbortedException: Request aborted。
综合以上三个现象,大概可以推测出问题机器的TCP连接管理出了问题,可能是虚拟机问题,也可能是物理机问题;与运维与系统侧沟通后,发现虚拟机与物理机均无明显异常,第一时间联系运维重启了问题机器,线上问题得到解决。
几天以后,线上部分其他机器也陆续出现了上述现象,此时基本可以确认是服务本身有问题;既然问题与TCP连接相关,于是联系运维在问题机器上建立了一个作业查看TCP连接的状态分布:
结果如下:
如上图,问题机器的CLOSE_WAIT状态的连接数已经接近200左右(该服务Http连接池最大连接数设置的250),那问题直接原因基本可以确认是CLOSE_WAIT状态的连接过多导致的;本着第一时间先解决线上问题的原则,先把连接池调整到500,然后让运维重启了机器,线上问题暂时得到解决。
调整连接池大小只是暂时解决了线上问题,但是具体原因还不确定,按照以往经验,出现连接无法正常释放基本都是开发者使用不当,在使用完成后没有及时关闭连接;但很快这个想法就被否定了,原因显而易见:当前的服务已经在线上运行了一周左右,中间没有经历过发版,以浏览器的业务量,如果是连接使用完没有及时关。
闭,250的连接数连一分钟都撑不到就会被打爆。那么问题就只能是一些异常场景导致的连接没有释放;于是,重点排查了下近期上线的业务接口,尤其是那种数据包体较大,响应时间较长的接口,最终把目标锁定在了某个详情页优化接口上;先查看处于CLOSE_WAIT状态的IP与端口连接对,确认对方服务器IP地址。
经过与合作方确认,目标IP均来自该合作方,与我们的推测是相符的。
在定位问题的同时,也让运维同事帮忙抓取了TCP的数据包,结果表明确实是客户端(浏览器服务端)没返回ACK结束握手,导致挥手失败,客户端处于了CLOSE_WAIT状态,数据包的大小也与怀疑的问题接口相符。
为了方便大家理解,我从网上找了一张图,大家可以作为参考:
CLOSE_WAIT是一种被动关闭状态,如果是SERVER主动断开的连接,那么就会在CLIENT出现CLOSE_WAIT的状态,反之同理;
通常情况下,如果客户端在一次http请求完成后没有及时关闭流(tcp中的流套接字),那么超时后服务端就会主动发送关闭连接的FIN,客户端没有主动关闭,所以就停留在了CLOSE_WAIT状态,如果是这种情况,很快连接池中的连接就会被耗尽。
所以,我们今天遇到的情况(处于CLOSE_WAIT状态的连接数每天都在缓慢增长),更像是某一种异常场景导致的连接没有关闭。
为了不影响其他业务场景,防止出现系统性风险,我们先把问题接口连接池进行了独立管理。
带着2.3的疑问我们仔细查看一下业务调用代码:
这段代码存在一个明显的问题:既关闭了数据传输流( IOUtils.closeQuietly(is) ),也关闭了整个连接( IOUtils.closeQuietly(httpResponse) ),这样我们就没办法进行连接的复用了;但是却更让人疑惑了:既然每次都手动关闭了连接,为什么还会有大量CLOSE_WAIT状态的连接存在呢?
如果问题不在业务调用代码上,那么只能是这个业务接口具有的某种特殊性导致了问题的发生;通过抓包分析发现该接口有一个明显特征: 接口返回报文较大,平均在500KB左右 。那么问题就极有可能是报文过大导致了某种异常,造成了连接不能被复用也不能被释放。
开始分析之前,我们需要了解一个基础知识: Http的长连接和短连接 。所谓长连接就是建立起连接之后,可以复用连接多次进行数据传输;而短连接则是每次都需要重新建立连接再进行数据传输。
而通过对接口的抓包我们发现,响应头里有Connection:keep-live字样,那我们就可以重点从HttpClient对长连接的管理入手来进行代码分析。
初始化方法:
进入PoolingHttpClientConnectionManager这个类,有一个重载构造方法里包含连接存活时间参数:
顺着继续向下查看
manager的构造方法到此结束,我们不难发现validityDeadline会被赋值给expiry变量,那我们接下来就要看下HttpClient是在哪里使用expiry这个参数的;
通常情况下,实例对象被构建出来的时候会初始化一些策略参数,此时我们需要查看构建HttpClient实例的方法来寻找答案:
此方法包含一系列的初始化操作,包括构建连接池,给连接池设置最大连接数,指定重用策略和长连接策略等,这里我们还注意到,HttpClient创建了一个异步线程,去监听清理空闲连接。
当然,前提是你打开了自动清理空闲连接的配置,默认是关闭的。
接着我们就看到了HttpClient关闭空闲连接的具体实现,里面有我们想要看到的内容:
此时,我们可以得出第一个结论:可以在初始化连接池的时候,通过实现带参的PoolingHttpClientConnectionManager构造方法,修改validityDeadline的值,从而影响HttpClient对长连接的管理策略。
2.6.2 执行方法入口
先找到执行入口方法:org.apache.http.impl.execchain.MainClientExec.execute,看到了keepalive相关代码实现:
我们来看下默认的策略:
由于中间的调用逻辑比较简单,就不在这里一一把调用的链路贴出来了,这边直接给结论:HttpClient对没有指定连接有效时间的长连接,有效期设置为永久(Long.MAX_VALUE)。
综合以上分析,我们可以得出最终结论:
HttpClient通过控制newExpiry和validityDeadline来实现对长连接的有效期的管理,而且对没有指定连接有效时间的长连接,有效期设置为永久。
至此我们可以大胆给出一个猜测:长连接的有效期是永久,而因为某种异常导致长连接没有被及时关闭,而永久存活了下来,不能被复用也不能被释放。(只是根据现象的猜测,虽然最后被证实并不完全正确,但确实提高了我们解决问题的效率)。
基于此,我们也可以通过改变这两个参数来实现对长连接的管理:
这样简单修改上线后,处于close_wait状态的连接数没有再持续增长,这个线上问题也算是得到了彻底的解决。
但此时相信大家也都存在一个疑问:作为被广泛使用的开源框架,HttpClient难道对长连接的管理这么粗糙吗?一个简单的异常调用就能导致整个调度机制彻底崩溃,而且不会自行恢复;
于是带着疑问,再一次详细查看了HttpClient的源码。
开始分析之前,先简单介绍下几个核心类:
connectionRequestTimout
connetionTimeout
socketTimeout
一定要注意:这里的超时不是数据传输完成,而只是接收到两个数据包的间隔时间,这也是很多线上诡异问题发生的根本原因。
free
leased
pending
available
注:由于存在maxTotal和maxPerRoute两个连接数限制,下文在提到这四种容器时,如果没有带前缀,都代表是总连接数,如果是r.xxxx则代表是路由连接里的某个容器大小。
maxTotal的组成
整个过程分析完,了解了httpclient如何管理连接,再回头来看我们遇到的那个问题就比较清晰了:
正常情况下,虽然建立了长连接,但是我们会在finally代码块里去手动关闭,此场景其实是触发了“连接的释放”中的步骤2,连接直接被关闭;所以正常情况下是没有问题的,长连接其实并没有发挥真正的作用;
那问题自然就只能出现在一些异常场景,导致了长连接没有被及时关闭,结合最初的分析,是服务端主动断开了连接,那大概率出现在一些超时导致连接断开的异常场景,我们再回到org.apache.http.impl.execchain.MainClientExec这个类,发现这样几行代码:
connHolder.releaseConnection()对应“连接的释放”中提到的步骤1,此时连接只是被放入了available容器,并且有效期是永久;
return new HttpResponseProxy(response, null)返回的ConnectionHolder是null,结合IOUtils.closeQuietly(httpResponse)的具体实现,连接并没有及时关闭,而是永久的放在了available容器里,并且状态为CLOSE_WAIT,无法被复用;
根据 “连接的产生与管理”的步骤3的描述,在free容器为空的时候httpclient是能够主动释放available里的连接的,即使连接永久的放在了available容器里,理论上也不会造成连接永远无法释放;
然而再结合“连接的产生与管理”的步骤4,当free容器为空了以后,从连接池获取连接时需要等待available容器里的连接被释放掉,整个过程是单线程的,效率极低,势必会造成拥堵,最终导致大量等待获取连接超时报错,这也与我们线上看到的场景相吻合。
HttpClient作为当前使用最广泛的基于Java语言的Http调用框架,在笔者看来其存在两点明显不足:
9月28日,百度手机浏览器Android版迎来重大更新!新版浏览器最大的亮点,是支持"直达号"的快捷进入。此外,它还持续优化了程序和页面的打开速度,让你的浏览“绝不等待”。
"直达号"于9月3日的2014百度世界大会正式推出,震惊业界。而所谓"直达号",就是商家在百度移动平台的官方服务账号,基于移动搜索、@账号、地图、个性化推荐等多种方式,让亿万客户随时随地直达商家服务。
而此次的百度手机浏览器Android 5.3版,主要就是新增了对"直达号"快捷进入的支持。具体来讲,通过在浏览器搜索框中输入”@ +商家名称或地点”等个性化信息,可跳过原有的搜索页面,精准直达用户所需商家服务的轻应用页面,用最短的路径让用户获得所需要的消息,随时随地直达所需!
以海底捞为例,如果很明确就是要去海底捞,可以直接在浏览框中输入"@海底捞",便可进入它的线上店铺。而如果不明确,那么可以输入"@火锅(川菜)"等,系统会根据输入内容,结合你的消费习惯、地理位置等,也能够推荐进入海底捞的店铺。如此,可以最大程度的挖掘和满足消费者的需求。
目前,海底捞、良子健身、宝泽行4S店等大量传统服务商都已经启用直达号。而先锋戏剧导演孟京辉,也于19日开通直达号,观众可在其中在线选座并购买工作室旗下所有话剧门票。不仅如此,兰州市政府旗下便民服务网站 "兰州三维服务网"也已经入驻百度,成为国内首个政府便民服务直达号。
面对百度手机浏览器新版的变化,业内人士指出,百度手机浏览器现在除了可以用来浏览信息、阅读小说、追影视剧外,它还成为了一个超级生活平台,在这个平台上,消费者能够通过在浏览器搜索框中输入”@ +商家名称或地点”等个性化信息的方式,快捷、方便地找到所需服务。
用户评论
我一直都使用的是vivo浏览器,还挺顺手的。
有15位网友表示赞同!
不知道为什么,vivo浏览器这个网页版一直没试过。
有17位网友表示赞同!
感觉现在网站越来越多的都是移动端体验优先啊。
有8位网友表示赞同!
希望vivo浏览器网页版的开发能更完善点。
有6位网友表示赞同!
其实手机版和电脑端的体验差别蛮大的。
有18位网友表示赞同!
vivo家产品都做得挺好的,这个网页版应该也不错吧?
有11位网友表示赞同!
平时都是用电脑浏览ウェブサイト比较方便。
有12位网友表示赞同!
希望vivo浏览器网页版的下载速度够快
有18位网友表示赞同!
这种跨平台的产品总觉得有些别扭。
有19位网友表示赞同!
我反而喜欢在手机上使用网站。
有14位网友表示赞同!
有的时候觉得在平板上看网页也很好。
有17位网友表示赞同!
希望能看到vivo浏览器网页版支持更多功能
有18位网友表示赞同!
用过vivo的手机,感觉他们的软件都挺人性化的。
有11位网友表示赞同!
希望vivo浏览器网页版可以跟手机版的体验接近。
有9位网友表示赞同!
对新产品还是抱有试试看的态度!
有16位网友表示赞同!
这个功能一直觉得很多小伙伴需要它,真的一点都不错的进步啦!
有7位网友表示赞同!
期待能看到vivo浏览器网页版的功能亮点!
有19位网友表示赞同!
有时候手机浏览不太方便,网页版正好可以弥补这部分需求。
有18位网友表示赞同!