后端工程师一生总要遇到三次 TCP 疑案:第一次是大促高峰「Cannot assign requested address」,临时端口耗尽;第二次是接口偶发多出 40ms 却查不到代码原因;第三次是跨机房大文件传输怎么调都跑不满带宽。三桩悬案的卷宗都指向同一页:TCP 状态机与拥塞控制的实现细节。本篇把这份卷宗摊开——TIME_WAIT 为什么存在又如何治理、Nagle 与 delayed ACK 怎么互坑、BBR 凭什么把 Google 的广域网吞吐抬了一个数量级。
一、两种关闭的不对称:TIME_WAIT 是谁的债
TCP 的四次挥手有一个关键的不对称:谁主动关闭,谁欠 TIME_WAIT。主动关闭方发出最后的 ACK 后进入 TIME_WAIT,停留 2×MSL(报文最大生存时间的两倍);被动关闭方走 LAST_ACK 直接归 CLOSED。RFC 9293 给出两个理由:其一,最后的 ACK 若丢失,对方重发的 FIN 还要有人应答——只有 TIME_WAIT 状态的自己能干这事;其二,等旧连接的迟到重复报文在网络里自然消亡,免得污染复用同一四元组的新连接。
Linux 把这个等待硬编码为 60 秒(include/net/tcp.h 里的 TCP_TIMEWAIT_LEN),没有 sysctl 可调——60 秒是内核对 2MSL 的工程近似。顺带回看三次握手本身:它让双方各自确认「我发的你能收、你发的我能收」的双向可达,同时协商窗口缩放与 MSS 等选项;初始序列号(ISN)随机化则防的是旧报文注入与连接预测攻击——协议的每一步都有防的是「上一场战争的武器」的味道。这里有个流传极广的混淆:tcp_fin_timeout(默认也是 60)控制的是孤儿连接在 FIN_WAIT_2 的停留时长,与 TIME_WAIT 无关,网上无数「调小 tcp_fin_timeout 治 TIME_WAIT」的教程都在治别的病。因为主动关闭方才背债,而发起连接的一方几乎总是主动关闭(客户端调完接口就关),所以端口耗尽的锅几乎总是客户端或反向代理侧的——服务端看到的是连接被别人关。
graph LR
subgraph 主动关闭方
A[ESTABLISHED] -->|FIN| B[FIN_WAIT]
B --> C[TIME_WAIT 停留 60s] --> D[CLOSED]
end
subgraph 被动关闭方
E[ESTABLISHED] -->|收到 FIN| F[CLOSE_WAIT]
F -->|发 FIN| G[LAST_ACK] --> H[CLOSED]
end
二、TIME_WAIT 治理:先分清谁的锅
治理的第一步是确认债务方在谁家:客户端/代理侧 TIME_WAIT 堆积,治理手段按性价比排序。第一优先永远是连接池与长连接——Cloudflare 专门写过一文:把短连接改成长连接复用后,临时端口耗尽直接根治,这是唯一不付出协议代价的解法。第二是 tcp_tw_reuse:内核文档写明默认值 2(仅 loopback 生效),设 1 才全局生效,依赖时间戳选项,且有 tcp_tw_reuse_delay(默认 1000ms)限制复用节奏,文档还警告「没有专家建议别乱动」。第三是扩 ip_local_port_range 临时端口的可用区间。
反面教材必须点名:tcp_tw_recycle 曾是无数「调优文档」的宠儿,它激进复用 TIME_WAIT 却依赖对端时间戳单调——NAT 之后同一出口 IP 背后是千万台时钟各异的设备,PAWS 检查直接丢 SYN。eBay 的中央日志服务就这么挂着「间歇性连接超时」跑了很久才定位到它。内核在 4.12(2017)把这个参数彻底移除——与其说修复,不如说官方承认这条路是错的。记住判词:TIME_WAIT 本身在 Linux 上大多无害,它是协议正确性的必要成本,真正要治理的是「短连接泛滥」这个上游病因。
三、SYN Flood 与 syncookies:半连接队列的保卫战
三次握手的成本天然可以被滥用:攻击者发 SYN 不回 ACK,服务端半连接队列瞬间被占满,正常用户的 SYN 进不来。两个队列先分清:tcp_max_syn_backlog 管半连接队列(SYN_RECV 状态,每条约占 304 字节内核内存);net.core.somaxconn 是全连接 accept 队列的内核上限,应用实际可用的是它与 listen() backlog 的较小值——应用传 511、内核 4096,实际就是 511。队列溢出是「丢连接」类故障的经典根源,netstat -s 里的 overflow 计数是第一证据。
syncookies 是半连接队列溢出时的兜底:内核把连接信息编码进 SYN+ACK 的序列号里,不为攻击连接分配任何状态,等对方回 ACK 时凭序列号反解重建连接。代价同样写进内核文档:它「严重违反 TCP 协议」——连接协商的扩展选项(窗口缩放、时间戳等)装不进序列号,握手质量下降;文档明确警告不要用 syncookies 掩盖 backlog 配置错误。工程口径:它该是「攻击发生时的最后防线」,告警要响,事后要复盘为什么常规队列没扛住。还要认清分工:syncookies 只能止血于单机,规模化 SYN Flood 的真正防线在网络前端——CDN、Anycast 与流量清洗把攻击挡在源站之外,RFC 4987 汇总过包括 syncookies 在内的各类缓解手段,值得当防御设计的清单用。
四、Nagle 与 delayed ACK:一对经典的相互等待
Nagle 算法(1984 年 RFC 896)的初衷是治「小包病」:应用一次写 1 字节就发一个包,网络会被协议头淹没。它规定:上一个小包没被确认前,后续小数据攒着不发。delayed ACK 则是接收端的节约:ACK 不立刻发,等一等,看能不能捎在回程数据上。两个优化单独看都对,合起来就是灾难:发送端在等 ACK,接收端在等回程数据,谁也不动——Linux 典型的延迟 ACK 定时器约 40ms,Windows 常见 200ms,接口上「莫名的 40ms」十有八九是这对活锁。
Stuart Cheshire 2005 年的论文是这对交互的经典分析;AWS 首席工程师 Marc Brooker 的经验之谈更直白:"It's always TCP_NODELAY. Every damn time"——排查莫名延迟毛刺,第一个该怀疑的就是它。解法分两层:传输层开 TCP_NODELAY 关掉 Nagle(主流客户端驱动——MySQL Connector/J、redis-py 等——早已默认开启);应用层配合缓冲——把多个小请求攒成一个 buffer 一次 write,既免小包又不交 40ms 税。Redis 客户端 pipeline 的正确姿势正是这个:攒批在客户端做,而不是靠内核碰运气。
五、拥塞控制:从 CUBIC 的「丢包即拥塞」到 BBR 的「建模瓶颈」
CUBIC 自 Linux 2.6.19(2006 年底)起是内核默认,也是 Windows 与 macOS 的默认,2023 年由 RFC 9438 完成标准化。它的世界观:丢包即拥塞——填满缓冲、等丢包、窗口砍半、再线性爬升。在浅缓冲的局域网里高效,但在两个场合失灵:深缓冲(跨洋链路,缓冲膨胀把 RTT 推上天)与浅缓冲(无线链路,丢包多但并不拥塞)。
BBR(Google,2016 年 ACM Queue 论文)换了个世界观:不猜丢包,直接建模——持续估计瓶颈带宽(BtlBw)与最小 RTT(RTprop),按「带宽 × RTT」乘积控制发送速率,主动维持一个小队列。效果数字:Google B4 骨干网上单流吞吐从约 2Mbps 提到 91Mbps(相对 CUBIC 近一个数量级);GCP 官宣 YouTube 全球平均吞吐 +4%、部分地区 +14%。BBR 随 Linux 4.9(2016 年 12 月)合入主线;BBRv3 在 2023 年 IETF 117 会议上公布,已部署于 google.com 全部公网流量(重传率降约 12%),但据 2024 年的报道尚未合入主线内核——2026 年的今天,主线仍以 BBRv1 为主,Cloudflare 的 QUIC 团队还贡献过 CUBIC 拥塞窗口被钉在最小值的内核修复。启用评估很简单(net.ipv4.tcp_congestion_control=bbr 配 fq 队列),但收益必须按链路特征实测:大带宽长肥管道收益最大,机房内短链路可能无感。fq 队列不是可省的配件——BBR 依赖发包侧按估算速率平滑发注(pacing),fq 正是内核里实现这件事的队列纪律,缺了它 BBR 的模型等于算完不执行。
六、TCP Fast Open:一场没赢的标准化
TFO 想把三次握手省成零次:SYN 包里带上加密 cookie,下次连接 SYN 直接携带数据。Linux 客户端 3.6、服务端 3.7(2012 年底)就支持了,RFC 7413(2014)却停留在实验性状态。没普及的四个原因中,中间盒是主犯:NAT 与防火墙剥改 TCP 选项,TFO 连接半路夭折;浏览器又顾虑 cookie 的隐私争议相继弃用。一个耐人寻味的现状是:2026 年的测量研究显示 Cloudflare 约 99% 的边缘端点在服务端支持 TFO——「服务端都准备好了,中间的路却没修通」。它给所有人的教训是:协议特性能不能活,不只取决于设计,还取决于部署路径上每一个中间盒。
七、工程落地清单
- TIME_WAIT 堆积先找上游病因。上连接池与长连接是根治;
tw_reuse、扩端口区间只是缓解。 - 永远不要怀念
tcp_tw_recycle。4.12 已移除,NAT 环境下它是丢 SYN 的元凶。 - 监听队列三件套一起对齐。应用 backlog、
somaxconn、tcp_max_syn_backlog配平,netstat -s的 overflow/syncookie 计数进告警。 - 延迟毛刺先查 Nagle。交互式负载开
TCP_NODELAY,客户端攒批在应用层做。 - 跨洋与大带宽链路评估 BBR。配 fq 队列,灰度实测吞吐与 RTT;机房内链路预期收益有限。
- TFO 别当核心依赖。可以开启服务端支持,但容量与延迟设计按「握手要三次」做。
结语
TCP 深水区的三个关键词——TIME_WAIT、Nagle、拥塞窗口——共同点是把「协议正确性」与「工程性能」的张力摆上台面:TIME_WAIT 是正确性的必要成本,Nagle 是节约带宽的善意优化,CUBIC 是对丢包的合理推断,而它们的副作用都要由工程师来兜底。BBR 的启示则更进一步:当启发式走到尽头,换一个世界观(建模而非猜测)比调参数更有效。下一篇我们看另一场世界观革命:容器——用三个内核老特性,重构了整个部署世界。
参考资料
- RFC 9293: Transmission Control Protocol——TIME_WAIT 2MSL 与关闭不对称的协议级依据。
- IP Sysctl — kernel.org——tw_reuse 默认值、syncookies 代价与 TFO 位掩码的一手定义。
- Coping with the TCP TIME-WAIT state on busy Linux servers — Vincent Bernat——TIME_WAIT 治理组合拳的系统性梳理。
- How to stop running out of ephemeral ports — Cloudflare——长连接根治端口耗尽的一手案例。
- Troubleshooting a Connection Timeout Issue with tcp_tw_recycle — eBay——tw_recycle 与 NAT 互害的事故复盘。
- TCP Performance problems caused by Nagle's Algorithm and Delayed ACK — Stuart Cheshire——40ms/200ms 毛刺的经典机理分析。
- BBR: Congestion-Based Congestion Control — ACM Queue——BBR 原理与 Google 收益数字的一手论文。
- BBRv3: Algorithm Bug Fixes and Public Internet Deployment — IETF 117——BBRv3 部署状态的一手材料。
- The Sad Story of TCP Fast Open — Craig Andrews——TFO 未普及的四条原因。
读者留言
COMMENTS 暂无还没有留言,来说第一句?