读书笔记 · 约 12,857 字 · 预计推送 2 部分

TCP 基础与调优入门:三次握手、拥塞控制、缓冲区与常见网络故障(主题精读) 未开始

目录

题记与说明


0. TL;DR

TCP 入门调优的知识骨架可以压缩成三句:

  1. 握手的设计目标不是"确认双方在线",而是"拦截历史连接 + 同步双向 ISN"的最小充分次数——RFC 793 的原文把"防止旧副本造成混乱"列为三次握手的首要原因;两次握手的根本缺陷是"服务端没有中间状态给客户端来阻止历史连接"。
  2. 传输效率由 cwnd 与 rwnd 中较小者决定,AIMD(加性增/乘性减)是收敛与公平的代价设计:每 RTT 线性 +1 MSS、超时即减半,形成经典锯齿;slow start 实为指数增;CUBIC 把增长从"按 ACK 计数"改为"按上次丢包后的时间的三次函数",成为 Linux 默认。
  3. 调优的第一定律是带宽时延积(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 的缓冲区。
  4. 最常见的"玄学"线上故障(连接超时、CPU 内存全正常但请求悄悄失败)往往就藏在两个握手队列里:accept 队列满时内核默认静默丢弃 ACK,客户端却以为连接已建立——用 ss -lnt 的 Recv-Q/Send-Q 和 netstat -s 的 listen 溢出计数器一测便知。

1. 核心问题

这组材料共同回答的问题是:如何让跑在不可靠 IP 之上的字节流既可靠又高效,并且当它表现异常时,工程师能按可观测信号把故障归位到具体环节? 拆成四个子问题:

  1. 握手为什么是三次? 三次握手到底在防什么、同步什么,多一次少一次各坏在哪。
  2. 拥塞控制如何同时追求效率与公平? 发送方明明不知道网络里有没有拥堵,凭什么能逼近瓶颈带宽而不压垮网络?
  3. 缓冲区该多大? 接收/发送缓冲区与"管道容积"(BDP)的定量关系;Linux 自动调优机制下,管理员真正该动的是哪几个旋钮。
  4. 常见故障如何分类与定位? 握手失败、连接被重置、队列溢出、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 明确指出"保证双方具有收发能力"只是片面回答,三大真正原因:

  1. 阻止历史连接(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 并可能开始发数据,"服务端没有中间状态给客户端来阻止历史连接",会对僵尸历史连接白白发数据直到资源耗尽。

  1. 同步双方初始序列号(ISN)。序列号承担去重、按序重组、确认三项职责,双向都要各确认一次对方的 ISN;服务端的 SYN 和 ACK 可以合并为一次发送,所以"四次"退化为"三次"。
  1. 避免资源浪费。两次握手下"服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接",重复阻塞 SYN 会堆积冗余连接。

ISN 为什么随机且每次不同:主要为了防止历史报文被下一个相同四元组的连接错收,其次是防伪造。RFC 793 的算法是 ISN = M + F(四元组),M 为每 4 微秒加 1 的计数器(约 4.55 小时循环一圈),F 为基于四元组的 Hash。

2.2 拥塞控制:自时钟 + AIMD 的锯齿,CUBIC 换了增长函数

《Systems Approach》§6.3 的理论骨架:

"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."

BBR 的定位(来自检索摘要与 ESnet 页侧写,未精读原文):Google 2016 年提出的算法,不再以丢包为拥塞信号,而是直接建模瓶颈带宽 × 往返时延,配合 fq 队列调度做 pacing。

2.3 缓冲区:BDP 定律 + "调上限而非调固定值"

"a 10Gbps flow across a 100ms network requires 120MB of buffering"

即缓冲区上限至少要等于带宽 × RTT,才能让管道持续满载。默认的 Linux TCP 缓冲区上限"still too small for 10G networks"。

  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"。

2.4 常见网络故障:按"握手失败的形态"分类定位

两个队列是故障重灾区(阿里云实验帖 + 小林coding 互证):

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 加速清队。

诊断命令与计数器(两文合计):

重传与"死亡"感知的时间账:


3. 方法

  1. 来源组合覆盖"图解入门 → 理论骨架 → 权威语义 → 实战配方 → 实验证据"五层:图解类(小林coding)负责把状态机画成可记的图并给出面试视角的"为什么";开放教科书(Systems Approach)负责把 AIMD/slow start 的数学讲透;内核手册(tcp(7))负责参数的准确语义与默认值;ESnet 负责大规模数据传输场景的实操配置;阿里云帖负责用内核实验把队列行为做实。
  2. 交叉验证同一参数在不同来源的说法,主动暴露矛盾点并记录(见 §2.4 与 §4.3 的三处不一致:syncookies 的推荐度、syncookies 是否完全防丢、tcp_abort_on_overflow 该不该开)。
  3. 引用原文关键句而非转述,中英文来源都保留原句以便复核。
  4. 未做本地复现实验:本机为 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_rcvbuf1(开)接收侧自动调优总开关
tcp_syncookies1半连接队列溢出时发 cookie
tcp_max_syn_backlog256(≥128MB 内存则 1024)半连接队列相关上限
tcp_abort_on_overflow0(关)accept 队列满时是否回 RST
tcp_fin_timeout60 sFIN_WAIT_2 的超时(close 关闭时)
tcp_keepalive_time/intvl/probes7200 / 75 / 9保活探测节奏(默认约 2h11m15s 才判死)
tcp_retries1 / tcp_retries23 / 15建立后重传:查路由阈值 / 放弃连接(约 13–30 min)
tcp_syn_retries / tcp_synack_retries5 / 5握手 SYN / SYN+ACK 重传次数(SYN 全程约 63 s)

4.2 阿里云队列溢出实验数据(kernel 4.19.91)

实验配置SYN_RECV 上限观测
1syncookies=0, somaxconn=1024, max_syn_backlog=12896(按公式预测触发丢包)
2syncookies=0, somaxconn=128, max_syn_backlog=512128(= accept 队列上限)
3asyncookies=1, somaxconn=128, max_syn_backlog=512128(syncookies 开着仍丢包)
3bsyncookies=1, somaxconn=55
4syncookies=1, somaxconn=256, max_syn_backlog=128256

公式(半连接队列上限):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 三处来源间矛盾(精读的价值所在)

  1. tcp_syncookies 的推荐度:小林coding 推荐设 1(仅队列满时启用);tcp(7) 却警告 syncookies "should be used as a last resort, if at all",称其为协议违规、可能与 TCP 扩展冲突。折中读法:对公开互联网抗 SYN flood,=1 是标准做法;对内网/可信环境,按 tcp(7) 保持谨慎。
  2. syncookies 能否完全防丢:流传说法是 syncookies=1 就不丢 SYN;阿里云实验证明只要半连接长度 > accept 队列上限照样触发丢 SYN(实验 3a/3b)——accept 队列满时 syncookies 救不了,必须把 somaxconn/backlog 一起调大。
  3. 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. 结论

  1. 三次握手的最小性有严格依据:它是"防历史连接 + 双向确认 ISN + 不浪费资源"三目标下的最小报文数;ISN 随机化(M + Hash(四元组))与第三握手的"中间状态"共同构成防线。
  2. 拥塞控制是分布式系统里"无信令、仅靠观测"的经典设计:ACK 自时钟驱动,AIMD 用指数退避+线性恢复逼近效率与公平的平衡点;从 Reno 到 CUBIC 的本质变化只是"增长函数从按 ACK 计数改为按时间的三次函数";BBR 进一步换掉了拥塞信号本身(丢包 → 带宽×RTT 模型)。
  3. 缓冲区调优的实务要点收敛为一条:保持自动调优开启,把 tcp_rmem[2]/tcp_wmem[2] 和 net.core.rmem_max/wmem_max 按 BDP 与场景(单流 or 并行流)调大,配 fq/fq_codel qdisc;默认值对高带宽长 RTT 链路差 1–2 个数量级,但对 <1 Gbps 用户反而要反向调小。
  4. 网络故障排查可以完全可观测化:握手失败的四种形态(无响应重传 SYN / 立即 RST / 建立后被 RST / 半开不对称)+ ss -lnt/netstat -s/ss -ti 三个命令 + ListenOverflows/ListenDrops/SYN cookies sent 等计数器,足以把大多数"连接慢/连不上/悄悄失败"定位到队列、防火墙或应用 accept 缺位。
  5. TIME_WAIT 与 CLOSE_WAIT 是两种不同性质的信号:前者是协议设计的一部分(《UNIX 网络编程》:"TIME_WAIT 是我们的朋友"),堆积到影响容量时调应用(长连接、连接池);后者几乎一定是代码缺陷,别动内核参数,去查 epoll 注册与 close 调用。

6. 局限

来源各自的局限(部分来源自认):

本篇的局限:


7. 对我的启发

  1. 调优先算 BDP,再动旋钮:任何"网络慢"的问题先问两个数——带宽和 RTT。10 Gbps×100 ms ≈ 120 MB 的管道对 4 MB 默认上限,问题不是 TCP 慢而是管道从来没满过。日常服务(内网、<1 Gbps、低 RTT)则几乎永远不需要动这些参数——ESnet 的配置是反例警示,不是通用模板。
  2. "调上限而非调固定值"是一条通用系统调优原则:Linux 自动调缓冲的思路(给范围、给天花板,让运行时自适应)优于写死参数;tcp_moderate_rcvbuf 的设计(自调优 + max 天花板)值得移植到其他子系统(线程池、连接池、缓存)的调优心智。
  3. 故障排查要有"信号 → 假设"的映射表:这次最值钱的产出是那张形态表——SYN 无响应(丢包/防火墙/队列满)、立即 RST(无监听)、RST after established(应用崩溃/空闲超时/有状态防火墙)、半开不对称(accept 队列满 + abort_on_overflow=0)。以后遇到"连不上"先对号入座,再用 netstat -s 的增量计数器证实,而不是先重启。
  4. 监控盲区在"两侧都正常"的路径上:那起 40% 利用率却大面积超时的事故提醒——服务端 CPU/内存/QPS 监控永远看不见"队列溢出导致的静默丢弃",必须把 ListenOverflows/ListenDrops 纳入告警,或者干脆开 tcp_abort_on_overflow=1 把静默失败变成显式失败。
  5. 写作参考:这批材料示范了把协议讲透的两种路径——图解类用"状态机 + 场景推演"(如果只有两次会怎样),教科书用"不变量 + 数学"(AIMD 为何收敛);我若写网络类博客,可以在两者之间取一页纸的 BDP 计算作为钩子。
  6. 可立即落地的 checklist:新服务上线前——ulimit -n/文件描述符三级限额、somaxconn 与 listen backlog 是否匹配、长连接 keepalive_requests 是否够、有没有应用层心跳(别指望 2h11m15s 的内核保活)、netstat -s 基线留档。

*本笔记由主题精读流程生成(2026-11-05);引文均来自当日在线抓取的原文内容,经 WebFetch 转述保留,个别措辞请以链接原页面为准。*

← 资料库今日播报 →