TCP 可靠传输、流量控制与拥塞控制
内容摘要:TCP 可靠性不是“丢了就重传”这么简单。发送端要从累计 ACK、SACK、重复 ACK、计时器和 RTT 样本推断丢包;同时受接收窗口
rwnd和拥塞窗口cwnd双重约束。本章把三类问题分开:可靠传输解决丢失/乱序,流量控制保护接收端,拥塞控制保护网络,并通过 RTO 与 BDP 实算、Linuxss -ti/tc netem实验理解性能边界。难度:高级 | 前置知识:03 性能指标与 BDP、09 MTU、13 TCP 状态机 | 所属层次:传输层
一、三类控制必须分开
| 机制 | 要保护谁 | 主要信号 | 核心变量 |
|---|---|---|---|
| 可靠传输 | 数据正确、完整、有序 | 序列号、ACK、SACK、计时器、校验和 | SND.UNA、SND.NXT、RTO |
| 流量控制 | 接收主机/应用 | 接收端通告窗口 | rwnd |
| 拥塞控制 | 网络路径与其他流量 | ACK 时钟、丢包、ECN、RTT | cwnd、ssthresh |
发送端实际可保留的在途数据上限:
send_window = min(rwnd, cwnd)还要受应用是否有数据、发送缓冲区和实现策略约束:
常见误判:吞吐低时只调
SO_SNDBUF没用。若限制来自cwnd(丢包/拥塞)或应用本身无数据,扩大缓冲区不会提高吞吐。
二、序列号、确认号与累计 ACK
TCP 给每个字节编号。设某段:
Seq = 1001, Len = 500它承载字节 [1001, 1501);接收端完整收到后返回:
ACK = 1501ACK 表示“1501 之前的字节已连续收到,下一个期望 1501”,不是“我收到了编号 1501 的段”。
乱序与重复
- 后到的高序号段可暂存在乱序队列,等待缺口补齐。
- 重复段通过序列号识别,不会向应用重复交付。
- 应用只读到连续有序字节,因此一个缺失段会阻塞后续字节交付,形成 TCP 层队头阻塞(Head-of-Line Blocking,HOL)。
- HTTP/2 在一条 TCP 连接多路复用时仍受此影响;HTTP/3/QUIC 用独立流降低跨流阻塞(16)。
三、校验和与错误恢复
TCP 校验和覆盖 IP 伪首部、TCP 头和数据:
- 检测偶然比特错误,错误段被丢弃。
- 不提供密码学完整性,攻击者可重新计算;安全传输用 TLS(17)。
- 本机发送侧抓包可能因 checksum offload 显示错误,需在对端抓包或关闭卸载验证(04)。
可靠性依赖“检测 + 恢复”:
四、重传超时 RTO
固定超时无法适应路径 RTT 变化。RFC 6298 用平滑 RTT(Smoothed RTT,SRTT)和 RTT 方差(RTTVAR)计算:
首次样本 R:
SRTT = R
RTTVAR = R / 2
RTO = SRTT + max(G, 4 × RTTVAR)后续样本 R',推荐 α=1/8、β=1/4:
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - R'|
SRTT = (1 - α) × SRTT + α × R'
RTO = SRTT + max(G, 4 × RTTVAR)其中 G 是时钟粒度。
1、本机可复现实算
python3 - <<'PY'
alpha = 1/8
beta = 1/4
K = 4
G = 0.001
r = 0.120
srtt = r
rttvar = r / 2
rto = srtt + max(G, K * rttvar)
print(f"first: SRTT={srtt:.3f}s RTTVAR={rttvar:.3f}s RTO={rto:.3f}s")
r2 = 0.180
rttvar = (1-beta)*rttvar + beta*abs(srtt-r2)
srtt = (1-alpha)*srtt + alpha*r2
rto = srtt + max(G, K*rttvar)
print(f"next: SRTT={srtt:.4f}s RTTVAR={rttvar:.4f}s RTO={rto:.4f}s")
print(f"RFC6298 floor: {max(1.0, rto):.1f}s")
PY真实输出(2026-09-07):
first: SRTT=0.120s RTTVAR=0.060s RTO=0.360s
next: SRTT=0.1275s RTTVAR=0.0600s RTO=0.3675s
RFC6298 floor: 1.0sRFC 6298 的保守规则包括:
- 尚无 RTT 样本时,初始 RTO 应为 1 秒;实现可使用更大值。
- 计算值小于 1 秒时,标准算法应向上取到 1 秒;实际现代内核的内部最小值可能不同,属于实现差异。
- RTO 到期重传最早未确认段,并执行
RTO = RTO × 2指数退避。 - 重传过的段存在 ACK 歧义,遵循 Karn 算法不直接取 RTT 样本;时间戳选项可消除部分歧义。
RTO 是最后兜底,通常比快速丢包检测慢得多。看到整秒级停顿时,应检查 RTO、重传与 ACK 路径。
五、快速重传、SACK 与现代丢包检测
1、重复 ACK 与快速重传
若中间段丢失而后续段到达,接收方反复确认同一个缺口。经典 Reno 在收到 3 个重复 ACK 后,无需等待 RTO 即快速重传疑似丢失段。
局限:
- 小窗口里没有足够后续段,产生不了 3 个重复 ACK。
- 重排序也会产生重复 ACK,可能导致伪重传。
- 一个窗口丢多个段时,只有累计 ACK 信息恢复较慢。
2、选择性确认 SACK
SACK(Selective Acknowledgment,RFC 2018)让接收方报告已收到的非连续区间:
累计 ACK = 1501
SACK block = [2001, 3001)
含义:1501 起有缺口,但 2001–3000 已收到发送端只重传缺失区间,不必重传已收到数据。边界:
- SYN 中先通过
SACK Permitted协商。 - SACK 是补充信息,不改变累计 ACK 的含义。
- SACK 选项空间有限;结合时间戳时常只能报告少量块。
- 丢包恢复仍必须遵守拥塞控制,不能因为知道缺口就无限加速。
3、RACK-TLP
现代 Linux 等实现通常支持基于时间的 RACK-TLP(RFC 8985):
- RACK(Recent ACKnowledgment)根据较新的已确认包和时间关系判断旧包丢失,更适应重排序。
- TLP(Tail Loss Probe)在尾部少量包丢失时主动探测,避免等待长 RTO。
具体启用状态、阈值与统计项随内核版本变化,排障须记录目标内核与 ss -ti/内核指标。
六、流量控制:接收窗口 rwnd
接收端在 TCP 头 Window 字段通告还能接收多少字节:
1、滑动窗口
发送窗口随 ACK 前移:
已确认 | 已发送未确认(FlightSize) | 当前允许发送 | 暂不允许
^ SND.UNA ^ SND.NXT- ACK 前移后,已确认数据从重传队列释放,窗口向右滑。
rwnd防止发送端压垮接收端。- 16 位 Window 最大 65,535;RFC 7323 Window Scale 在 SYN 中协商左移因子,支持大 BDP。
2、零窗口与 Persist Timer
接收端通告 rwnd=0 后,如果后续 Window Update 丢失,双方可能永久等待。发送端使用 persist timer 周期发送零窗口探测,促使接收端重发窗口信息。
持续零窗口说明:
- 接收应用读取太慢或阻塞。
- 接收缓冲区设置不合理。
- 下游背压没有传导,应用仍在上游堆积数据(04)。
不要把零窗口直接归因“网络拥塞”;它首先是接收端容量信号。
七、拥塞窗口 cwnd 与 ACK Clock
cwnd 是发送端对网络可承载在途数据的估计。ACK 返回说明数据离开网络,触发发送新数据,形成 ACK clock:
拥塞控制不能只追求单流吞吐,还要:
- 避免拥塞崩溃。
- 与其他流公平共享瓶颈。
- 对容量变化和突发做出响应。
八、经典四算法:慢启动、拥塞避免、快速重传、快速恢复
RFC 5681 定义经典 Reno 行为,关键变量:
cwnd:拥塞窗口。ssthresh:慢启动阈值。FlightSize:已发送但未累计确认的数据量。SMSS:发送方最大报文段数据长度。
1、慢启动
名字叫“慢”,窗口增长实际上接近每 RTT 翻倍:
每收到确认新数据的 ACK:cwnd 增加最多一个 SMSS
一个 RTT 收到约 cwnd/SMSS 个 ACK
因此 cwnd 约按 RTT 指数增长新连接先探测未知路径容量,达到 ssthresh 或遇到拥塞后退出。
初始窗口版本边界:
- RFC 5681 的基础文本限定 IW 为 2–4 个段(按 SMSS)。
- RFC 6928 是 Experimental,提出
IW10,上限min(10×MSS, max(2×MSS, 14600))。 - IW10 已在主流实现广泛部署,但文档编写时必须区分“RFC 5681 标准基线”“RFC 6928 实验规范”和“具体 OS 默认值”,不能混写成唯一标准。
2、拥塞避免
经典 Reno 约每 RTT 增长一个 SMSS,属于加性增大:
cwnd += SMSS × SMSS / cwnd # 每个确认新数据的 ACK 近似更新发生拥塞后乘性减小,形成 AIMD(Additive Increase Multiplicative Decrease)。
3、快速重传与快速恢复
- 重复 ACK/SACK 表明网络仍在前进,只是出现缺口。
- 快速重传缺失段,不等待 RTO。
ssthresh通常按max(FlightSize/2, 2×SMSS)降低。- 快速恢复保持 ACK clock,避免完全回到一个段。
RTO 表示反馈链路更严重中断,处理比快速恢复保守。
九、CUBIC、Reno、BBR 与 ECN
1、CUBIC
RFC 9438(Standards Track,取代 RFC 8312)定义 CUBIC:
- 用距上次拥塞事件的时间和三次函数增长窗口。
- 在上次最大窗口附近形成平台,减少反复越过瓶颈。
- 在大 BDP 路径上比 Reno 线性增长更易利用带宽。
- 拥塞事件后标准乘性减小因子为 0.7,而 Reno 常减半。
- RFC 9438 推荐配合 HyStart++(RFC 9406)更早退出过度慢启动。
RFC 9438 记录 CUBIC 已成为 Linux、Windows 和 Apple TCP 栈的默认拥塞控制算法,但部署仍要以目标系统版本实测。
2、BBR
BBR(Bottleneck Bandwidth and Round-trip propagation time)基于带宽与最小 RTT 建模,尝试在不依赖丢包作为唯一拥塞信号的情况下控制 pacing rate 和在途量。
边界:
- BBR 版本、内核实现和公平性行为持续演进。
- 它不是 RFC 9438 那样的 IETF Standards Track TCP 算法。
- 不应只因“吞吐高”在生产全局切换;需测试 RTT 公平性、队列、重传和与 CUBIC/Reno 共存。
3、ECN
显式拥塞通知(Explicit Congestion Notification,ECN)允许支持的路由器标记拥塞,而不是必须丢包:
ECN 需要端点与路径设备共同支持;开启状态和算法细节需实测。
十、BDP、窗口与吞吐上限
带宽时延积(Bandwidth-Delay Product,BDP)是“填满管道”所需在途数据:
BDP = bandwidth × RTT1 Gbit/s、RTT 80ms:
1,000,000,000 bit/s × 0.080s ÷ 8 = 10,000,000 bytes ≈ 10 MB本机脚本实算输出:
BDP: 10.0 MB若 min(rwnd, cwnd) 长期只有 1 MB,理想单流吞吐上限约:
1 MB / 0.080s = 12.5 MB/s ≈ 100 Mbit/s因此高带宽高时延路径需要:
- Window Scale。
- 足够大的 socket/自动调优缓冲区。
- 适合大 BDP 的拥塞控制与 pacing。
- 低丢包;单次丢包在长 RTT 上恢复代价高。
缓冲区并非越大越好。瓶颈队列过深会产生缓冲膨胀(bufferbloat),吞吐看似正常但 RTT/尾延迟暴涨(QoS 与高可用)。
十一、Nagle、延迟 ACK 与小包延迟
- Nagle 算法:有未确认小段时合并后续小写入,减少 tinygram。
- Delayed ACK:接收端短暂等待,以便累计确认或把 ACK 搭载在反向数据上。
组合不当可能让交互式小消息等待:
发送端等待 ACK 才发小段
接收端等待更多数据才发 ACK实践:
- 延迟敏感 RPC/游戏控制消息可评估
TCP_NODELAY。 - 批量吞吐通常保留合并更有效。
- 先测抓包中的小段、PSH、ACK 间隔和应用写入模式;不要全局盲关。
十二、本机 TCP 参数观测
macOS 26.6.2 实测:
sysctl net.inet.tcp.sendspace
sysctl net.inet.tcp.recvspace
sysctl net.inet.tcp.win_scale_factor
sysctl net.inet.tcp.delayed_ack
sysctl net.inet.tcp.mssdflt真实输出:
net.inet.tcp.sendspace: 131072
net.inet.tcp.recvspace: 131072
net.inet.tcp.win_scale_factor: 3
net.inet.tcp.delayed_ack: 3
net.inet.tcp.mssdflt: 512解释边界:
- 这是本机默认参数快照,不是每条连接最终缓冲区、窗口或 MSS。
mssdflt=512是缺少路径信息时的系统默认之一,不能据此断言实际以太网连接 MSS 为 512。- 自动调优、路由缓存、对端 MSS、PMTUD 和 socket 选项都会影响单连接结果。
- macOS 参数名和 Linux
net.ipv4.tcp_*不可直接互换。
十三、Linux 故障注入实验
以下在 20 统一实验拓扑 中执行;当前 macOS 未执行。
1、目标
观察:
- 正常路径的
cwnd、RTT、RTO、重传。 - 1% 丢包后 SACK/重传与吞吐变化。
- 增加 80ms RTT 后 BDP 对窗口的要求。
- 接收应用暂停读取时的
rwnd/零窗口。
2、服务与观测
# [未在当前环境执行] server
sudo ip netns exec srv iperf3 -s -1
# client:每 200ms 查看 TCP 内部信息
watch -n 0.2 "sudo ip netns exec cli ss -tin dst 198.51.100.2"
# 另一个终端发流 20 秒
sudo ip netns exec cli iperf3 -c 198.51.100.2 -t 20 -i 1ss -ti 关注:
cubic wscale:... rto:... rtt:... mss:... cwnd:... ssthresh:...
bytes_sent:... bytes_acked:... retrans:... pacing_rate:...字段随内核/iproute2 版本不同,以本机输出为准。
3、注入延迟与丢包
# 增加 80ms ± 5ms 延迟和 1% 随机丢包
sudo ip netns exec rtr tc qdisc replace dev veth-rs root netem \
delay 80ms 5ms loss 1%
sudo ip netns exec cli iperf3 -c 198.51.100.2 -t 30 -i 1
# 清理
sudo ip netns exec rtr tc qdisc del dev veth-rs root预期而非伪造输出:
- RTT 上升,RTO 随样本调整。
- 丢包时重传计数增长,
cwnd降低后再恢复。 - 吞吐呈波动;具体值取决于 CPU、虚拟化、算法和随机丢包序列。
4、抓包过滤
sudo ip netns exec rtr tcpdump -i any -nn -s 0 -w tcp-loss.pcap \
'host 192.0.2.2 and host 198.51.100.2'Wireshark:
tcp.analysis.retransmission
tcp.analysis.fast_retransmission
tcp.analysis.duplicate_ack
tcp.options.sack
tcp.analysis.zero_window
tcp.analysis.window_fullWireshark 的 analysis 标记是基于当前抓包点推断;抓包缺包、GRO/TSO、非对称路径都可能制造假象(04、20 抓包与实验)。
十四、性能排查决策表
| 证据 | 更可能的限制 | 下一步 |
|---|---|---|
rwnd 很小/零窗口 | 接收应用或接收缓冲区 | 查应用读取、CPU/GC、缓冲区 |
cwnd 小且重传增长 | 丢包/拥塞 | 双点抓包、队列/接口丢包、ECN |
| RTT 随吞吐大幅上升 | 队列过深/bufferbloat | AQM、整形、队列位置 |
| RTT 稳定但单流吞吐约等于 Window/RTT | BDP/窗口限制 | Window Scale、缓冲区自动调优 |
| 周期性整秒停顿 | RTO/尾丢包/ACK 路径 | ss -ti、RACK/TLP、双向抓包 |
| 应用写阻塞但网络无丢包 | 背压或对端零窗口 | socket 缓冲、接收应用 |
| 多流总吞吐高、单流低 | 单流 cwnd/BDP 或哈希路径 | 单流 TCP 指标、并发与 ECMP |
十五、常见误区
| 误区 | 纠正 |
|---|---|
| “TCP 有重传,所以不会失败” | 重试有时限,持续断网最终仍失败 |
| “ACK 表示应用已处理” | 通常只表示对端 TCP 已接收,不代表业务提交 |
| “丢包一定是物理链路坏” | 队列拥塞、socket 缓冲、策略设备都可能丢 |
| “rwnd 和 cwnd 是同一个窗口” | rwnd 保护接收端,cwnd 保护网络 |
| “慢启动增长很慢” | 近似每 RTT 翻倍,可能很快形成突发 |
| “扩大缓冲区一定提速” | 可能制造 bufferbloat;先确认限制项和 BDP |
| “CUBIC/BBR 永远优于 Reno” | 取决于路径、竞争流、版本与目标;需基准测试 |
| “Wireshark 标记重传就一定在线上重传” | 抓包点、卸载和抓包丢失可造成误判 |
十六、安全与稳健性
- 伪造 ACK 可诱导错误发送行为;现代 TCP 依赖不可预测序列号与严格窗口校验。
- ACK Division、乐观 ACK 等异常接收端可能试图放大
cwnd;RFC 5681 推荐按确认字节控制增长。 - SACK 实现曾出现整数溢出等漏洞;内核需保持受支持版本并跟踪安全公告。
- 应用必须设置总超时和取消机制,不能把内核无限重试当作业务可用性策略。
十七、版本与实现差异
- RFC 9293 是现代 TCP 基础规范;RFC 5681 是经典 Reno 拥塞控制基线。
- RFC 9438 已将 CUBIC 提升到 Standards Track,并取代 RFC 8312。
- RFC 6928 的 IW10 状态仍是 Experimental,尽管主流实现广泛采用。
- Linux、Windows、Apple 的 CUBIC 默认与参数可能因版本不同;BBR 也有版本差异。
- RTO 最小值、delayed ACK、自动缓冲、RACK/TLP 与 pacing 细节必须按目标内核核验。
十八、关联章节
- 上一章:13 TCP 报文、连接与状态机
- 下一章:15 DNS
- 相关:03 BDP 与 RTT、09 PMTUD、16 HTTP/2 与 HTTP/3、QoS 与高可用、20 抓包与实验
检索关键词
TCP 序列号 ACK 下一个期望字节 累计确认 SACK 快速重传 RACK TLP TCP RTO SRTT RTTVAR RFC 6298 Karn 算法 滑动窗口 rwnd 零窗口 persist timer 拥塞窗口 cwnd ssthresh FlightSize TCP 慢启动 拥塞避免 快速恢复 IW10 RFC 6928 CUBIC RFC 9438 BBR ECN BDP TCP 吞吐 Nagle delayed ACK
权威来源
- RFC 9293:Transmission Control Protocol
- RFC 5681:TCP Congestion Control
- RFC 6298:Computing TCP's Retransmission Timer
- RFC 2018:TCP Selective Acknowledgment、RFC 6675
- RFC 7323:Window Scale、Timestamps 与 PAWS
- RFC 8985:RACK-TLP
- RFC 9438:CUBIC、RFC 9406(HyStart++)
- RFC 6928:IW10(Experimental)
算法与版本状态已于 2026-09-07 对照 RFC Editor 正文核验。RTO/BDP 脚本和 macOS TCP 参数已真实执行;Linux
ss -ti、iperf3、tc netem与 PCAP 实验因当前平台/权限限制明确标记为未执行。
