ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JMeter压测报错Address already in use:端口耗尽原理与系统调优方案

JMeter压测报错Address already in use:端口耗尽原理与系统调优方案 遇到jmeter address already in use这个报错很多人的第一反应是去压线程数、调循环次数甚至怀疑电脑配置不够。我一开始也这么干过结果就是问题反复出现压测报告根本没眼看。这个报错说白了不是脚本的问题而是压测机本地端口被消耗殆尽。理解了这一层排查和修复的方向就清晰了。这篇文章我从报错原理、定位方法、系统参数调优、JMeter脚本调整、压测策略五个层面完整过一遍适合做接口压测或性能测试时被这个报错卡住的同学。我主要用 Windows 压测机举例但 Linux 下的对应配置也会一起给出。1. 报错背后的TCP连接真相为什么本地端口会不够用1.1 一次HTTP请求背后发生了什么JMeter 压测时每个线程模拟一个用户每发一次 HTTP 请求底层 HttpClients 就会创建一个 Socket 去连目标服务器。这里有个容易被忽略的细节TCP 连接的四元组是“本地IP 本地端口 远程IP 远程端口”其中本地端口不是随便选的而是由操作系统从本机“动态端口范围”里临时分配一个空闲的。以 Windows 为例默认动态端口范围通常是49152-65535一共大约16384个端口。也就是说一台压测机上同一时刻能够建立的 TCP 连接数理论上限就是这么多如果全部处于未释放状态。而连接断开后端口并不会立刻回到可用池里而是要经过一个TIME_WAIT状态等待 2MSL 时间不同系统默认值有差异Windows 常见配置下可能在 120 秒上下主要是为了保证网络上延迟到达的旧报文不会干扰新连接。于是问题就来了压测时连接断开速度很快但端口回收速度很慢两者一旦失衡新连接就分不到端口了于是抛出Address already in use。这个报错里的 “address” 指的是本地IP:端口这个组合不是目标服务器不可达。很多人误以为是被压服务出了问题其实压测机自己先扛不住了。1.2 为什么浏览器没事压测机却爆掉普通上网时一个浏览器同时也就开几十个连接而且有连接复用机制端口消耗量级很小。压测则完全不同几百个线程每个线程每秒发几十个请求如果连接没有被复用每秒新建的连接数会非常夸张。拿我之前遇到的一个简单计算来说单机 500 线程每线程每秒发 1 个请求如果每个请求都新建 TCP 连接那 1 秒就产生 500 个新连接。Windows 默认动态端口 16384 个TIME_WAIT 周期按 120 秒算端口释放速度大约是每秒 136 个。500 的消耗速度对 136 的释放速度存量端口几分钟就见底。这还没算上多线程瞬时并发带来的尖峰。所以从机制上看address already in use的本质是本地端口池的消耗速度大于回收速度。方向找对了后面的所有处理手段都是在围绕“扩大端口池”和“降低消耗速度”这两件事做文章。2. 动手前先确认三步定位到端口耗尽问题2.1 第一步从报错堆栈里区分“本地问题”还是“目标问题”遇到报错先别急着改配置把 JMeter 的报错信息完整看一遍。address already in use通常伴随着java.net.BindException错误内容里会出现connect字样。这一点非常关键因为网络类报错很多方向错了会白折腾很久。报错信息含义排查方向java.net.BindException: Address already in use: connect本地端口分配失败端口耗尽压测机操作系统参数、连接复用Connect to x.x.x.x:8080 timed out目标服务器或网络路径不通服务端负载、防火墙、网络链路org.apache.http.conn.HttpHostConnectException: Connect to x.x.x.x failed目标地址建立连接失败服务可用性、连接数限制、网络Connection reset by peer服务端主动断开服务端连接池、超时时间如果日志里是Address already in use: connect那基本上可以肯定是压测机本地端口池的问题。这里还需要留意一个近似报错Cannot assign requested address。在 Windows 上压测时它和Address already in use经常成对出现。前者偏向“可用端口池耗尽”后者偏向“端口还在 TIME_WAIT 里没释放”根因都是端口分配失败处理方式完全一样。2.2 第二步用 netstat 统计 TIME_WAIT看真实数据确定是本地端口问题后别靠猜直接上命令统计。在 Windows 的 cmd 里执行netstat -ano | findstr TIME_WAIT | find /c /v Linux 下用netstat -an | grep -c TIME_WAIT如果想持续观察变化趋势Linux 下可以用 watch 每 2 秒刷新一次watch -n 2 netstat -an | grep -c TIME_WAIT判断标准很简单如果 TIME_WAIT 数量长期徘徊在 14000 以上而你的动态端口范围默认只有 16384基本可以盖章定论——端口池快见底了。我在实际压测中见过 TIME_WAIT 冲到 15000 时开始批量报address already in use的情况和端口池上限高度吻合。另外建议同时统计一下 ESTABLISHED 状态的数量因为持续占用中的连接同样占端口netstat -ano | findstr ESTABLISHED | find /c /v 如果 ESTABLISHED 本身就有上万个说明压测并发高到单机已经兜不住了这种情况后面还得回到压测策略上去解决。2.3 第三步核对当前动态端口范围Windows 下查看当前动态端口范围netsh int ipv4 show dynamicport tcp正常未改过的机器输出里协议 tcp 的动态端口范围一般是49152-65535也就是 16384 个端口。Linux 下查看cat /proc/sys/net/ipv4/ip_local_port_range默认一般是32768 60999大约 28232 个端口比 Windows 默认多一点但同样会被压测打满。把这三步做完问题定位就很扎实了报错类型确认、TIME_WAIT 数量接近上限、动态端口范围确认。三者对上了就可以放心地进行下一步修复。3. 系统层修复把动态端口池做大把TIME_WAIT时间缩短3.1 Windows压测机的两个关键调整Windows 下第一件事就是把动态端口范围扩大。用管理员身份打开 cmd执行netsh int ipv4 set dynamicport tcp start1025 num64511这条命令把动态端口起点改到 1025数量 64511也就是让端口池覆盖到1025-65535可用端口从约 1.6 万提升到约 6.4 万。执行后建议再执行一次netsh int ipv4 show dynamicport tcp确认生效。这个命令改的是操作系统分配临时端口时的可选范围已建立的连接不受影响通常不需要重启立即生效。为什么不直接start1因为 1024 以下的端口是保留端口而且 1025 避开了一批常见服务监听端口。如果压测机上本身还跑着数据库、Web服务等大量固定端口业务建议不要这么激进可以改成start20000 num45535之类留出空间避免冲突。第二件事是缩短 TIME_WAIT 的保持时间。Windows 上对应的参数是注册表里的TcpTimedWaitDelay执行reg add HKLM\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters /v TcpTimedWaitDelay /t REG_DWORD /d 30 /f把值设为 30单位是秒。这个参数修改后需要重启系统才能生效。适当调低 TIME_WAIT 时间端口就能更快被重新用于新连接。不过要有个概念TIME_WAIT 本身是为了防止延迟的旧报文串到新连接里压测环境为了吞吐可以牺牲一点这层保护生产环境不建议盲目调。这两个调整叠加后的效果可以用数字直观感受一下配置端口数量TIME_WAIT时间理论每秒可新建连接上限Windows 默认16384120秒约136扩大端口池 缩短TIME_WAIT6451130秒约2150同样是单机压测优化前每秒新建连接超过 136 个就会开始堆积优化后到 2000 以上才需要担心量级完全不一样。需要注意的是“理论每秒上限”不等于建议跑满给系统留余量永远是压测的基本原则。实测中单机每秒建连长期稳定在几百到一千出头是比较健康的状态超过一千五就要考虑分布式了。3.2 Linux压测机对应的 sysctl 配置如果压测机是 Linux对应调整的是这三个参数sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_fin_timeout30 sysctl -w net.ipv4.tcp_tw_reuse1ip_local_port_range对应扩大端口池tcp_fin_timeout对应缩短 TCP 连接释放时间tcp_tw_reuse允许客户端在有时间戳的情况下复用处于 TIME_WAIT 的连接——压测机作为连接发起方这个参数对端口的释放效果非常明显。要让配置持久化写入/etc/sysctl.confnet.ipv4.ip_local_port_range1024 65535 net.ipv4.tcp_fin_timeout30 net.ipv4.tcp_tw_reuse1 net.ipv4.tcp_timestamps1然后执行sysctl -p加载。Linux 压测机还需要多注意一个点文件描述符限制。端口再大进程打不开文件描述符也没用。压测前检查ulimit -n如果只有 1024果断调大否则配合扩大后的端口池会较早碰到Too many open files。这个虽然和address already in use报错不同但同样是单机压测时的经典瓶颈顺手一起处理掉最省事。3.3 修改系统参数的操作风险和注意事项系统层的参数修改有几个坑先说清楚注册表和 sysctl 都需要管理员权限。Windows 的TcpTimedWaitDelay需要重启生效netsh 的端口范围变更通常不需要。压测机如果同时承担业务功能改端口起始值前要先确认不会影响现有随机端口业务。TIME_WAIT 压到 30 秒在压测场景够用但调更小就没有太大意义了节省的那点时间换不来更多收益。这些参数都是压测机侧的不是被压服务端侧的。被压服务端如果也有 TIME_WAIT 堆积那又是另一套排查逻辑别混在一起。曾在公司里见过一种做法压测机参数改好了被压服务器 Nginx 也频繁报address already in use结果是因为服务端高并发短连接同样耗尽了自己的端口池。两个方向的调整都要做但本文核心聚焦在压测机这一端。4. JMeter侧优化让连接活得更久、复用得更多4.1 KeepAlive最容易忽略也最有效的一项JMeter 的 HTTP Request 采样器里默认勾选了Use KeepAlive但不少场景下实际并没有生效。KeepAlive 的作用是让同一个线程的多次请求复用同一个 TCP 连接避免每个请求都走一遍“建连-断开”的流程。如果服务端响应头里带了Connection: close或者响应没有正确的Content-Length、不是 chunked 传输客户端就无法安全复用连接只能每次新建。这种情况下你勾不勾 KeepAlive连接照样每次重建。排查方法很简单压测时用抓包工具或者服务端连接日志看如果请求频率很高但连接数基本稳定在并发数上下说明 KeepAlive 生效了。如果连接数一直在涨或者忽上忽下大概率是没生效。确认响应头里有Connection: keep-alive或合法的Content-LengthKeepAlive 才能真正发挥作用。在压测脚本里的操作层面我建议把所有 HTTP Request 都显式保持 KeepAlive 勾选而不是依赖全局默认。脚本里的请求如果指向不同域名不同 host 之间的连接池是分开的这一点不用过度担心但要清楚“ KeepAlive 是针对同一 host 的连接复用”。4.2 关闭自动重试避免重复请求制造额外连接JMeter 的 HTTP 请求基于 HttpClient4 实现时存在重试机制具体行为受jmeter.properties里的httpclient4.retry.count影响。压测场景里我不太建议开启自动重试网络抖动时重试会额外制造请求量这些重试请求既污染性能数据又会带来额外的连接建立和端口消耗。压测要测的是系统在正常负载下的表现而不是测它被重试流量反复轰炸时的表现。可以在jmeter.properties里把重试次数改为 0httpclient4.retry.count0改完重启 JMeter 生效。这个细节常规教程很少提但在一次长时间压测里重试流量叠加起来足以把端口池消耗速度再拉高一截。4.3 HTTP Cache Manager 和其他减少请求数的手段如果压测场景里有大量静态资源请求图片、CSS、JS 等可以考虑在测试计划里加一个HTTP Cache Manager。它模拟浏览器缓存行为缓存命中后不再向服务器发出请求请求数减少连接数自然就减少。但要注意如果压测目标是核心业务接口的动态响应Cache Manager 对接口请求本身没有任何帮助别指望它能解决address already in use。它只对混合场景里的静态资源部分有意义。真正减少短命连接的核心思路是让线程在执行脚本期间保持有持续的请求而不是频繁地建连、发一两个请求就结束。比如一个线程只发一次登录请求就结束那每个线程生命周期里必然产生一个完整的新连接加上 TIME_WAIT 的延迟释放端口消耗和线程数几乎是线性关系。如果你压的是登录这种“一次请求即结束”的场景可以考虑增加循环次数让同一线程重复执行登录-登出动作或者合并请求把多个动作放到一个事务里串行执行延长连接生命周期。另外提一个容易被忽略的点HTTP Request默认实现是 HttpClient4有些人看到报错后会去把Implementation改成Java以为换个实现就好了。实际上 Java 实现也有自己的连接管理机制换实现如果不解决连接复用和系统参数问题大概率是换个姿势继续报。根因不在这不值得优先折腾。5. 压测策略层面的补救不能只靠加线程数5.1 Ramp-Up 与思考时间让连接建立变得平缓很多人建压测计划时把Ramp-Up Period设成 0也就是所有线程在瞬间同时启动。500 个线程在同一秒钟争抢本地端口瞬时建连速率是 500/秒端口池的消耗曲线直接拉满TIME_WAIT 在压测刚开始的前几秒就会堆积出一个高峰。我现在的习惯是线程数 500 时Ramp-Up 至少给到 60 秒让线程均匀地逐步启动。这样建连速率被摊平到每秒 8 个左右配合系统参数调整后端口池完全能兜住。Ramp-Up 拉长还能顺带模拟更真实的用户进入曲线而不是一股脑全部涌进来压测结果本身也更可信。思考时间Think Time同样重要。如果脚本里没有任何等待那每个线程的请求循环频率完全由服务器响应时间决定在响应极快的接口上每秒请求数会非常高。在事务之间加一个合理的定时器延迟比如Constant Throughput Timer或Uniform Random Timer让请求速率接近真实用户行为端口消耗会平缓很多。5.2 用恒定吞吐量定时器代替盲目加并发性能压测很容易陷入一个误区为了更高的 TPS一味把线程数往上加。但线程数越高端口消耗速度越快到后面根本不是目标系统扛不住而是压测机先趴下了。更好的做法是用Constant Throughput Timer把目标吞吐量卡住。比如你只需要验证系统能不能稳定支撑 1000 TPS那就设常量吞吐量定时器目标为每分钟 60000 次1000 TPS让 JMeter 自动控制发压速率而不是靠 1000 个线程在那儿空转。线程数只要足够支撑这个吞吐量就行多出来的线程纯粹是在消耗本地端口和系统资源。这个思路的本质是把“压测”从“堆并发”变成“控速率”对定位系统瓶颈的准确度也有帮助。因为线程太多时响应时间的统计里会混入客户端线程调度的噪声数据反而不干净。5.3 多机分布式用多张网卡、多台施压机摊薄端口消耗单机优化参数后仍然不够用的情况是真实存在的。特别是压测网关、登录这类需要频繁建连的场景每秒新建连接上千个时即便端口池扩大、TIME_WAIT 缩短单机仍然可能紧张。这时候的解法是分布式压测。JMeter 的分布式模式里Master 负责编排脚本Slave施压机负责真正执行请求。每台 Slave 都有自己独立的动态端口池所以整体端口能力是线性叠加的。比如原来单机扛不住 2000 连接/秒扩展到 3 台施压机每台只需要承担 700 左右压力就降下来了。分布式压测需要配置jmeter.properties里的remote_hosts把施压机地址填进去然后在 Master 上远程启动。操作本身不复杂但要多注意施压机之间的时间同步和脚本包的一致性否则采集的数据会有偏差。如果只是临时需要更多端口不用上完整分布式也可以直接在同一台压测机上多跑几个 JMeter 进程每个进程分开端口池——不过这种方式要注意多进程的总资源开销以及监控和汇总会分散。5.4 各种手段怎么选一张对照表手段解决的问题操作成本适用场景扩大动态端口范围端口池总量不足低命令改动端口耗尽初现单机还能跑缩短 TIME_WAIT端口回收速度太慢低需重启TIME_WAIT 数量持续走高开启/确认 KeepAlive请求频繁新建连接低脚本级短连接场景、混合事务压测关闭 HTTP 自动重试重试流量叠加消耗端口低改配置文件长时间稳定性压测拉长 Ramp-Up瞬时建连尖峰低改压测计划高线程数启动瞬间报错恒定吞吐量定时器并发不可控、CPU空转中需设计目标 TPS 明确做容量验证分布式多机压测单机端口池和资源上限高需环境压测规模大、长时间高并发从成本角度我建议按表格从上往下依次尝试。大部分项目在“系统参数 KeepAlive 压测策略”这三层就能解决address already in use直接跳到分布式反而把问题复杂化了。6. 实测复盘一次压测从报错到稳定的完整变化6.1 压测前的配置和现象之前帮朋友调一个内部系统的验收压测压测机是 Windows Server 20168核16G目标是一个 HTTPS 查询接口。压测计划是 300 线程、循环 100 次、Ramp-Up 10 秒。压测跑到第 4 分钟左右监听窗口开始随机冒java.net.BindException: Address already in use: connect而且报错频率越来越高。当时第一反应是线程数开太大把线程降到 150 重新跑结果只是把报错出现的时间往后推了一两分钟问题依旧。停下来后用 netstat 统计TIME_WAIT 数量接近 14000动态端口范围是 Windows 默认的49152-6553516384 个端口几乎被占满。到这里方向已经很清楚了单机端口池在短连接高并发下被打穿了。6.2 调整过程与实际参数变化因为压测机是专用的我直接做了一套组合拳管理员执行netsh int ipv4 set dynamicport tcp start1025 num64511把端口池扩到 6.4 万。注册表设置TcpTimedWaitDelay30重启压测机。确认脚本里所有 HTTP Request 都勾选了 KeepAlive并且检查了服务端响应头确认Content-Length正常、没有Connection: closeKeepAlive 是真生效。jmeter.properties里把自动重试次数改为 0。Ramp-Up 从 10 秒调整到 60 秒避免瞬时建连尖峰。重启后的第一轮压测300 线程循环 100 次跑完全程没有报错。当时特意跑压测的同时持续观察 TIME_WAIT曲线从之前的“直线上升逼近 16000”变成了“涨到 5000 左右波动”稳定后维持在 3000-5000 之间端口池余量充足。而且有个意外收获去掉自动重试并确认 KeepAlive 生效后TPS 比之前高了大约 8%。原因不复杂之前大量新建连接本身消耗了 CPU 和系统时间连接复用后请求开销明显下降。6.3 验证时我额外注意的几个点第一改动系统参数后一定要做回归验证不能只跑一轮就下结论。长压测中 TIME_WAIT 的堆积是慢性的最好跑一轮 30 分钟以上的持续压力确认曲线平稳。第二注意区分修改前后的变量。一次只改一个层面观察它对端口曲线的影响这样排查记录才是可追溯的。我之前就有过一次同时把端口参数、脚本循环和定时器都改了结果出了问题不知道是哪一步引入的教训。第三压测机上如果没有承载其他业务系统参数可以直接往比较激进的值调如果有业务改完端口范围后要确认业务使用的随机端口逻辑不受影响必要的时候在低峰期操作。回头看这个问题的解决过程最核心的认知转变就是报错虽然出现在 JMeter 的采样器里但address already in use不是被压系统的问题而是施压端本地端口池管理的问题。只要把“扩大端口池、加快端口回收、减少不必要建连”这三件事做扎实这个报错基本就能根治。最后再分享一个我常留作底线的检查方式压测开始前先跑 5 分钟快速验证同时监控 TIME_WAIT 的曲线斜率。如果从开始就在线性爬升说明连接复用或端口参数还没到位这时候停下来调整比等报错批量出现再排查要省力得多。压测这东西施压机本身的状态往往决定了你能不能拿到靠谱指标先把自家后院的端口问题收拾干净再去看被压系统的表现才有意义。
返回列表