我以为宽带坏了,结果是 13977 个端口不够用
2026年10月1日 · 2207 字
昨晚这台机器几乎没法上网。下载慢,GitHub 要愣好几秒,有些页面干脆打不开,转几圈就没了。
我一度准备打运营商电话。后来发现,问题从头到尾都不在宽带上。
先排除那些最容易怀疑的东西
按习惯,先把最常见的几个嫌疑对象过一遍:
网关 192.168.3.1 : 0% 丢包,<1ms
223.5.5.5(阿里DNS) : 21ms,0% 丢包
1.1.1.1(Cloudflare): 92ms,0% 丢包
网卡 : Realtek 2.5GbE,链路 1 Gbps
MTU 1500,PMTU 探测无黑洞
网关不丢包,DNS 是通的,网卡也没掉速。这几项都正常,说明线路本身没问题。
然后我测了一次下载,看到两个数字摆在一起,怎么看怎么别扭:
| 指标 | 数值 |
|---|---|
| 实际下载速度 | 65 KB/s |
| 链路可用带宽 | 163 Mbps(约 20 MB/s) |
带宽空着九成九,下载速度却退回拨号时代。
让我改方向的不是这个比例
按理说看到「带宽闲置 + 下载慢」,接下来该去查限速、QoS、路由。我确实也往那个方向想了一会儿,直到注意到另一件事。
阿里、清华、中科大,三个互不相干的镜像站,都在 5.1 到 5.6 秒之间断掉。
阿里巴巴慢,我可以理解成它在限速;清华慢,也能找到理由。但两个毫无关系的站点,卡在几乎同一时刻,误差不到半秒,这就没法用「对方的服务器有问题」解释了。
GitHub 的对比更直接:同一个页面,快的时候 2.8 秒,慢的时候转满 20 秒超时。而我用 TcpClient 单独测 TCP 握手,21 毫秒。
握手 21 毫秒,页面却要 2.8 秒到 20 秒。中间那段八竿子打不着的时间,是等出来的。
5 秒、21 秒、20 秒,这几个数都太整齐了。它们不是网络抖动的样子,而是协议里写死的等待时长。具体说就是 TCP 的 SYN 重传超时——连接压根没建起来,系统在按部就班地重试,跟对面服务器状态无关。
既然连接建不起来,问题多半在本地。
端口用光了
Windows 的临时端口是靠一段动态范围分配的。先看范围有多大:
netsh int ipv4 show dynamicport tcp
Start Port : 1024
Number of Ports : 13977 # 即 1024-15000
一万三千个。听起来不少。再数实际占了多少:
动态端口容量: 13977 个
已占用不同端口: 13993 个
剩余可用: -16 ← 负数
TIME_WAIT 连接: 31875 个,占用 13710 个端口(98%)
13993 个端口被占,池子只有 13977 个。
每发起一条 TCP 连接都要先领一个临时端口。领不到,连接就建不起来,然后退回重试。这正好能解释前面那个整齐的 5 秒,也解释了为什么带宽闲着:数据传不动,不是因为管子细,是因为压根没接上。
带宽利用率查不出这个问题,它只反映传得快不快,不反映能不能连上。
这件事 Windows 早就说过了
接下来这步让我有点不想提。
Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='Tcpip'} -MaxEvents 20
| 时间 | 事件 ID | 含义 |
|---|---|---|
| 21:14:10 | 4227 | TCP/IP 无法建立传出连接,本地端口已用尽 |
| 19:53:20 | 4227 | TCP/IP 无法建立传出连接,本地端口已用尽 |
| 16:16:12 | 4227 | TCP/IP 无法建立传出连接,本地端口已用尽 |
| 11:00:58 | 4266 | UDP 全局端口空间全部用尽 |
从早上十一点开始报,一天三次。
也就是说,临时端口耗尽是 Windows 自带报警的,日志里写得清清楚楚。我用了这么多年,从来没想过去翻 Tcpip 这个来源。
还有一行值得单独说:11 点的 4266,UDP 也满了。TCP 和 UDP 是两套独立的端口空间,两边同时见底,说明这不是某个程序开多了,而是整台机器在很高频率地反复建连、断连。
查占用大户
Get-NetTCPConnection | Group-Object OwningProcess |
ForEach-Object { [pscustomobject]@{
Proc=(Get-Process -Id $_.Name -ErrorAction SilentlyContinue).ProcessName
Count=$_.Count } } | Sort-Object Count -Descending
排第一的是 Steam,正在下游戏,几十个并发连接。这个没什么好说的,下载本来就该吃连接。
第二是 verge-mihomo(我用的 Clash Verge TUN 模式),652 个 TCP 连接里,有 43 个卡在 SYN_SENT 状态。它在给一批电信 IPv6 地址做节点健康检查,对面不响应,它就一直挂着重试。
再加上 47 个 msedge 进程和几个同步工具,账就平了。
一个说明了问题的旧配置
真正让我坐直的是注册表里的这一行:
MaxUserPort : 15000
默认值是 5000,这个被改成了 15000,而且明显是手动改的。
改它的人大概是我自己,时间很久了,我完全不记得。但从这个值能反推出当时发生过什么:以前的端口上限只有 5000,撑不住峰值,于是有人把它抬高到 15000,报警消失了,这事就算过去了。
问题在于,抬上限只是让池子晚点满。根因一点没碰。从 5000 撑到 15000,最后还是满了,只不过这次撑得久一点。
处理
一、把池子扩大
# 管理员权限
netsh int ipv4 set dynamicport tcp start=10000 num=55535
netsh int ipv4 set dynamicport udp start=10000 num=55535
范围从 1024-15000(13977 个)扩到 10000-65535(55535 个),大约是原来的四倍。
二、让端口回收快一点
这一步才是关键。TIME_WAIT 默认保留 240 秒,也就是连接关闭之后,端口还得再占四分钟才能给别人用。
端口周转这么慢,池子扩大四倍也只是延缓。那天占着 31875 个 TIME_WAIT,正常吗?在这个回收速度下,非常正常。
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' `
-Name MaxUserPort -Value 65534 -Type DWord
Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' `
-Name TcpTimedWaitDelay -Value 30 -Type DWord
240 秒压到 30 秒,端口周转快八倍。改完重启才生效。
两件事得一起做。只扩池子,高位时段照样见底;只调回收时间,峰值仍然可能不够。我这次是一起改的。
结果
| 测试项 | 修复前 | 修复后 |
|---|---|---|
| 清华镜像下载 | 65 KB/s(5.5s 卡死) | 20.32 MB/s |
| GitHub HTTPS | 2.8s ~ 20s 超时 | 279–401ms 稳定 |
| 代理路径下载 | 0 B/s 完全卡死 | 130 KB/s 正常 |
| 20 并发连接 | — | 20/20 成功,67ms |
| 剩余可用端口 | -16(透支) | 32961(59.4% 空闲) |
清华那行差了 320 倍,但真正解决我问题的是 GitHub 那行:从时快时慢变成稳定在 300ms 上下。
间歇性的慢比稳定的慢更难忍,因为它会让你反复怀疑是不是别的地方出了问题。
顺便记一个反例,省得以后误判:阿里云的镜像源,修复之后依然只有 69~76 KB/s,而同一次测清华有 20 MB/s。所以那是它自家在限速,跟我这边没关系。我一开始也怀疑过是本地问题,白查了一轮。
复盘
这次绕远路,是因为我一开始的提问方式就不对。
「网速慢」是个笼统的说法。它底下至少是两件事:连接能不能建立,和数据能传多快。带宽、限速、QoS 这些工具全都在回答第二个问题。我这次卡在第一个,用查第二个问题的工具去查,自然什么都查不出来。
所以现在我会先看一眼失败的时间点:如果几个不相关的站点都在固定的秒数上失败,那就不是网络质量的问题,而是连接压根没建成。五秒、二十一秒这类整数,基本都是重传超时。
至于端口耗尽,其实有更快的路径。Windows 的 Tcpip 日志会直接报 4227,不用像我这样从带宽一路推过来。
剩下没解决的
- 路由器
192.168.3.1的 DNS 不响应查询。现在所有解析都由 Mihomo 的 fake-IP 接管,意味着代理一旦出问题,直连域名的解析会一起挂掉。 - Mihomo 那 43 个 SYN_SENT 还在持续占用端口。得去把节点健康检查的间隔调大。
- 我存了 79 个 WiFi 配置,里面有大量酒店、机场的明文热点。同名热点伪造的风险不小,需要清理。
附录:本次用到的命令
# 端口范围与占用
netsh int ipv4 show dynamicport tcp
Get-NetTCPConnection -State TimeWait | Measure-Object
# TCP 状态统计
Get-NetTCPConnection | Group-Object State | Select-Object Name,Count
# 端口耗尽事件(关键!)
Get-WinEvent -FilterHashtable @{LogName='System';ProviderName='Tcpip'} -MaxEvents 20 |
Select-Object TimeCreated,Id,LevelDisplayName
# 谁在占用端口
Get-NetTCPConnection | Group-Object OwningProcess |
ForEach-Object { [pscustomobject]@{
Proc=(Get-Process -Id $_.Name -ErrorAction SilentlyContinue).ProcessName
Count=$_.Count } } | Sort-Object Count -Descending
# 链路真实吞吐(判断是否真被限速)
Get-NetAdapterStatistics