TCP 三次握手、重传与拥塞窗口:可运行的序列号实验
TCP 三次握手、重传与拥塞窗口:可运行的序列号实验

TCP 三次握手、重传与拥塞窗口:可运行的序列号实验

网页之所以能稳定加载,并不是因为互联网的骨干网完美无缺(实际上丢包是常态),而是因为 TCP 状态机通过一套极其严密的数学模型和拥塞控制算法,在混乱的网络中强制维持了字节流的连续性。对于高频交易(HFT)系统或超大规模 CDN,教科书上的“三次握手”早已无法满足排障需求。在生产环境中,你必须能够深入内核调整拥塞窗口(cwnd)、接收窗口(rwnd),对抗缓冲区膨胀(Bufferbloat),并利用 BBR 的平滑发包(Pacing)算法将 P99 尾部延迟压到极限。

在这篇深度解析中,我们将直接潜入 Linux 内核 net/ipv4/tcp_input.c 的源码,推导 CUBIC 与 BBR 的核心微分方程,并使用 perf 火焰图来剖析 TCP 协议栈在高并发下的性能天花板。

一、序列号空间与内核状态机机制

三次握手(SYN, SYN-ACK, ACK)通常不是性能瓶颈所在,除非你遭遇了 SYN 泛洪攻击(需要开启 net.ipv4.tcp_syncookies 抵御)。TCP 真正的复杂度和算力消耗,集中在数据传输阶段对“序列空间 (Sequence Space)”的极速运算上。

在 Linux 内核中,每个 TCP 连接由一个庞大的 struct tcp_sock 结构体维护。每次收到 ACK 报文,内核都会触发 tcp_ack() 函数。这个数百行的巨兽必须瞬间判断出:这是不是一个重复确认?是否携带了 SACK 块?是否需要解除定时器?以及最核心的——是否应该推进拥塞窗口?

/* 节选自 linux/net/ipv4/tcp_input.c,核心的 ACK 处理逻辑 */
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 prior_snd_una = tp->snd_una;
    u32 ack_seq = TCP_SKB_CB(skb)->ack_seq;

    /* 防御性编程:如果是乱序、过期的老 ACK,直接丢弃 */
    if (before(ack_seq, prior_snd_una))
        goto old_ack;

    /* 解析 SACK(选择性确认) 块,用于在丢包时精准定位缺失的数据段 */
    if (tcp_is_sack(tp) && tcp_check_sack_reneging(sk, flag))
        tcp_retransmit_timer(sk);

    /* 调用核心的拥塞控制状态机,更新 cwnd 和 pacing rate */
    tcp_cong_control(sk, ack_seq, prior_snd_una, flag);
    return 1;
}

当序列号出现空洞(乱序到达),内核会将这些包暂存到 Out-Of-Order (OOO) 队列,并向发送方回复 Duplicate ACK。当发送方连续收到 3 个 Duplicate ACK,将直接跳过漫长的 RTO 定时器,立刻触发快速重传 (Fast Retransmit)

sequenceDiagram
    participant Client (发送端内核)
    participant Server (接收端内核)
    Note over Client,Server: RTT = 20ms, MSS = 1460
    Client->>Server: DATA, seq=1001, len=1460
    Server->>Client: ACK, ack=2461
    Client-xServer: DATA, seq=2461, len=1460 (网络丢包!)
    Client->>Server: DATA, seq=3921, len=1460
    Server->>Client: DUP ACK, ack=2461 (SACK 3921-5381)
    Client->>Server: DATA, seq=5381, len=1460
    Server->>Client: DUP ACK, ack=2461 (SACK 3921-6841)
    Note over Client: 收到 3 个 DUP ACK,触发快速重传
    Client->>Server: DATA, seq=2461, len=1460 (精准重传丢失段)
    Server->>Client: ACK, ack=6841 (累积确认瞬间推进!)

二、拥塞控制的数学博弈

当网络发生丢包时,TCP 必须通过动态收缩发送窗口来缓解拥塞。传统的算法(如 NewReno)遵循 AIMD(加性增,乘性减)的简单数学模型。

被时代抛弃的 CUBIC 算法

旧版 Linux 默认使用 CUBIC。CUBIC 的拥塞窗口大小是一个关于时间的“三次函数”,这使得其增长曲线不再受 RTT 延迟长短的影响。在时间 ( t ) 的窗口大小 ( W ) 定义为:

$$ W_{cubic}(t) = C(t – K)^3 + W_{max} $$

其中 ( K = sqrt[3]{ frac{W_{max} beta}{C} } ),而 ( beta ) 是乘性减小因子(CUBIC 默认为 0.7)。致命弱点在于:一旦发生任何轻微丢包,CUBIC 会毫不留情地将发送速率砍掉 30%(( W = W times beta )),在千兆宽带下这会导致吞吐量断崖式暴跌。

革命性的 BBR 算法 (Bottleneck Bandwidth and RTT)

Google 提出的 BBR 算法彻底推翻了“丢包即拥塞”的假设。BBR 利用排队论中的利特尔法则 (Little’s Law, ( L = lambda W )),实时测算链路的瓶颈带宽(BtlBw)和最小物理延迟(RTprop),进而计算出最佳的飞行中数据量(BDP, 带宽延迟乘积):

$$ BDP = BtlBw times RTprop $$

BBR 的状态机在 PROBE_BW(带宽探测)阶段,会施加一个 ( 1.25 ) 的 Pacing Gain (起步增益)。当探测到链路塞满、RTT 开始上升时,它会立刻将增益下调至 ( 0.75 ) 来排空路由器队列。这种基于模型的控制,使得 BBR 完全免疫了无线网络中因信号衰减引发的随机丢包。

CUBIC 锯齿波形与 BBR 平滑曲线对比
数学轨迹对比:CUBIC 遇到丢包时的锯齿状重度惩罚,对比 BBR 基于 BDP 演算的平滑 Pacing 发包机制。

三、内核级性能剖析:`perf` 与火焰图

在高并发服务端,仅仅知道概念不够,你必须能够测量 TCP 协议栈在内核中的 CPU 开销。使用 perf 抓取内核事件并生成火焰图,是定位高负载机器 TCP 瓶颈(如自旋锁竞争、Checksum 计算阻塞)的唯一手段。

# 在全系统范围内追踪内核的 TCP 重传事件
sudo perf record -e tcp:tcp_retransmit_skb -aR -g
# 将 perf 数据转化为直观的火焰图 (需配合 Brendan Gregg 的工具集)
sudo perf script | stackcollapse-perf.pl | flamegraph.pl > tcp_retrans_flame.svg

# 提取活跃 Socket 的内部状态,观察 BBR 核心测算指标
ss -nti | grep -A 1 'ESTAB'
# 输出示例:
#  cwnd:10 pacing_rate 1200Mbps delivery_rate 950Mbps bbr:(bw:950Mbps,mrtt:15.2ms,pacing_gain:1.25)

当应用层出现莫名的 P99 延迟毛刺时,抓取 tcp:tcp_retransmit_skb 事件频率,通常能直接将锅扣在网络重传,或者证明系统 GC (垃圾回收) 导致了内核缓冲区得不到及时读取。

四、深度生产事故复盘:Bufferbloat 与 FQ-CoDel

被“大内存路由器”坑惨的流媒体集群

在一次大型直播流媒体平台的发布上线中,我们的负载均衡器虽然只跑到了 40Gbps(远低于硬件极限),但大量用户的播放器疯狂卡顿,后台显示 Ping 延迟从正常的 30ms 飙升到了荒谬的 2500ms!通过提取机器的 ss -nti 数据,我们发现 TCP 的 cwnd 膨胀到了夸张的地步。

这正是经典的 缓冲区膨胀 (Bufferbloat) 灾难。由于运营商节点和中间路由器配置了极其庞大的内存队列,传统的 CUBIC 算法在没遇到丢包前会拼命增大发送窗口。这导致原本应该被路由器提早丢弃以发出拥塞信号的数据包,全部堆积在了路由器的深度队列中,形成了可怕的排队延迟。

最终的 SRE 介入方案是:在内核修改排队规则(Qdisc)为 fq_codel (公平队列控制延迟),并将 TCP 拥塞控制全面切换为 BBR。FQ-CoDel 主动将驻留时间过长的数据包丢弃,而 BBR 则按瓶颈带宽严格均匀发包(Pacing)。修改生效的瞬间,尾部延迟直接被打平回 50ms 以下。

sysctl -w net.core.default_qdisc=fq_codel
sysctl -w net.ipv4.tcp_congestion_control=bbr

五、事件自动化数据仿真

为了不受公网抖动影响地研究 AIMD 和 BBR 状态转换,我们可以利用 Python 进行数学建模,并配合绑定在 127.0.0.1 上的 C11 Socket 程序来导出精确的状态事件矩阵。

python src/tcp_reliability_cwnd.py
cc -std=c11 -Wall -Wextra -O2 src/tcp_loopback_echo.c -o /tmp/tcp_echo
/tmp/tcp_echo

每次状态转换(包括 cwnd 收缩和重传触发点)的精细日志已导出至 tcp-cwnd-results.csv

六、动画解析:Fast Recovery 状态机

动画展示了当探测到重复 ACK 时,TCP 状态机如何从 Open 平滑切换到 Fast Recovery 模式,并在接收到累计确认后恢复窗口。

七、工程排障防坑指南 (Anti-Patterns)

  • 关闭网卡硬件卸载: 永远不要手动去执行 ethtool -K eth0 tso off!让内核 CPU 去计算每一个 TCP 包头是一种严重的资源浪费。开启 TSO (TCP Segmentation Offload) 和 LRO 可以将这部分消耗卸载给网卡芯片,最高节省 40% 的 CPU 占用。
  • 误读 Zero-Window: 当你在抓包时看到客户端发来 TCP Zero Window,这并不代表网络拥塞!这说明客户端所在主机的应用进程“卡死了”(可能是 CPU 被占满或遇到了死锁),导致它没有调用 recv() 读取套接字缓冲区的内容,内核只能通知发送端停止发送。
  • 盲目调大缓冲区参数: 有些新手喜欢在 sysctl.conf 里把 tcp_rmemtcp_wmem 设成几个 G。这毫无意义,不仅不会加速传输,反而会加剧系统内存消耗和 Bufferbloat。除非你在调试跨大西洋的 100Gbps 专线,否则请相信内核的自动调优机制 (Auto-Tuning)。

FAQ

为什么谷歌要强推 BBR 彻底替换 CUBIC?

在 Wi-Fi 和 5G 时代,数据包经常因为无线电干扰而随机丢失。CUBIC 作为基于丢包的算法,只要看到丢包就盲目认为网络塞车了,然后把速度砍掉一半。而 BBR 足够聪明,它发现丢包的同时 RTT 并没有明显增加,就判定这是物理层干扰,从而忽略此次丢包,继续保持满速传输。

三次握手成功,就能保证数据不丢吗?

完全不能。三次握手仅仅是在操作系统内核中开辟了内存结构,并完成了序列号(ISN)和扩展参数(WScale, SACK)的密码学随机协商。数据的可靠到达,100% 仰赖于后续的滑动窗口确认和超时重传状态机。

References

当网络层路由和传输层 TCP 保证了数据的完整与可靠后,我们在最后一篇将视线拔高到应用协议的顶端:HTTP/3、QUIC 架构以及极致的 CDN 边缘缓存技术。

搜索问题

常见问题

这篇文章适合谁读?

这篇文章适合想用 进阶 难度理解“TCP 三次握手、重传与拥塞窗口:可运行的序列号实验”的读者,预计阅读时间约 13 分钟,重点覆盖 TCP, Congestion Control, Python, C sockets。

读完后下一步应该看什么?

推荐下一步阅读“HTTPS 与 TLS 1.3 握手原理:密钥交换、证书和 RTT 实验”,这样可以把当前知识点接到更完整的学习路线里。

这篇文章有没有可运行代码或配套资源?

有。页面里的运行说明、资源卡片和下载入口会指向复现实验所需的命令、数据、代码或说明文件。

这篇文章和整个网站的学习路线有什么关系?

它会通过文章上下文、学习路线、资源库和项目时间线连接到同一主题下的其他内容。

文章上下文

网络基础原理

从 DNS、TCP、TLS 与 HTTP/3 到代理隧道、负载均衡和共享缓存,以可重现的代码和图分析网页请求路径。

难度: 进阶 阅读时间: 13 分钟
  • TCP
  • Congestion Control
  • Python
  • C sockets
对应语言版本 TCP Reliability and Congestion Window: A Runnable Sequence Number Experiment
可分享摘要 TCP 三次握手、重传与拥塞窗口:可运行的序列号实验

从 TCP sequence/ACK 和慢启动出发,用确定性丢包曲线与 localhost C socket 实验理解可靠传输。

下载分享图 打开分享中心

配套资源

发表回复

项目时间线

已发布文章

  1. DNS 解析过程详解:从域名查询到 TTL 缓存的 Python 实验 从 RFC DNS 报文与递归查询出发,用 Python 和 C 实验计算 TTL 缓存命中对解析延迟的影响。
  2. CIDR、子网掩码与最长前缀匹配:用代码算清 IP 路由和 MTU 手算 CIDR 网段、最长前缀匹配与 MTU/MSS 分段,并用 Python/C 输出固定路由结果。
  3. TCP 三次握手、重传与拥塞窗口:可运行的序列号实验 从 TCP sequence/ACK 和慢启动出发,用确定性丢包曲线与 localhost C socket 实验理解可靠传输。
  4. HTTPS 与 TLS 1.3 握手原理:密钥交换、证书和 RTT 实验 解释 TLS 1.3 消息 flight、证书与临时密钥交换,用安全的教学模型计算一次 RTT 握手。
  5. HTTP/2、HTTP/3 与 CDN 缓存:从网络瀑布图理解网页加载速度 用确定性 waterfall 模型拆解 HTTP/2、HTTP/3、QUIC stream 和 CDN HIT/MISS 对网页等待时间的影响。
  6. 正向代理与反向代理原理:连接路径、信任边界和时延计算 从连接方向和 TLS 终止点解释正向代理、反向代理与隧道代理,并用 Python 模型分段计算代理 hop 与缓存收益。
  7. HTTP CONNECT 与 HTTPS 代理隧道:TLS 边界和握手时延 以 RFC CONNECT 状态机解释 HTTPS 代理隧道、TLS 可见性和首次加密请求时延。
  8. SOCKS5 代理原理:协议字节、DNS 解析边界与泄漏风险 按 RFC 1928 拆解 SOCKS5 CONNECT 字节,通过安全编码实验比较本地 DNS 与代理侧域名解析。
  9. 反向代理负载均衡原理:队列、健康检查和可复现调度实验 用固定请求队列比较 round robin 与负载感知调度,并解释反向代理健康检查和重试边界。
  10. 代理缓存与重新验证:Cache-Control、ETag 和可观测性实验 依据 RFC 9111 计算共享缓存 MISS、HIT 与 304 revalidation 的时延,并解释缓存 key 和隐私边界。

已公开资源

  1. Network Fundamentals Lab 说明 安装、无权限安全边界、十个 Python 实验和三个 C 示例的运行说明。
  2. 网络基础原理完整实验包 打包 Python/C 源码、固定场景、十份结果 CSV 与协议/代理图。
  3. DNS TTL 结果 CSV 四次固定查询的 HIT/MISS、过期时间和解析延迟。
  4. CIDR 与 MTU 结果 CSV 最长前缀路由和 3600 B payload 分段计算结果。
  5. TCP cwnd 事件 CSV 逐轮记录 ACK、窗口和固定重传事件。
  6. TLS 1.3 flight 结果 CSV 固定 RTT 模型中的消息方向、时间点和教学共享值。
  7. HTTP/CDN waterfall 结果 CSV HTTP/2 与 HTTP/3 在冷暖缓存模型中的分阶段耗时。
  8. 代理路径时延结果 CSV 直接访问、正向代理隧道与反向代理缓存路径的分阶段等待。
  9. CONNECT/TLS 时间线 CSV 记录 CONNECT authority、隧道建立与加密 HTTPS 请求的状态边界。
  10. SOCKS5 DNS 边界 CSV 保存 ATYP、目标字节、请求长度和本机 DNS 解析计数。
  11. 代理负载均衡队列 CSV 比较 round robin 与 least queue 的 backend 选择和排队等待。
  12. 代理缓存重新验证 CSV 记录 MISS、HIT、304 重新验证、对象年龄和响应时延。
  13. 网络请求链路交互演示 在浏览器里调整 TTL、前缀、丢包、握手 RTT 与缓存路径。
  14. 网络基础原理专题分享图 用于分享 DNS、TLS、HTTP/3、代理隧道和缓存专题的 1200x630 SVG 图。

下一步计划

  1. 补充 IPv6 与 QUIC 报文观察笔记
  2. 继续用真实用户指标复查缓存与协议收益
向下探索