Http客户端连接数占用分析

问题描述

Java应用程序中调用阿里云三方服务,阿里云告警服务SLB连接数逼近上限5000。

排查服务调用代码,使用okHttp3Client发起调用,默认使用长连接发起Http请求。

问题分析

由于调用阿里云服务的机器数量有限,如果正常使用http长连接,会在同一个tcp连接中发起多个http请求,不可能达到4000多的tcp连接数,极有可能是每次发起HTTP请求都是建立新的TCP连接,并且维持长连接导致tcp连接得不到释放导致连接数耗尽。

方案验证

  1. 搭建nginx服务器,模拟SLB并监控tcp连接数。在Debian linux虚拟机中安装nginx,nginx开放801、802、803端口,用来模拟客户端到不通域的连接。

  2. 编写测试demo

    1. 原代码中每次请求阿里云服务都会新建一个okHttpClinet,以这种的方式请求目标801端口,构成对照组,5个线程,每个线程请求200次。

    2. 使用原代码中方式构建httpClient,在请求头中添加Connection:close,建立短连接,请求802端口,5个线程,每个线程请求200次。

    3. 使用单例创建HttpClient,使用默认长连接,请求803端口,5个线程,每个线程请求200次。

  3. 运行demo并监控,在nginx服务器中通过命令 watch -n 1 “netstat -nap|grep 端口号”,分别监控801、802、803端口的连接数。

未运行demo时,可以看到nginx进程(5887)监听三个端口传入连接,监控如下:

image-20240810142600055

运行demo中,可以看到801端口有大量ESTABLISHED状态的连接,表明在801端口有大量的长连接在传输数据;在802端口中有大量的TIME_WAIT状态连接,等待进入最后的CLOSE状态;803端口则维持少量连接用来传输HTTP请求。

image-20240810142609503

继续监控会发现,在Spring环境下运行demo,会发现三个端口的连接都会维持一部分时间后关闭,其中802的连接60秒后先关闭,801、803的连接会先由ESTABLISHED(75秒)变为FIN_WAIT_2(60秒)后再维持一段时间关闭。

使用main方法直接启动demo,801、803端口的连接会在请求结束后立马关闭,802的连接仍会维持一分钟。

为探究其中的原因及机制,单线程各两次请求分别对三种请求进行抓包分析。

image-20240810142640670

Spring 801抓包:

image-20240810142705580

可以看到通过建立两次TCP连接发起两次HTTP请求,并且在75s开始断开TCP连接,断开阶段由服务器发起断开请求,收到客户端的ack后进入FIN_WAIT_2,由于后续没有收到客户端的挥手包,触发FIN_WAIT_2超时机制释放掉连接。

Main801抓包:

image-20240810142715164

对比Spring环境下的抓包,可以看到在两次请求结束后由客户端向服务器发送两个RST包,用于在进程结束时由客户端发送复位包关闭异常连接。

802抓包:

image-20240810142724140

802端口也是通过建立两个TCP连接完成了两次的HTTP请求,其中在挥手阶段,由服务器主动发起断开请求,在完整的完成四次挥手后,断开发起方(服务器)进入到TIME_WAIT阶段,其中在Linux的内核中,TCP/IP协议的TIME-WAIT状态持续60秒且不可轻易修改。

结论

  1. 原有的代码调用阿里云服务方式中,每次请求都会建立新的连接,并且连接持续时长为75s(keepalive)+60s(FIN_WAIT_2超时)=135s。

  2. 在请求头中加入Connecttion:close后,每次都会建立新的短tcp连接,但是由于发起断开连接方为服务器,服务器tcp连接会在进入TIME_WAIT后60s释放连接。

短链接方案能一定程度上缓解SLB连接数耗尽的问题,但要想彻底解决还需要正确的使用http长连接。