题记与说明
- 日期:2026-11-05
- 形式:主题研究(先检索 5 个来源并在线精读原文;检索阶段另参考了 Wikipedia TCP congestion control、RHEL 9 官方调优文档、LogicMonitor/OneUptime 故障排查指南等搜索摘要作为线索与补充,未逐字精读,已在 §6 标注)
- 本次阅读的来源(全部在线精读):
- 4.1 TCP 三次握手与四次挥手面试题 — 小林coding《图解网络》(图解类,覆盖握手/挥手/队列/SYN 攻击/TIME_WAIT/保活)
- TCP Congestion Control — 《Computer Networks: A Systems Approach》§6.3(免费开放教科书,覆盖 cwnd/AIMD/slow start/fast retransmit/CUBIC)
- tcp(7) Linux man page(内核权威文档,覆盖缓冲区自调优与全部关键 sysctl 语义)
- Linux Tuning — ESnet Fasterdata(美国能源部科研网 ESnet 的实战调优手册,覆盖 BDP 与推荐配置)
- TCP SYN Queue and Accept Queue Overflow Explained — Alibaba Cloud(带内核实验数据的工程帖,覆盖两类队列溢出与诊断)
0. TL;DR
TCP 入门调优的知识骨架可以压缩成三句:
- 握手的设计目标不是"确认双方在线",而是"拦截历史连接 + 同步双向 ISN"的最小充分次数——RFC 793 的原文把"防止旧副本造成混乱"列为三次握手的首要原因;两次握手的根本缺陷是"服务端没有中间状态给客户端来阻止历史连接"。
- 传输效率由 cwnd 与 rwnd 中较小者决定,AIMD(加性增/乘性减)是收敛与公平的代价设计:每 RTT 线性 +1 MSS、超时即减半,形成经典锯齿;slow start 实为指数增;CUBIC 把增长从"按 ACK 计数"改为"按上次丢包后的时间的三次函数",成为 Linux 默认。
- 调优的第一定律是带宽时延积(BDP):10 Gbps × 100 ms ≈ 120 MB 在途数据,默认缓冲区(tcp_rmem max 通常仅约 4 MB)远不够;正确做法是开着内核自动调缓冲(tcp_moderate_rcvbuf=1),只调大 tcp_rmem/tcp_wmem 的 max 与 net.core.rmem_max/wmem_max 上限,而不是手动指定每个 socket 的缓冲区。
- 最常见的"玄学"线上故障(连接超时、CPU 内存全正常但请求悄悄失败)往往就藏在两个握手队列里:accept 队列满时内核默认静默丢弃 ACK,客户端却以为连接已建立——用
ss -lnt的 Recv-Q/Send-Q 和netstat -s的 listen 溢出计数器一测便知。
1. 核心问题
这组材料共同回答的问题是:如何让跑在不可靠 IP 之上的字节流既可靠又高效,并且当它表现异常时,工程师能按可观测信号把故障归位到具体环节? 拆成四个子问题:
- 握手为什么是三次? 三次握手到底在防什么、同步什么,多一次少一次各坏在哪。
- 拥塞控制如何同时追求效率与公平? 发送方明明不知道网络里有没有拥堵,凭什么能逼近瓶颈带宽而不压垮网络?
- 缓冲区该多大? 接收/发送缓冲区与"管道容积"(BDP)的定量关系;Linux 自动调优机制下,管理员真正该动的是哪几个旋钮。
- 常见故障如何分类与定位? 握手失败、连接被重置、队列溢出、TIME_WAIT/CLOSE_WAIT 堆积、死亡连接——各自的症状、计数器与根因。
2. 主要主张
2.1 三次握手:防历史连接是第一因,ISN 同步是第二因
三次握手每步状态变化:客户端发随机 ISN 的 SYN → SYN_SENT;服务端回 SYN+ACK(确认号为 client_isn+1)→ SYN_RCVD;客户端回 ACK → 双方 ESTABLISHED。两个关键细节:前两次握手不能携带数据,第三次可以;客户端 connect 在收到 SYN+ACK 即返回,而服务端 accept 要等第三次握手完成。
小林coding 明确指出"保证双方具有收发能力"只是片面回答,三大真正原因:
- 阻止历史连接(RFC 793 列出的首要原因)。原文引述:
"The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion."(RFC 793,转引自小林coding)
场景:网络中滞留的旧 SYN(如 seq=90)先于新 SYN(seq=100)到达,服务端回复确认号 91,客户端发现与期望(101)不符,回 RST 终止历史连接。两次握手的致命点:服务端在收到第一个 SYN 就进入 ESTABLISHED 并可能开始发数据,"服务端没有中间状态给客户端来阻止历史连接",会对僵尸历史连接白白发数据直到资源耗尽。
- 同步双方初始序列号(ISN)。序列号承担去重、按序重组、确认三项职责,双向都要各确认一次对方的 ISN;服务端的 SYN 和 ACK 可以合并为一次发送,所以"四次"退化为"三次"。
- 避免资源浪费。两次握手下"服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接",重复阻塞 SYN 会堆积冗余连接。
ISN 为什么随机且每次不同:主要为了防止历史报文被下一个相同四元组的连接错收,其次是防伪造。RFC 793 的算法是 ISN = M + F(四元组),M 为每 4 微秒加 1 的计数器(约 4.55 小时循环一圈),F 为基于四元组的 Hash。
2.2 拥塞控制:自时钟 + AIMD 的锯齿,CUBIC 换了增长函数
《Systems Approach》§6.3 的理论骨架:
- 有效发送窗口受双重约束:
MaxWindow = MIN(CongestionWindow, AdvertisedWindow),即拥塞窗口与接收窗口取小。 - AIMD(加性增、乘性减):超时视为拥塞信号,cwnd 减半(16→8→4→2→1 个报文);每个无错的 RTT 内 cwnd 增加约 1 个 MSS,实现上每个 ACK 到达时
Increment = MSS × (MSS/CongestionWindow)。原文强调这种不对称是刻意设计:
"the source is willing to reduce its congestion window at a much faster rate than it is willing to increase"
并给出稳定性判断:"AIMD is a necessary condition for a congestion-control mechanism to be stable."
- Slow start 并不"慢":cwnd 从 1 个报文起,每 RTT 翻倍(1→2→4→…),原文:"Slow start effectively increases the congestion window exponentially, rather than linearly." "慢"只是相对早期 TCP 一上来就灌满整个通告窗口而言。到达阈值(文中称 CongestionThreshold,即通常的 ssthresh)后转入线性增。丢包时:阈值 = cwnd/2,cwnd 重置为 1 重新指数爬升。
- Fast retransmit / fast recovery:收到 3 个重复 ACK 即重传丢失报文,不必等粗粒度超时;fast recovery 让 cwnd 直接减半后继续线性增,跳过 slow start("effectively removes the slow start phase")。效果是消除约一半粗超时、吞吐提升约 20%;局限是窗口太小时凑不齐 3 个重复 ACK,且每窗口最多探测到约 3 个丢包。
- CUBIC(Linux 默认):窗口增长函数改为
CWND(t) = C×(t−K)³ + W_max(K = ∛(W_max(1−β)/C)),β = 0.7(标准 TCP 为 0.5)——丢包后先快速恢复到 W_max(凹段),停顿(平台段),再凸段向外探测。关键改进是按时间而非按 ACK 计数增长,长肥网络(高 RTT)里的流不再被短 RTT 流压制。
BBR 的定位(来自检索摘要与 ESnet 页侧写,未精读原文):Google 2016 年提出的算法,不再以丢包为拥塞信号,而是直接建模瓶颈带宽 × 往返时延,配合 fq 队列调度做 pacing。
2.3 缓冲区:BDP 定律 + "调上限而非调固定值"
- BDP(带宽时延积)定律:ESnet 给出定量例子——
"a 10Gbps flow across a 100ms network requires 120MB of buffering"
即缓冲区上限至少要等于带宽 × RTT,才能让管道持续满载。默认的 Linux TCP 缓冲区上限"still too small for 10G networks"。
- Linux 的正确调法是三层参数 + 保留自调优。tcp(7) 权威语义:
net.ipv4.tcp_rmem/tcp_wmem=[min, default, max]三元组(接收/发送各一组),控制内核自动调节的范围;tcp_moderate_rcvbuf(默认开,2.4.17+):接收侧自动调优,原文:"TCP performs receive buffer auto-tuning, attempting to automatically size the buffer (no greater than tcp_rmem[2]) to match the size required by the path for full throughput."——自动调优的天花板就是 tcp_rmem[2],这正是"调 max 而非调 default"的原因;net.core.rmem_max/wmem_max:封顶setsockopt(SO_RCVBUF/SO_SNDBUF)能设到的值;注意 TCP 实际分配 twice 你请求的值,且 setsockopt 必须在listen()/connect()之前调用。- 注意 tcp_rmem[2] 的内核默认大约
max(87380, min(4MB, tcp_mem[1]*PAGE_SIZE/128))——对高 BDP 场景严重不足。
- ESnet 推荐配置(10G 网卡、RTT ≤ 100 ms,面向并行流工具):
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 33554432
net.ipv4.tcp_wmem = 4096 65536 33554432
net.ipv4.tcp_mtu_probing = 1
net.core.default_qdisc = fq(更长 RTT/更高带宽按比例上调,100G 场景 rmem_max 顶到 2GB−1 的内核上限。)刻意让 per-socket 上限 32–64 MB,使并行流(如 Globus 用 4–8 条流)共享总管道而不吃光整机内存——"the maximum values are 2 to 4 times less than what would be required to support a single stream"。
- 配套项:拥塞控制用默认 cubic 即可("we no longer recommend setting congestion control to htcp");qdisc 选 fq 或 fq_codel(4.12+ 内核默认 fq_codel),BBR 团队推荐 fq;别关
tcp_timestamps/tcp_sack——ESnet "strongly disagree[s] with that recommendation for WAN performance";net.tcp_mem保持默认;< 1 Gbps 的家用场景照抄反而有害。
2.4 常见网络故障:按"握手失败的形态"分类定位
两个队列是故障重灾区(阿里云实验帖 + 小林coding 互证):
- 半连接队列(SYN 队列):收到 SYN、回了 SYN+ACK、还没等到 ACK 的连接在此(SYN_RCVD)。
- 全连接队列(accept 队列):三次握手完成、等待应用调用
accept()的连接在此(ESTABLISHED)。上限 = min(somaxconn, listen 的 backlog 参数)。 - accept 队列满时的默认行为是静默丢弃客户端的 ACK:服务端反复重传 SYN+ACK,客户端却以为连接已建立、发数据无应答——这是最迷惑人的一种"半开"状态。设
tcp_abort_on_overflow=1可改为立刻回 RST 快速失败。 - SYN 队列满:新 SYN 被丢,客户端只能重传 SYN。
SYN 攻击与防御(小林coding):伪造海量不同源 IP 的 SYN 占满半连接队列。四招:调大 netdev_max_backlog;同时调大 tcp_max_syn_backlog + listen backlog + somaxconn 三者(只调一个没用);开启 tcp_syncookies=1(队列满时算出 cookie 塞进 SYN+ACK 的序列号,合法 ACK 回来直接进 accept 队列,绕过半连接);把 tcp_synack_retries 从 5 降到 2 加速清队。
诊断命令与计数器(两文合计):
ss -lnt:LISTEN 套接字的 Recv-Q = accept 队列当前长度,Send-Q = accept 队列上限(非 LISTEN 套接字则分别是未读字节数/未确认字节数);netstat -s | grep -i listen:累计计数器 "…times the listen queue of a socket overflowed" 与 "…SYNs to LISTEN sockets dropped",持续增长即队列正在溢出;ss -ti:单连接的 cwnd、RTT、重传计数(来自检索摘要,本次未精读原文)。
重传与"死亡"感知的时间账:
- 握手期:客户端 SYN 重传由 tcp_syn_retries 控制(默认 5 次,超时翻倍,共约 63 秒放弃);服务端 SYN+ACK 由 tcp_synack_retries 控制;"ACK 报文是不会有重传的"。
- 建立后:tcp_retries1(默认 3,之后先重新查路由)、tcp_retries2(默认 15,约 13–30 分钟后放弃连接——tcp(7) 注明 RFC 1122 要求的 100 秒下限"generally considered too short")。
- 保活:需显式
SO_KEEPALIVE开启,Linux 默认 tcp_keepalive_time=7200s + intvl=75s × 9 次探测——"最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接",所以应用层普遍用自己的心跳。 - 挥手堆积的两种形态含义完全不同:主动关闭方大量 TIME_WAIT 多是 HTTP 未用长连接/keepalive 超时/keepalive_requests 打满(nginx 默认 100),可用 tcp_tw_reuse + tcp_timestamps(仅客户端侧)缓解;服务端大量 CLOSE_WAIT 则是程序 bug——收到对端 FIN 后没调 close(socket 没注册进 epoll、没 accept、或事件处理里漏了 close/死锁)。
- 进程崩溃时内核会替它发第一次 FIN,连接可正常四次挥手关闭;而主机宕机/拔网线对端无法感知,才需要保活。
3. 方法
- 来源组合覆盖"图解入门 → 理论骨架 → 权威语义 → 实战配方 → 实验证据"五层:图解类(小林coding)负责把状态机画成可记的图并给出面试视角的"为什么";开放教科书(Systems Approach)负责把 AIMD/slow start 的数学讲透;内核手册(tcp(7))负责参数的准确语义与默认值;ESnet 负责大规模数据传输场景的实操配置;阿里云帖负责用内核实验把队列行为做实。
- 交叉验证同一参数在不同来源的说法,主动暴露矛盾点并记录(见 §2.4 与 §4.3 的三处不一致:syncookies 的推荐度、syncookies 是否完全防丢、tcp_abort_on_overflow 该不该开)。
- 引用原文关键句而非转述,中英文来源都保留原句以便复核。
- 未做本地复现实验:本机为 Windows(Git Bash),无 Linux sysctl/iproute2 环境,队列溢出实验无法本机重跑,仅引用来源的实验数据——这是本篇与"含本地实证"笔记的差别。
4. 数据与证据
4.1 关键默认值速查(tcp(7) 与小林coding 交叉)
| 参数 | 默认值 | 含义 |
|---|---|---|
| tcp_rmem | [页大小, 87380, ≈4MB 内核公式] | 接收缓冲 [min, default, max],max 是自调优天花板 |
| tcp_wmem | [页大小, 16KB, ≈4MB 内核公式] | 发送缓冲三元组 |
| tcp_moderate_rcvbuf | 1(开) | 接收侧自动调优总开关 |
| tcp_syncookies | 1 | 半连接队列溢出时发 cookie |
| tcp_max_syn_backlog | 256(≥128MB 内存则 1024) | 半连接队列相关上限 |
| tcp_abort_on_overflow | 0(关) | accept 队列满时是否回 RST |
| tcp_fin_timeout | 60 s | FIN_WAIT_2 的超时(close 关闭时) |
| tcp_keepalive_time/intvl/probes | 7200 / 75 / 9 | 保活探测节奏(默认约 2h11m15s 才判死) |
| tcp_retries1 / tcp_retries2 | 3 / 15 | 建立后重传:查路由阈值 / 放弃连接(约 13–30 min) |
| tcp_syn_retries / tcp_synack_retries | 5 / 5 | 握手 SYN / SYN+ACK 重传次数(SYN 全程约 63 s) |
4.2 阿里云队列溢出实验数据(kernel 4.19.91)
| 实验 | 配置 | SYN_RECV 上限观测 |
|---|---|---|
| 1 | syncookies=0, somaxconn=1024, max_syn_backlog=128 | 96(按公式预测触发丢包) |
| 2 | syncookies=0, somaxconn=128, max_syn_backlog=512 | 128(= accept 队列上限) |
| 3a | syncookies=1, somaxconn=128, max_syn_backlog=512 | 128(syncookies 开着仍丢包) |
| 3b | syncookies=1, somaxconn=5 | 5 |
| 4 | syncookies=1, somaxconn=256, max_syn_backlog=128 | 256 |
公式(半连接队列上限):roundup_pow_of_two(min(min(somaxconn, backlog), tcp_max_syn_backlog) + 1) 对应 2^max(3, log2(...))。例:backlog=128、tcp_max_syn_backlog=1024 → 半连接上限 256。
4.3 三处来源间矛盾(精读的价值所在)
- tcp_syncookies 的推荐度:小林coding 推荐设 1(仅队列满时启用);tcp(7) 却警告 syncookies "should be used as a last resort, if at all",称其为协议违规、可能与 TCP 扩展冲突。折中读法:对公开互联网抗 SYN flood,=1 是标准做法;对内网/可信环境,按 tcp(7) 保持谨慎。
- syncookies 能否完全防丢:流传说法是 syncookies=1 就不丢 SYN;阿里云实验证明只要半连接长度 > accept 队列上限照样触发丢 SYN(实验 3a/3b)——accept 队列满时 syncookies 救不了,必须把 somaxconn/backlog 一起调大。
- tcp_abort_on_overflow=1 该不该开:小林coding 与阿里云都把它当作快速失败的修复手段;tcp(7) 则警告 "Enabling this option can harm the clients of your server."。合理定位:它是诊断期快速暴露溢出的开关与过载时的明确拒绝,而不是长期静默丢弃的替代品——根因仍是 accept 消费太慢。
4.4 真实事故案例(阿里云帖)
大促值班期间,apiserver 集群 CPU/内存利用率仅 40%,客户端却大面积连接超时。根因即连接队列满:somaxconn=128、tcp_max_syn_backlog=512、syncookies=1、abort_on_overflow=0。修复:调大 somaxconn 并临时开 tcp_abort_on_overflow=1 快速失败。作者的核心观察:这类溢出"easily ignored but fatal"——服务器侧一切监控正常,失败只发生在客户端,短连接 + 高频上游调用最易中招。
5. 结论
- 三次握手的最小性有严格依据:它是"防历史连接 + 双向确认 ISN + 不浪费资源"三目标下的最小报文数;ISN 随机化(M + Hash(四元组))与第三握手的"中间状态"共同构成防线。
- 拥塞控制是分布式系统里"无信令、仅靠观测"的经典设计:ACK 自时钟驱动,AIMD 用指数退避+线性恢复逼近效率与公平的平衡点;从 Reno 到 CUBIC 的本质变化只是"增长函数从按 ACK 计数改为按时间的三次函数";BBR 进一步换掉了拥塞信号本身(丢包 → 带宽×RTT 模型)。
- 缓冲区调优的实务要点收敛为一条:保持自动调优开启,把
tcp_rmem[2]/tcp_wmem[2]和net.core.rmem_max/wmem_max按 BDP 与场景(单流 or 并行流)调大,配 fq/fq_codel qdisc;默认值对高带宽长 RTT 链路差 1–2 个数量级,但对 <1 Gbps 用户反而要反向调小。 - 网络故障排查可以完全可观测化:握手失败的四种形态(无响应重传 SYN / 立即 RST / 建立后被 RST / 半开不对称)+
ss -lnt/netstat -s/ss -ti三个命令 + ListenOverflows/ListenDrops/SYN cookies sent 等计数器,足以把大多数"连接慢/连不上/悄悄失败"定位到队列、防火墙或应用 accept 缺位。 - TIME_WAIT 与 CLOSE_WAIT 是两种不同性质的信号:前者是协议设计的一部分(《UNIX 网络编程》:"TIME_WAIT 是我们的朋友"),堆积到影响容量时调应用(长连接、连接池);后者几乎一定是代码缺陷,别动内核参数,去查 epoll 注册与 close 调用。
6. 局限
来源各自的局限(部分来源自认):
- 小林coding《图解网络》面向面试,默认内核较新但个别参数行为随版本变动(如 somaxconn 上限、backlog 语义),且"为什么三次"的 RFC 依据原文比博文更谨慎(RFC 793 只把防历史副本称为 principle reason,未列"三次才够确认能力"这类推导)。
- 《Systems Approach》§6.3 未使用 Tahoe/Reno 命名、未讨论 BBR 与 bufferbloat 的 BDP 缓冲 sizing 规则(其自述排队论放在 §6.2);引用值如 fast retransmit "20% 吞吐提升"是早期网络的实验结论。
- tcp(7) 的默认值随内核版本浮动(手册自身标注了 2.2/2.4 时代的差异),照抄默认值前应
sysctl -a实测当前内核。 - ESnet 页面明确面向 1 Gbps+ 科研网数据传输主机,并自承其配置对 <1 Gbps 主机会降低性能;其上限按并行流而非单流设计。
- 阿里云帖基于 kernel 4.19.91 单机实验,评论区证实不同发行版/内核(CentOS 7 3.10 vs RedHat 4.18/Ubuntu 5.4)在"半开 vs 半闭"场景的行为不一致,结论不可跨版本照搬。
本篇的局限:
- BBR 只有侧写(ESnet 提及 + 检索摘要),未精读 BBR 论文或 Google 原帖;维基百科拥塞控制页、RHEL 9 文档、OneUptime/LogicMonitor 排查指南仅停留在检索摘要层,未逐字精读。
- 未做本地实验:Windows 环境无法复现 Linux sysctl/ss/netstat 验证;文中所有实验数字均转引自来源。
- 未覆盖 TLS 握手故障、DNS、NAT 重映射、ECN、MTU 黑洞探测细节、QUIC 对比——这些属于"TCP 之外"的常见故障域。
- 图解类资料的价值(ASCII/示意图形)在纯文字笔记中只能以状态名与数据流文字化保留,原页面的图形需点击链接查看。
7. 对我的启发
- 调优先算 BDP,再动旋钮:任何"网络慢"的问题先问两个数——带宽和 RTT。10 Gbps×100 ms ≈ 120 MB 的管道对 4 MB 默认上限,问题不是 TCP 慢而是管道从来没满过。日常服务(内网、<1 Gbps、低 RTT)则几乎永远不需要动这些参数——ESnet 的配置是反例警示,不是通用模板。
- "调上限而非调固定值"是一条通用系统调优原则:Linux 自动调缓冲的思路(给范围、给天花板,让运行时自适应)优于写死参数;
tcp_moderate_rcvbuf的设计(自调优 + max 天花板)值得移植到其他子系统(线程池、连接池、缓存)的调优心智。 - 故障排查要有"信号 → 假设"的映射表:这次最值钱的产出是那张形态表——SYN 无响应(丢包/防火墙/队列满)、立即 RST(无监听)、RST after established(应用崩溃/空闲超时/有状态防火墙)、半开不对称(accept 队列满 + abort_on_overflow=0)。以后遇到"连不上"先对号入座,再用
netstat -s的增量计数器证实,而不是先重启。 - 监控盲区在"两侧都正常"的路径上:那起 40% 利用率却大面积超时的事故提醒——服务端 CPU/内存/QPS 监控永远看不见"队列溢出导致的静默丢弃",必须把
ListenOverflows/ListenDrops纳入告警,或者干脆开 tcp_abort_on_overflow=1 把静默失败变成显式失败。 - 写作参考:这批材料示范了把协议讲透的两种路径——图解类用"状态机 + 场景推演"(如果只有两次会怎样),教科书用"不变量 + 数学"(AIMD 为何收敛);我若写网络类博客,可以在两者之间取一页纸的 BDP 计算作为钩子。
- 可立即落地的 checklist:新服务上线前——
ulimit -n/文件描述符三级限额、somaxconn 与 listen backlog 是否匹配、长连接 keepalive_requests 是否够、有没有应用层心跳(别指望 2h11m15s 的内核保活)、netstat -s基线留档。
*本笔记由主题精读流程生成(2026-11-05);引文均来自当日在线抓取的原文内容,经 WebFetch 转述保留,个别措辞请以链接原页面为准。*