我以为宽带坏了,结果是 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:104227TCP/IP 无法建立传出连接,本地端口已用尽
19:53:204227TCP/IP 无法建立传出连接,本地端口已用尽
16:16:124227TCP/IP 无法建立传出连接,本地端口已用尽
11:00:584266UDP 全局端口空间全部用尽

从早上十一点开始报,一天三次。

也就是说,临时端口耗尽是 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 HTTPS2.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