
我在压测组里泡了这么多年几乎每周都能在群里看到有人贴出这么一行日志java.net.BindException: Address already in use: connect第一次遇到的人基本都会懵明明被测服务还好好的怎么压测机先罢工了更诡异的是有时候重启JMeter就好了有时候重启也没用过一会儿又自己恢复。这种“薛定谔的报错”背后的原因其实是TCP/IP协议栈在客户端也就是跑JMeter的这台机器上积累了一个非常经典的资源耗尽问题。这篇文章就把这个报错掰开揉碎讲清楚从报错特征到根因从应急手段到一劳永逸的配置方案全部用的都是我在真实压测现场踩过的坑和验证过的方法。这期内容适合正在被这个报错折磨的JMeter新手也适合那些压测环境搭好了、一跑高并发就崩的老手甚至对做网关、中间件性能测试的同学一样有参考价值。文章里涉及的Windows和Linux两种客户端环境我都会讲因为实际工作中压测机两种系统都太常见了。1. 这个报错到底在说什么先分清“谁的锅”很多人一看到“Address already in use”就认为是端口被别的进程占用了然后在压测机上到处找是哪个进程抢了端口。这个方向不完全错但绝大多数情况下都是白费力气。要理解这个报错第一步是看清异常出现的阶段。1.1 报错出现的三种常见场景第一种连接建立时报错。JMeter发起请求客户端操作系统在调用connect()时直接抛异常日志里通常是ERROR o.a.j.JMeter: Error while processing sampler: java.net.BindException: Address already in use: connect这种情况最典型说明客户端根本没有办法为这次请求分配一个可用的本地端口。第二种JMeter启动时报错。比如监听端口被占用java.net.BindException: Address already in use: JVM_Bind这种和压测过程中的端口耗尽完全是两码事。JVM_Bind指的是JMeter自己要去监听某个端口比如命令行模式下默认的1099端口或者代理录制模式的端口时发现端口被占用。这个多半就是真的“端口被占用”了用netstat查一下谁占用了端口杀掉那个进程或者改JMeter的端口配置就行处理思路完全不同。第三种服务端返回的SocketException。这种更隐蔽日志里报的是服务端的IP和端口说明请求其实是发出去了但服务端处理连接时出了问题。这时候锅很可能在被压服务那边而不是压测机。1.2 核心概念TCP四元组与本地端口池要彻底搞懂这个报错绕不开一个底层概念——TCP连接的四元组。一条TCP连接由四个元素唯一确定源IP、源端口、目标IP、目标端口。只要这四个里面任何一个不一样就是一条不同的连接。压测场景下目标IP和目标端口通常是固定的比如压一个Nginx地址是192.168.1.10:80那系统能创建的连接数量天花板就取决于源IP和源端口。源IP一般也是固定的所以最终瓶颈就落在源端口上。每一个主动发起的TCP连接都要占一个本地端口。这个端口是从操作系统的动态端口范围里分配的。Windows默认是49152到65535Linux默认是32768到60999。也就是说一个客户端默认最多同时有大约16000多个可用的本地端口Windows/ 28000多个Linux。压测一旦跑起来每秒成百上千的请求端口很快就分完了。1.3 端口耗尽的那一刻发生了什么端口耗尽的直接原因通常不是“连接太多”而是TIME_WAIT状态的连接堆积。TCP协议在主动关闭连接的一端会进入TIME_WAIT状态并且要等一段时间才能把端口释放。这个时间是系统参数决定的Windows固定为120秒注册表可调Linux默认是60秒可通过内核参数调整。你可以算一笔账假如压测机每秒发出2000个请求每个请求的TCP连接用完就关那每一秒就会产生2000个TIME_WAIT状态的端口残留。即便系统给每个端口都分配出去120秒内就会积累24万个等待释放的连接。16000个端口哪里够用一旦端口池耗尽新的连接就无法建立操作系统直接抛出BindException。我不止一次在压测现场看到有人把JMeter线程数调到500然后看着报错日志一脸无辜——问题压根不是JMeter线程数太多而是连接没有复用短连接在疯狂地烧端口。提示记住这个关键区别——“Address already in use: connect”基本可以断定是客户端本地端口不够用了而“Address already in use: JVM_Bind”才是真正的端口被占用。2. 排查流程三步定位别一上来就改系统参数遇到这个报错第一反应应该是排查而不是改参数。因为“端口耗尽”只是一个表象背后的原因可能是连接复用没配好、TIME_WAIT堆积过多、压测机本身配置太低也可能是系统参数默认值太小。三步排查走下来基本能把根因锁死。2.1 第一步确认是本机还是被压服务的问题检查异常堆栈里有没有出现被测服务的IP和端口。如果在“connect”前面的地址是被压服务比如.connect(192.168.1.10:80)那问题很大概率在客户端如果异常是出现在读取响应阶段或者服务端日志里也有大量报错那就要两边一起查。我自己的习惯是先在被压服务上执行ss -sLinux看当前连接数再在压测机上执行netstat -ano | findstr TIME_WAIT | findstr 被测端口看TIME_WAIT的堆积情况。两边一对比基本就能定位是哪一端先顶不住了。2.2 第二步统计TIME_WAIT连接数量判断堆积程度这一步非常关键直接决定了要走哪套方案。在Windows命令行下执行netstat -ano | findstr TIME_WAIT | find /c /v 在Linux下执行ss -s netstat -ant | awk {print $6} | sort | uniq -c | sort -nr如果TIME_WAIT数量已经上万甚至接近两万那基本就是端口池被占满了。如果TIME_WAIT并不多但依然报错那就要考虑动态端口范围本身是不是被改小了或者压测机上还有其他进程在大量占用端口。2.3 第三步结合JMeter脚本判断是复用问题还是容量问题打开JMeter脚本看一眼HTTP Sampler的配置。如果TCP连接复用相关的选项没开后面第3节会详细说那每个请求都是新建连接、用完就关TIME_WAIT当然会爆发式增长。这种情况下就算把系统端口范围调大10倍也只是把报错往后推迟几分钟而已。还有一种情况是压测机本身的端口确实只够用这么点。比如默认Windows动态端口范围就是49152到65535全部放出来也就16384个端口去掉系统保留的实际可用的大概15000左右。如果你的压测目标就是“每秒3000个请求”那即便复用连接做得好某些极端场景下依然可能会撞到端口上限——这时候就要考虑方案层面的改动了。3. 五套解决方案从应急手段到根治措施排查完之后就要根据根因选择方案了。我把这五套方案按照“见效速度”和“根治程度”排了个序实际工作中我也是这么一层层处理的。3.1 方案一应急措施——重启JMeter或者等一会儿看到这个报错最直接的办法是先停掉压测等一两分钟让TIME_WAIT状态的连接慢慢释放然后再启动压测。这个办法“治标”都算不上但确实是现场最省事的应急动作。如果压测任务比较紧还有一个更快的办法——调低JMeter的并发线程数或者把压测时长切分成多段每段之间隔几十秒。这样做的好处是给系统一个“喘气”的时间窗口TIME_WAIT连接会逐渐被回收。但我不建议把这套作为常规方案因为压测最忌讳的就是“为了让压测能跑完而降低压力”那样测出来的数据失真等于白测。3.2 方案二真正管用的第一步——开启TCP连接复用这是我最推荐的起点也是最容易被忽略的优化点。JMeter的HTTP Sampler本身支持Keep-Alive但默认配置不一定适合你的压测场景。打开JMeter脚本在HTTP请求的Advanced页签下找到Timeouts和Implementation配置。关键是把HTTP请求实现方式设置为HttpClient4然后勾选KeepAlive相关选项。如果你用的是HTTP Request Defaults也可以在那一层统一配置。配好之后JMeter会在同一个TCP连接上复用发送多个HTTP请求连接不会每个请求都关闭重建。这样端口消耗速度会大幅下降TIME_WAIT的堆积量也会肉眼可见地减少。我自己实测过一个场景开200线程压一个接口不开启连接复用的时候不到2分钟就开始报Address already in use开启Keep-Alive之后跑了半小时端口池还很健康。就这么一个配置改动效果立竿见影。3.3 方案三调整操作系统参数——扩大端口池缩短TIME_WAIT如果连接复用已经开了但压测规模还是很大那就要动操作系统的参数了。Windows环境打开注册表编辑器找到以下路径HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters需要调整和新建如下几个值参数名推荐值作用TcpTimedWaitDelay30把TIME_WAIT的等待时间从默认120秒缩短到30秒端口回收更快MaxUserPort65534扩大可用于TCP连接的动态端口范围上限TcpNumConnections16777214提高系统级最大连接数修改完注册表必须重启操作系统才能生效。这个坑我踩过很多次改完不重启参数不生效还以为是修改没起作用。Linux环境Linux下用sysctl调整内核参数# 临时生效 sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_fin_timeout30 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_timestamps1 # 永久生效追加到 /etc/sysctl.conf echo net.ipv4.ip_local_port_range 1024 65535 /etc/sysctl.conf echo net.ipv4.tcp_fin_timeout 30 /etc/sysctl.conf echo net.ipv4.tcp_tw_reuse 1 /etc/sysctl.conf echo net.ipv4.tcp_timestamps 1 /etc/sysctl.conf sysctl -p这里有个值得注意的点net.ipv4.tcp_tw_reuse参数只对发起连接的一端生效也就是客户端视角。服务端的TIME_WAIT优化不能用这个参数服务端更合适的做法是调整tcp_fin_timeout或者用SO_REUSEADDR。很多文章把tcp_tw_reuse和tcp_tw_recycle混在一起说其实tcp_tw_recycle在Linux 4.12之后的内核里已经被移除因为它NAT环境下有严重问题会导致丢包和连接异常。看到网上老教程让你开tcp_tw_recycle的直接忽略。注意tcp_tw_reuse依赖时间戳选项tcp_timestamps如果关闭了时间戳这个参数不生效。3.4 方案四压测机扩容与多机分发如果调整到这一步还是发现单机端口根本不够用——比如你要压的目标本身就是万级QPS那单台压测机的端口池再怎么调也是有物理上限的。这时候的解法是把压力源从一台机器拆到多台机器。JMeter支持分布式压测一台Master控制多台Slave每个Slave用自己的IP去发起连接。这相当于把端口池的容量乘以N。分布式压测在Windows上配置不算复杂但有几个坑需要注意。Slave机的防火墙要放行JMeter默认的1099端口Master和Slave的JMeter版本尽量保持一致JDK版本差异可能导致RMI通信异常。另外每台Slave上也要做前面提到的TIME_WAIT优化否则Slave机自己也会同样报错。我见过最夸张的一次压测客户让我“用一台8C16G的机器压出一个网关的全部性能”压到5000 QPS就上不去了。后来换了4台压测机做分布式同样配置的网关轻轻松松跑到2万 QPS。问题很简单——不是网关不行是压测机单机端口池到顶了。3.5 方案五给JMeter设置JVM参数规避IPv6优先问题还有一个容易被忽略的小细节如果压测机的/etc/hostsLinux或hosts文件Windows里没有正确配置IPv4地址解析Java可能会优先尝试IPv6连接。某些网络环境下IPv6不可用底层socket创建就会出现异常表现为“Address already in use”。这个问题的排查方式是看JMeter日志里连接的目标IP是不是IPv6格式类似/0:0:0:0:0:0:0:1这种。如果确实走了IPv6可以在JMeter的启动脚本jmeter.bat或jmeter.sh里加上-Djava.net.preferIPv4StacktrueWindows下修改jmeter.bat找到JVM_ARGS相关行把参数加进去set JVM_ARGS-Djava.net.preferIPv4Stacktrue %JVM_ARGS%Linux下修改jmeter.shJVM_ARGS-Djava.net.preferIPv4Stacktrue $JVM_ARGS这个参数的作用是让Java在解析网络地址时优先使用IPv4避免IPv6环境配置不完整带来的额外端口占用问题。4. 实操过程与验证一套完整的高并发压测配置实录讲了这么多理论我用一个实际案例带大家走一遍完整操作。场景是压测一个电商系统的商品查询接口目标吞吐量是每秒2000个请求压测机是Windows Server 2019JMeter版本是5.6。4.1 压测前的系统参数调整我先在压测机上打开了注册表编辑器WinR输入regedit定位到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters右键新建了两个DWORD32位值TcpTimedWaitDelay设置为30十进制MaxUserPort设置为65534十进制TcpNumConnections设置为16777214十进制。然后重启系统。重启后我特意验证了一次参数是否生效。执行命令netsh int ipv4 show dynamicport tcp输出显示动态端口范围已经变成了从1024到65534说明注册表修改生效了。顺便提一句如果不想动注册表Windows也可以用命令临时设置netsh int ipv4 set dynamicport tcp start1024 num64511但这个命令重启后失效适合临时应急用不适合长期压测环境。4.2 JMeter脚本配置调整打开JMeter在线程组下面找到HTTP Request Defaults进入Advanced选项卡。HTTP请求实现方式选择HttpClient4勾选KeepAlive。另外把Response Timeout设成5000毫秒连接超时设成3000毫秒避免连接长时间挂起占用端口。这些配置看起来简单但影响了JMeter的行为模式连接会被复用而不是每个HTTP请求新建一个TCP连接。端口消耗速度从“每秒请求数”降级为“并发连接数”完全不是一个量级。4.3 压测过程中实时监控端口消耗压测启动后我每隔几秒执行一次netstat -ano | findstr TIME_WAIT | find /c /v 观察TIME_WAIT数量的变化曲线。稳定压测十分钟后TIME_WAIT数量一直维持在3000到5000之间完全没有触发Address already in use。同样的脚本、同样的线程数在没有开启KeepAlive之前跑了不到三分钟就开始大量报错。这个对比放在一起差距非常直观。4.4 JVM参数微调压测过程中我发现JMeter占用的CPU偏高顺手看了一下JMeter启动脚本里的JVM参数。默认堆内存只有1G高并发下频繁GC会导致采样线程卡顿间接影响压测结果的稳定性。在jmeter.bat里把堆内存调到了4Gset HEAP-Xms1g -Xmx4g -XX:MaxMetaspaceSize512m这个调整不直接解决Address already in use但能避免因为GC停顿导致的连接处理异常属于压测环境的基础调优。如果压测机内存足够建议把Xmx至少调到4G以上尤其是线程数多的场景。4.5 压测结果的合理性验证压测跑完以后我看了两个关键指标一是聚合报告里的吞吐量、错误率二是JMeter日志里还有没有BindException。如果错误率不为零就要回到日志里逐条排查错误类型。Address already in use的日志级别是ERROR在JMeter的jmeter.log里用关键字BindException一搜就知道还有没有。我这次压测跑完错误率是0.00%日志里干干净净。再看吞吐量稳定在2100 QPS左右超过了最初2000 QPS的目标说明这套配置完全撑得住这个量级的压力。5. 常见问题与排查技巧实录做了这么多次压测围绕这个报错衍生出来的问题和坑我整理了一份速查表基本能覆盖大家日常遇到的各种变种情况。5.1 问题速查表问题现象可能原因解决办法压测中报BindException且大量TIME_WAIT短连接未复用端口池被占满开启KeepAlive调整系统端口参数修改注册表后重启报错依旧注册表键值类型或路径错误检查是否为DWORD类型路径是否在Tcpip\Parameters下JMeter运行一段时间后偶发报错压测峰值时段端口瞬时耗尽降低并发线程数或开启连接复用增大MaxUserPort分布式压测时Slave机报错Slave机未优化系统参数对每台Slave机执行同样的系统优化配置报错在压测刚开始就出现有其他进程占用了大量端口用netstat排查端口占用或重启压测机日志中的报错是JVM_BindJMeter需要监听的端口被占用用netstat查找占用进程修改JMeter监听端口配置服务端日志大量报Address already in use被压服务自身连接耗尽优化被压服务的连接队列、文件句柄限制等Windows上改了sysctl错误理解在Windows上用了Linux命令Windows用注册表或netshLinux用sysctl5.2 修改了注册表还是报错大概率是这两个原因很多人改完注册表重启了系统跑压测还是报错。这种情况我遇到两次第一次是MaxUserPort和TcpTimedWaitDelay没建对键值类型创建成了字符串值系统根本不认。第二次是改了注册表但JMeter没有重启JVM在启动时已经缓存了系统网络参数只有重启JMeter进程才会重新读取这些配置。正确的操作顺序是修改注册表 → 重启操作系统 → 重启JMeter → 再跑压测。少了任何一步都可能“改了等于没改”。5.3 压测目标机也报这个错怎么办有一类场景比较恶心压测机不报错反而是被压的服务端报了Address already in use。这种情况通常是被压服务主动发起了大量外部连接比如调用下游接口、写入数据库或者服务端处理连接时本地端口耗尽。此时压测机调参已经没用了要到服务端去查它的系统参数和连接池配置。我曾经压过一个Java写的网关压到一定量级后它自己报BindException查了半天发现是网关转发请求时用的HTTP客户端没有做连接池限制每一个转发请求都新建连接。优化成连接池复用后问题直接消失。这类问题本质上是应用代码层面的连接管理策略有问题不是操作系统参数能解决的。5.4 压测结果测不准先看看是不是压测机自身的问题很多人忽略了压测机本身也是性能瓶颈。后台跑着各种程序、开了很多浏览器页面、动辄几百个TIME_WAIT积压压测机自己比被压服务还忙测出来的数据必然失真。我的习惯是压测机上只装JMeter和必要的监控工具其余服务一律不跑。压测前先用tasklist检查一下有没有占用CPU和内存高的进程发现异常先清理掉再开压测。压测机硬件配置不足的话建议优先做分布式压测不要把宝全押在一台机器上。6. 一些压测现场才用得上的补充经验做压测越久越觉得大多数性能问题其实都藏在最“低端”的地方——端口、连接、文件句柄、超时时间。这几个地方任何一个没配置好压测结果都会是假的。最后再分享几条我自己长期积累的小技巧。不要只盯着报错日志看要配套做端口和连接数的监控数据收集。我每次压测都会用脚本把netstat的输出定时记录下来压测结束后再回放时间线。有时候压测结果波动很大回头一看监控数据原来是压到某个时间点TIME_WAIT突然飙升导致的。有监控数据和没有监控数据排查效率天差地别。关于KeepAlive的配置有一点容易被忽略HTTP KeepAlive只在请求头里有Connection: keep-alive时生效如果被压服务端或者中间层比如Nginx主动关了KeepAlive或者设置了很短的keepalive_timeout你这边配置得再好也没用。压测前先curl -v看一下响应头里的Connection字段确认KeepAlive链路是通的。再补充一个关于连接复用和真实性的取舍问题。有些场景下开KeepAlive会让QPS看起来很高但服务端可能只是吃了“连接复用”的红利并不代表它在真实业务场景下能扛住同样的压力。如果你们压测的目的是评估生产环境的真实承载建议压测脚本里保留一定比例的新建连接或者干脆用长连接和短连接两种模式各压一轮综合评估。这一点很多新手不会想到。最后就是压测报告里一定要记录压测机的配置、系统参数、JMeter关键配置和版本信息。这样后续复现或者排查问题的时候不用靠拍脑袋回忆。我把每次压测的环境配置都写在一个markdown文件里和压测报告放在一起后面回看的时候省了太多事。