第 16,216 次连接之后,它不干了

文章目录

上一篇讲连接池,结尾留了个问题:短连接除了慢,还会怎样?这篇给它一个戏剧性的答案。

我在本机做了个实验:对一个测试服务连续发起短连接------连上、发一个字节、客户端主动关闭------然后数着 netstat 里的 TIME_WAIT。第 16,216 次,连不上了。

没有报「端口不足」,没有 EADDRNOTAVAIL,而是一个朴素的 TimeoutError。这篇文章讲清楚这 16,216 是怎么数出来的、中间的每一秒发生了什么,以及为什么你的代码迟早会撞上同一面墙。

一、TIME_WAIT 是主动关闭方的「墓碑」

TCP 四次挥手,先发起关闭的那一方 要付代价:进入 TIME_WAIT,待够 2×MSL(MSL=Maximum Segment Lifetime,报文在网络里的最长寿命)才彻底消失。

为什么需要这个墓碑?两个理由,都和「网络上还有旧报文」有关:

  1. 最后的 ACK 可能丢。主动方收到对方的 FIN 后要回最后一个 ACK;这个 ACK 如果在路上丢了,对方会重传 FIN------主动方必须还在,才能补发 ACK,让对方正常关闭;
  2. 旧报文可能迟到 。如果立刻用同一个四元组(源 IP:端口 + 目标 IP:端口)建新连接,旧连接的延迟报文可能被新连接误收,造成数据错乱。等够 2×MSL,旧报文自然消亡。

TIME_WAIT 期间,这个四元组被占着。对一个固定目标(同一个 IP:端口)发起连接时,可变的是源端口------而源端口的池子是有限的。

macOS 实测参数:net.inet.ip.portrange = 49152--65535,共 16,384 个端口 ;net.inet.tcp.msl = 15,000 ms ,所以 TIME_WAIT ≈ 30 秒 。这两个数一除,答案就出来了:同一个目标,30 秒窗口内最多约 546 次短连接(16384 ÷ 30)。我实测的 16,216 次撞墙,和这个理论值只差一点点------差的是系统里其他进程占用的端口。

二、实测:一条短连接,一个 30 秒的坟

向本地测试服务连续发短连接(客户端主动 close),用 netstat 数服务端视角的 TIME_WAIT:

  • 50 次短连接 → 50 个 TIME_WAIT;
  • 累计 500 次 → 558 个;
  • 累计 1,000 次 → 1,591 个。

连接数与 TIME_WAIT 数基本 1:1 ------每一条短连接,都在对方的连接表里留一个 30 秒的坟。注意坟是「对方视角」也有份:服务端如果也主动 close(HTTP/1.0 服务端就是每次响应完就关),服务端侧同样堆 TIME_WAIT------只是服务端的 TIME_WAIT 共享同一个 listen 端口,占内存,不占 ephemeral 端口。

三、反常识:它不是慢慢消化的

我原以为 TIME_WAIT 会像队列一样慢慢消化------每秒消失几个,平滑下降。实测完全不是:

55 个 TIME_WAIT,原封不动挂了 32.6 秒 ,然后在第 33.6 秒集体归零 。另一组 1,643 个的实测:30.5 秒归零。

断崖,不是斜坡。原因是每条连接的 TIME_WAIT 是各自计时的(从「最后一次握手」起算 2×MSL),而这批连接是同一时刻建立的------所以它们也几乎同时到期 。这个形态对排障很重要:如果你在 31 秒时看 netstat,看到的不是「快消完了」,而是「一个都没少」。指标上看不到进展,但再过 3 秒全部清零------「看着没动」和「马上全好」可以同时是真的。

四、撞墙那一刻,报错不是教科书那样

第 16,216 次连接失败。错误是:

复制代码
TimeoutError: [Errno 60] Operation timed out

不是全网教程都在讲的 EADDRNOTAVAIL(Cannot assign requested address)。

这不是实验失败,是诚实的观察:端口耗尽时的错误形态取决于内核状态 。端口池见底后,connect() 拿不到源端口,SYN 发不出去,最终以连接超时的形式浮出水面------Linux 上更常见干净的 EADDRNOTAVAIL,但 macOS 这次给的是超时。排障时别只认那一种错,短连接洪水后的「连接超时」也要把端口耗尽列进怀疑名单。

顺带一个服务端侧的数字:撞墙时服务端 TIME_WAIT 计数 16,883------客户端端口池(16,384)和服务端连接表几乎同时见底。

五、解法:从根上不产生坟

方案一,连接池(正解,上一篇的主角)。 一条长连接复用 100 次请求:TIME_WAIT = 0 。100 次短连接:TIME_WAIT = 100 。同样 100 个请求,坟数 0 与 100 的差别。这就是 requests.Session / PoolManager 除了「省握手耗时」之外的第二个价值------省端口。

方案二,让服务端当主动关闭方要小心。 如果服务端先关(HTTP/1.0 模式每次响应完就关),坟堆在服务端:不占 ephemeral 端口,但每个坟占内核内存,几千个并发短连接就是几千个 TIME_WAIT 结构。HTTP keep-alive(服务端保持连接)把「每请求一次坟」变成「一条连接零座坟」。

方案三,系统参数是兜底不是首选。 Linux 上 net.ipv4.tcp_tw_reuse=1 允许内核把 TIME_WAIT 的四元组复用给出站新连接(靠 TCP 时间戳保证安全,只对作为客户端的出站连接有效);扩大 ip_local_port_range 能提高上限。但这些都是「把墙往后挪」------连接池是「不撞墙」。另外 SO_REUSEADDR 救不了这个场景:它管的是 bind() 重绑监听端口,不出站端口耗尽。

六、30 秒带走版

  1. TIME_WAIT 是主动关闭方 的代价,时长 2×MSL(macOS 实测约 30 秒,Linux 一般 60 秒);
  2. 短连接与 TIME_WAIT 1:1------50 条短连接留 50 个坟;
  3. TIME_WAIT 不是慢慢消化,是整批到点集体消失(实测 55 个挂 32.6 秒后断崖归零)------排障时「看着没动」可能意味着「马上全好」;
  4. 同一目标 30 秒窗口内的短连接上限 ≈ 端口数(macOS 16,384,实测第 16,216 次撞墙)------约 546 次/秒的可持续速率;
  5. 撞墙报错不一定是 EADDRNOTAVAIL,也可能是连接超时------取决于内核;
  6. 正解是连接池(100 请求 0 座坟);tcp_tw_reuse 是兜底,SO_REUSEADDR 不管这个场景。

相关推荐
我命由我123451 小时前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee
云和数据.ChenGuang1 小时前
LLM大模型对接本地Ollama部署模型
开发语言·人工智能·python·langchain
云和数据.ChenGuang2 小时前
光哥说Python 循环导入(Circular Import)深度解析:成因、排查与解决方案
开发语言·前端·人工智能·python·langchain
TomEval2 小时前
【测AI】第16篇:模型公平性与可解释性测试 — AI 的偏见与黑箱
人工智能·python·功能测试·ai·测试用例
田里的水稻2 小时前
FA_CNN卷积神经网络四---线性层及其拉平PyTorch
人工智能·pytorch·python·深度学习·神经网络·cnn
钱栈up2 小时前
GitHub热榜上手|Claude插件:4.3万星官方目录
python·开源·github
青春不朽5122 小时前
Python 的 list append 为什么是均摊 O(1)?拆开过度分配那张表,看容量翻倍背后的暗账
java·python·list
小羊没烦恼!3 小时前
.NET 11 Runtime Async 详解
java·运维·开发语言·javascript·python
染指11103 小时前
152.Agent-LangGraph-Command的参数
人工智能·python·langchain·agents