ARTICLE DETAIL

资讯详情

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

Java报错No buffer space available?其实是Windows动态端口被榨干了

Java报错No buffer space available?其实是Windows动态端口被榨干了 凌晨一点监控告警把我和值班同事同时震了起来Java服务日志里刷出了一片刺眼的报错——java.net.SocketException: No buffer space available (maximum connections reached?): connect。当时我的第一反应是内存不足毕竟报错里带着 buffer space 字样结果内存查了一圈完全正常CPU 也不高但服务就是大面积超时。折腾了两个多小时最后才发现根子根本不在 JVM 里而是这台 Windows 机器上的 TCP 动态端口被榨干了。这个坑我踩过身边不少同行也踩过相关搜索热度一直不低。但很多帖子只给一句“执行 netsh 扩大动态端口范围”不讲为什么读完能抄但不懂原理换台机器换套环境又不会排查了。这篇文章把我完整的排查链路、背后的 TCP 工作原理、系统参数调整和应用层改造方案全部写清楚给所有跑在 Windows 上的 Java 应用做个参考。1. 从一条日志到服务雪崩报错的真实场景与表象1.1 日志现场的典型面貌当时服务日志里出现的完整异常长这样java.net.SocketException: No buffer space available (maximum connections reached?): connect at java.base/java.net.Socket.connect0(Native Method) at java.base/java.net.Socket.connect(Socket.java:634) at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:139) ...注意报错信息后半段自带的括号提示(maximum connections reached?)这是 Windows 底层在告诉你“连接数可能到顶了”。调用栈通常指向Socket.connect或SocketChannel.connect也就是代码里主动发起的出站 TCP 连接操作。也就是说这个错不是接收连接时爆的而是“主动连别人”时爆的。1.2 最容易踩坑的三类场景我复盘了自己遇到的情况以及帮别人排查过的案例基本逃不出下面这几类Windows 服务器上跑 Java 服务对外高频发起 HTTP/RPC 调用。这是最常见的一种。业务逻辑里每来一个请求就要向下游服务发一次请求而下游服务又没开连接池导致每次请求都新建连接。定时任务批量并发拉数据。比如每天凌晨整点跑批几十个线程同时向外连数据库、连第三方接口瞬间把连接数拉高。压测环境。用 JMeter 或自研脚本压测时QPS 一上来Java 应用作为调用方疯狂建连接几分钟之内端口池就空了。热搜里也有jmeter报错org.apache.http.conn.httphostconnectexception这类关键词说明压测阶段遇到连接问题非常普遍。我自己的案例是定时任务触发的每天凌晨有几十个批任务同时启动每个任务都新建 HTTP 连接去拉数据。峰值时每秒新建连接接近 150 个Windows 默认只给 16384 个动态端口坚持不到两分钟就开始大面积抛错。1.3 很容易被误判的“假线索”遇到这个报错最坑的是它自带误导属性。我第一反应是查内存相信很多人也一样结果 JVM 堆没问题物理内存也没问题直接白查了半小时。这里先把容易出现误判的几个方向列出来方便你对照排除以为是 JVM 堆内存或物理内存不足报错里“buffer space”这个词实在太像内存告警了。实际上如果内存不足通常会先有OutOfMemoryError而不是SocketException。以为是防火墙或网络策略拦截防火墙拦截连接时一般是Connection refused或Connection timed out不会出现 “No buffer space available”。以为只是代码里“忘记关闭连接”导致的句柄泄漏连接泄漏确实会让句柄数上升但就算每条连接都正确关闭只要建立频率够高TIME_WAIT 依然会堆积照样报这个错。真正应该做的是把目光从 JVM 转移到操作系统网络协议栈。下面我逐步拆解这个错误的本质。2. WSAENOBUFS 的真面目No buffer space available 到底缺的是什么2.1 Windows 错误码 10055 的官方含义在 Windows 平台上SocketException背后映射的是 Winsock 错误码WSAENOBUFS10055。微软官方文档描述是由于系统缺少足够的缓冲区空间或者因为队列已满无法对套接字执行某项操作。关键点在于这里说的“缓冲区”并不是我们脑子里的那根内存条而是 TCP/IP 协议栈为每个套接字分配的传输缓冲区以及发起出站连接时需要的临时端口资源。你主动connect一个远端地址时内核要做两件事第一从动态端口范围里挑一个空闲的临时端口第二为这个套接字分配发送/接收缓冲区。这两件事任何一个做不成都报 buffer space 相关错误。在实际高并发场景下缓冲区内存一般够用真正被抢光的是临时端口池也就是俗称的动态端口。2.2 临时端口的一生从被选中到 TIME_WAIT 释放每个 TCP 连接由四元组唯一标识本地 IP 本地端口 远端 IP 远端端口。对外发起连接时本地端口不是你指定的而是内核从动态端口范围里自动分配的。连接关闭之后这个端口不会立刻回到可用池而是进入TIME_WAIT状态等待 2MSLMaximum Segment Lifetime报文最大存活时间之后才能复用。Windows 上默认 2MSL 是 120 秒也就是有一个TcpTimedWaitDelay注册表参数在控制。打个比方这就像餐厅的叫号小票。客人拿号入座结账离开后这个号不能马上发给下一个人得等服务员确认上一桌的账单、餐具、遗留物品都处理干净了号码才敢再发出去。TCP 也是这个逻辑——旧连接的迟到报文可能还在网络中游荡如果立刻把端口复用给新连接新连接就可能收到旧报文导致数据错乱。所以TIME_WAIT是 TCP 可靠性的基石不能取缔。能做的只有两条路缩短等待时间TcpTimedWaitDelay调小或者扩大可用端口池动态端口范围调大。2.3 “maximum connections reached?” 的含义报错信息里的(maximum connections reached?)是 Windows 附加的一句提示性文案。它其实非常直白你发起的连接数已经触碰到了系统层面的上限。这个上限的物理承载者就是动态端口池的剩余数量。同一个端口理论上可以同时被多个连接使用吗可以只要四元组里的远端 IP 或远端端口不同就行。但在“一个 Java 服务向同一个下游服务发起大量并发连接”的典型场景里远端 IP 和端口往往是固定的能变化就只剩下本地端口所以本地端口池就成了硬约束。一旦池子见底新的connect请求就无法分配端口异常随之而来。2.4 什么时候要怀疑“另有原因”动态端口耗尽是最常见的原因但不能看到报错就无脑下结论。我给一个区分原则如果netstat里 TIME_WAIT 连接数常年徘徊在万台级别且动态端口范围只剩 20% 以内那几乎可以确定是端口耗尽如果连接数不高、端口余量充足却依然报No buffer space available这时候才需要去考虑非分页缓冲池Nonpaged Pool内存耗尽、安全软件驱动LSP/WFP 驱动占用资源等冷门问题。3. 现场取证三步定位动态端口耗尽的完整排查链路被这个报错折腾过的人都知道直接改注册表、放大端口范围是能“解决”但不看清楚每个环节就动手容易误判也可能改完还是复发。我习惯按下面这套链路走一遍从现象到根因全程取证基本十分钟内心里有数。3.1 第一步确认机器系统与目标进程先确认这套 Java 服务跑在什么系统上。No buffer space available这种措辞在 Windows 上出现频率最高Linux 下一般是Cannot assign requested address后面我会专门对照。确认命令tasklist /fi imagename eq java.exe找到对应 PID一会儿netstat的结果要对着 PID 看判断哪些连接是当前 Java 进程产生的。3.2 第二步用 netstat 统计连接状态分布在管理员 CMD 或 PowerShell 里执行netstat -ano | findstr TIME_WAIT | find /c TIME_WAIT这个命令会直接数出当前机器的 TIME_WAIT 连接总数。如果数量已经过万大概率中招了。更精细一点把结果导出到文件再区分是哪个 PID 的netstat -ano | findstr TIME_WAIT timewait.txt findstr 你的JavaPID timewait.txt | find /c TIME_WAIT还可以顺手看看ESTABLISHED活跃连接、CLOSE_WAIT等待关闭的数量netstat -ano | findstr ESTABLISHED | find /c ESTABLISHED netstat -ano | findstr CLOSE_WAIT | find /c CLOSE_WAIT这三个状态数字的意义完全不同连接状态说明排查含义TIME_WAIT 多连接已关闭端口在等待复用建连频率太高默认等待 120 秒导致堆积ESTABLISHED 多当前活跃的连接很多可能并发量真的很大也可能连接没释放CLOSE_WAIT 多连接已被对端关闭本地程序没有调用 close几乎可以断定代码里有连接泄漏我当时统计出来的结果是 TIME_WAIT 一万三千多ESTABLISHED 两千多CLOSE_WAIT 也有好几百。CLOSE_WAIT 的那几百个说明代码里还有没被关闭的连接但即便忽略这些光靠 TIME_WAIT 也已经把端口池快占满了。3.3 第三步查看动态端口范围接下来看系统到底给了多少个可用端口netsh int ipv4 show dynamicport tcp netsh int ipv4 show dynamicport udp输出类似协议 tcp 动态端口范围 --------------------------------- 起始端口 : 49152 端口数量 : 16384这就是 Windows 出厂默认配置动态端口从 49152 到 65535总共 16384 个。也就是说这台机器最多只能同时维持一万六千多个处于“占用”状态的出站连接包括 TIME_WAIT、ESTABLISHED 等等。3.4 第四步计算余量判断是否踩线用第一步统计出来的“占用端口总数”除以 16384就是端口池使用率。比如 TIME_WAIT 一万三加上 ESTABLISHED 两千多加起来已经一万五千多使用率超过 90%。这个数字意味着只要再来一波瞬时并发端口池直接清空新连接全部报错。到这里根因基本锁定了动态端口池枯竭端口消耗速度远大于释放速度。40 分钟级别的时间窗口内如果还没有恢复再用资源监视器看网络活动但一般走到这一步该查的已经查清了。4. 默认 16384 个端口为什么不够用算一笔 TIME_WAIT 的账4.1 端口占用的稳态公式要理解为什么“看起来很多”的 16384 个端口这么容易爆得先算一笔账。TIME_WAIT 的稳态数量约等于每秒新建连接数 × TIME_WAIT 等待时间默认 120 秒举个直观的例子每秒新建连接数TIME_WAIT 等待 120 秒是否逼近 16384 上限506,000安全但已不宽裕10012,000危险余量不到 30%15018,000爆了20024,000彻底雪崩每秒只建 100 个连接对一台业务机器来说高吗不算高。很多 Java 服务在高峰期只要被三个上游同时调用每个上游每秒三四十个请求一下子就到这个量级了。4.2 为什么“正确关闭连接”也救不了这是我踩过最深的坑代码里每条连接都正确 close 了但服务还是报错。后来才反应过来正确关闭只是让端口进入了 TIME_WAIT 而不是 CLOSE_WAIT但只要连接是“短连接 高频创建”TIME_WAIT 的堆积速度完全取决于“新建速率”而不是“有没有关闭”。正确关闭连接只解决了一半问题把端口从“泄漏”变成了“正常等待复用”。可 Windows 默认要等 120 秒每秒建 150 个连接等 120 秒算下来就会积压 18000 个 TIME_WAIT而端口池只有 16384必然爆。另一个放大因素是异常分支不关闭连接。比如代码里connect成功了但在读写阶段抛出异常没有走 finally 块这条连接就挂在 CLOSE_WAIT 上没人管。CLOSE_WAIT 是连接被对端关闭、本地程序没关的状态它会一直占着端口比 TIME_WAIT 更可怕因为 TIME_WAIT 至少 120 秒后会释放CLOSE_WAIT 可能挂到天荒地老。4.3 为什么 Windows 默认只给了 16384 个端口这是历史设计留下的坑。Windows 动态端口的默认起始值是 49152这原本是为了避开低于 49152 的“已注册端口段”避免与系统服务、传统应用冲突。这个设计在二十年多前是够用的当时一个应用服务器同时维持几百个连接就算重负载了。今天完全不一样了微服务架构下每个应用都是“连接大户”一个 Java 服务可能同时要调用十几个下游每个下游又要维持几十上百个连接再加上短连接反复横跳16384 个端口很快就不够看。我不是说微软设计错了而是在现在的部署密度下默认参数确实需要按实际负载重新调。4.4 缩短等待时间的数学收益如果把默认的 120 秒缩短到 30 秒结果会立刻不同每秒建 150 个连接稳态 TIME_WAIT 变成约 4500 个每秒建 300 个连接也才 9000 个。端口池立刻回到安全水位。这也是为什么“扩大端口范围 缩短 TIME_WAIT”能成为标配组合拳的原因两者不是在抢同一个空间而是分别从“池子大小”和“占用时长”两头解决问题。5. 两条腿走路系统参数调整与 Java 应用层改造的配合方案系统参数调整是止血应用代码改造是根治。只调参数不改代码端口池变大只是把爆炸时间往后推只改代码不调参数又可能在某些流量突刺时被打穿。正确的姿势是同时做。5.1 系统侧扩大动态端口范围改动前先把当前配置备份下来方便回滚netsh int ipv4 show dynamicport tcp netsh int ipv4 show dynamicport udp然后用管理员权限修改netsh int ipv4 set dynamicport tcp start1025 num60000 netsh int ipv4 set dynamicport udp start1025 num60000改完再查一次确认生效netsh int ipv4 show dynamicport tcp会看到起始端口 1025端口数量 60000也就是说动态端口池从原来的 16384 直接扩到 60000。为什么从 1025 开始因为 1024 以下的端口大多被系统服务保留从 1025 起跳能避开常规冲突同时也更贴近 Linux 上ip_local_port_range的常用设置。注意几点已经建立的连接不受影响新发起的连接会从新范围里分配。有的机器立即生效有的需要等一会或重启按低峰变更窗口来操作。不要把范围扩得太满。有人喜欢直接start1 num65535其实没必要1024 以下的保留段一旦占用容易和其他服务打架留一点余量给系统运维更省心。改完动态端口范围后可以用netstat -ano观察新连接是否落到了新范围比如本地端口是否出现了 1025 以上的。5.2 系统侧缩短 TIME_WAIT 等待时间第二个参数是TcpTimedWaitDelay注册表位置HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters在该路径下新建或修改 DWORD 值TcpTimedWaitDelay设为30十进制单位秒MaxUserPort设为65534十进制微软官方文档对TcpTimedWaitDelay的建议一般是最低 30 秒我生产环境里也按 30~60 秒来设置实测稳定没有因为缩短等待时间出现过数据延迟报文串连的问题。改完注册表一般需要重启机器或者重启 TCP/IP 协议栈不建议直接重启协议栈会断掉本机所有网络连接稳妥做法是安排在重启窗口执行。这里补充一个容易混淆的点MaxUserPort是早期 Windows TCP/IP 参数netsh动态端口范围是更上层的配置入口。在较新的 Windows 版本上实际限制以 netsh 动态端口范围为准两者取更严格的那个生效。所以单独调MaxUserPort而不管 netsh可能没有效果正确做法是先扩 netsh 动态端口范围注册表里的等待时间调节作为辅助。网上很多老教程只提注册表移植到新系统上容易踩坑这点要留意。5.3 应用侧用连接池替代“每次新建”不管系统参数怎么调应用层最该做的还是降低建连频率。能复用就不要新建这是对端口池最根本的减负。我以一个 Java 服务最常使用的 Apache HttpClient 5 为例连接池的最小配置长这样PoolingHttpClientConnectionManager connectionManager PoolingHttpClientConnectionManagerBuilder.create() .setMaxConnTotal(200) // 整个连接池最大连接数 .setMaxConnPerRoute(100) // 到同一个目标主机的最大连接数 .build(); CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(connectionManager) .evictExpiredConnections() // 剔除过期连接 .evictIdleConnections(60, TimeUnit.SECONDS) // 60秒空闲连接回收 .build();如果你用的是 SpringRestTemplate默认的SimpleClientHttpRequestFactory是每次请求都新建连接必须替换掉Bean public RestTemplate restTemplate() { CloseableHttpClient client HttpClients.custom() .setConnectionManager(connectionManager) .build(); ClientHttpRequestFactory factory new HttpComponentsClientHttpRequestFactory(client); return new RestTemplate(factory); }如果你用的是OkHttp它内置连接池核心是配置maxIdleConnections和keepAliveDuration同样能做到连接复用。总之原则就一条让连接活着重复用而不是用完立刻拆掉。5.4 应用侧资源释放和超时必须规范连接池解决的是“高频新建”问题但代码里如果仍然有裸Socket或裸连接释放逻辑必须规范。最简单可靠的是 try-with-resourcestry (Socket socket new Socket()) { socket.connect(new InetSocketAddress(host, port), 3000); // 连接超时3秒 // 业务处理... } catch (IOException e) { // 异常处理 }如果项目里确实有手动管理连接的老代码请确保关闭动作放在 finally 里无论正常流程还是异常流程都要执行到Socket socket null; try { socket new Socket(); socket.connect(...); // ... } finally { if (socket ! null) { try { socket.close(); } catch (IOException ignored) {} } }还要给所有连接设置超时。connectTimeout如果不设连接请求可能长期卡在SYN_SENT端口一直被占着readTimeout不设对端不回应时会一直占着连接。这两个超时不仅关系到用户体验也是端口池能否及时回收的关键。5.5 两种手段的优先级安排处理时机不同侧重点也不同当前状态优先动作正在大面积报错服务受影响先扩大动态端口范围netsh必要时同时调低 TcpTimedWaitDelay 到 30 秒快速止血报错但服务还能扛系统参数调整 应用层连接池改造一起做缺一不可新项目上线前直接在代码里设计好连接池系统参数按高并发模型预留不要等上线后补救我实际遇到的情况是“正在大面积报错”当时先用 netsh 把端口池扩到 60000再顺手把TcpTimedWaitDelay调到 30 秒业务在几分钟内恢复。但后续仍然花了半天时间把所有调用点的连接池补齐把裸连接改成 try-with-resources才算真正把这个“已解决”钉死。6. 别把 Windows 问题带到 Linux跨平台部署的对照排查如果你的服务会部署到 Linux 服务器这个问题的表现形式不同但底层逻辑完全一致。对照着看以后切平台不会一头雾水。6.1 Linux 上的对应报错Linux 下动态端口池耗尽时Java 日志里通常不是No buffer space available而是java.net.BindException: Cannot assign requested address (Bind failed)有人看到BindException以为代码里绑定端口出了问题其实在客户端主动出站连接的场景下这个错误往往就是本地临时端口分配失败。同一个病Windows 和 Linux 换了件马甲而已。6.2 Linux 的排查与调整先看当前端口范围cat /proc/sys/net/ipv4/ip_local_port_range默认通常是32768 60999也就是只有约 28232 个端口。统计 TIME_WAITnetstat -ant | grep TIME_WAIT | wc -l调整参数临时生效sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_fin_timeout30 sysctl -w net.ipv4.tcp_tw_reuse1要永久生效写入/etc/sysctl.conf后执行sysctl -p。这里必须强调两个容易踩坑的点。第一tcp_tw_reuse只对主动发起连接的一方有效也就是说 Java 服务作为客户端时才有意义架设在服务器侧的监听连接开启tcp_tw_reuse可能会引入数据串扰但一般不会作为服务器调优手段。第二tcp_tw_recycle在新内核已经移除老内核开启它会对 NAT 环境下的连接造成严重干扰不要碰这个参数。6.3 容器化环境还多一层限制在 Docker/K8s 环境里排查时要先确认你看到的端口范围是宿主机命名空间的还是容器命名空间的。桥接网络模式下容器出网流量做 NAT 时宿主机的动态端口同样会被消耗。也就是说即使容器里的 Java 应用已经把连接池做得很好了宿主机端口池如果被大量并发短连接打爆一样会报类似错误。这种场景下的排查要从容器和宿主机两边看别只盯着 Java 进程本身。7. 从“已解决”到“不再犯”监控与日常体检说实话这个报错“解决一次”不难难的是“不再犯”。服务一扩容、调用链一调整、流量一增长老问题可能换个形式再回来。我现在会在收尾阶段把监控和日常巡检也一起做掉这里分享几个自己长期用的手段。7.1 给端口余量做一个简易看板Windows 上可以写个简单的 PowerShell 采样脚本定时把端口余量打到监控系统$total (netsh int ipv4 show dynamicport tcp | Select-String 端口数量).ToString().Split(:)[1].Trim() $timewait (netstat -ano | Select-String TIME_WAIT).Count $established (netstat -ano | Select-String ESTABLISHED).Count $used [int]$timewait [int]$established $left [int]$total - $used Write-Output $(Get-Date) TIME_WAIT$timewait ESTABLISHED$established 剩余端口估算$left用计划任务每 1~5 分钟跑一次都行。告警阈值我一般设置成剩余端口低于总端口 20% 且持续 5 分钟就发告警。这个指标比 CPU、内存更能提前反映“连接风暴”。7.2 应用层连接池的可观测性系统层面看端口应用层还要看连接池。连接池有没有耗尽、活跃连接数有没有异常上涨、等待获取连接的线程数是多少这些都要能实时看到。Apache HttpClient 的PoolStats、OkHttp 的连接池指标、数据库连接池的 active/idle 状态都可以接入日志或指标系统。重点关注“每秒新建连接数”这是端口池告警的前置指标看到它蹭蹭上涨就该查代码了。7.3 上线前的连接压测新服务上线或者老服务要扩容建议先做一轮“连接压力验证”。用 JMeter 模拟调用方视角对目标 Java 服务发请求中间件看板里同时观察目标服务的出站连接数。如果在压测过程中 TIME_WAIT 曲线一路上扬、端口余量跌破 20%那说明代码里的连接复用还有问题或者连接池配小了在正式流量进来之前就得修掉。这个环节便宜又有效比上线后半夜被叫醒强太多。7.4 最容易复发的三个原因见过不少团队调完参数后隔几个月又踩同一个坑根据我的观察复发原因基本就这三种新增调用链没走连接池业务代码加了个新的下游调用开发图省事直接 new 连接老连接池形同虚设。扩容后连接数成倍放大应用从 2 台扩到 10 台每台都在建连接系统参数没跟着同步评估。机器镜像或重建后参数丢失有些云主机镜像会重置 netsh 动态端口配置机器重新拉起后默认值又回来了需要把参数固化到初始化脚本或运维平台里。这三点都不难防难的是把它写进流程里。我的做法是把“动态端口范围配置”加进服务器初始化脚本把“连接池是否启用”加进代码评审清单让成本和门槛都变得极低。这次把No buffer space available完整解决之后我把“先查动态端口、再看连接池配置”固化成了 Java 网络报错的默认排查顺序。说句大实话这个错十次里有九次不是内存问题而是连接洪峰挤爆了临时端口系统参数调一调很快但真要稳还是得让代码里的连接学会复用。希望这套从原理到实操的完整链条能帮你少掉几根头发。
返回列表