tcp - flow - congestion
07. TCP 流量控制、拥塞控制与性能
0. 本章先解决什么问题
理解 TCP 基础以后,还会遇到一个更现实的问题:
连接已经建立,数据也没有完全丢,为什么传输还是慢?
答案通常不在“TCP 可不可靠”,而在三个限制:
- 接收方能不能处理得过来。
- 网络路径能不能承受这么多在途数据。
- 发送方有没有把链路填满,又有没有造成排队和丢包。
本章讨论 TCP 的两个核心控制机制:
| 机制 | 控制对象 | 目标 |
|---|---|---|
| 流量控制 | 接收方缓冲区和处理能力 | 不把对端撑爆 |
| 拥塞控制 | 中间网络路径 | 不把网络打爆 |
然后把它们和真实性能现象联系起来:吞吐低、延迟高、重传多、窗口小、长距离传输慢。
这张图怎么读
这张图把 TCP 数据阶段的核心约束画出来:发送方真正能放在路上的数据量受 min(rwnd, cwnd) 限制。rwnd 代表接收方还能接多少,cwnd 代表发送方估计网络路径还能承受多少。
1. 发送速度不是只由带宽决定
很多人看到“带宽 100 Mbps”就以为应用可以稳定传 100 Mbps。真实吞吐还受 RTT、窗口、丢包、接收能力、拥塞控制影响。
先看一个关键概念:带宽时延积,Bandwidth-Delay Product,简称 BDP。
BDP = 带宽 × 往返时间
它表示如果要把链路“填满”,网络中大约需要同时有多少数据在路上。
例子:
代码块收起展开
带宽: 100 Mbps
RTT: 100 msBDP = 100 Mb/s × 0.1 s = 10 Mb = 1.25 MB
如果发送窗口只有 64 KB,即使链路带宽很高,发送方也会频繁等 ACK,吞吐上不去。
发送一窗口数据 -> 等 ACK -> 再发送
窗口太小,链路中一直没有足够数据,带宽就空着。
2. 流量控制:别把接收方缓冲区塞爆
TCP 接收方有接收缓冲区。应用从 socket 读取数据之前,数据先进入这个缓冲区。
网络到达 -> 内核接收缓冲区 -> 应用 read
如果应用读取慢,缓冲区会逐渐变满。接收方通过接收窗口 rwnd 告诉发送方:
我还能接收多少字节。
发送方不能让未确认数据超过接收窗口,否则对方缓冲区可能溢出。
2.1 接收窗口变化
应用读得快:
缓冲区空闲多 -> rwnd 大 -> 发送方可以多发
应用读得慢:
缓冲区空闲少 -> rwnd 小 -> 发送方必须少发
应用长期不读:
rwnd 可能变为 0 -> 发送方暂停发送
这就是流量控制。
2.2 零窗口
当接收方窗口为 0 时,发送方不能继续发送普通数据,只能周期性探测窗口是否恢复。
常见原因:
| 原因 | 解释 |
|---|---|
| 接收应用卡住 | 数据到了内核,但应用没有读取 |
| 接收端 CPU 忙 | 来不及处理 socket |
| 缓冲区设置过小 | 高延迟高带宽场景下窗口不足 |
| 下游处理慢 | 应用读了数据,但业务处理形成反压 |
所以看到“TCP 慢”,不能只怪网络。接收方应用和缓冲区也可能是瓶颈。
3. 拥塞控制:别把中间网络打爆
网络路径由许多链路和路由设备组成。发送方不知道中间每一段的真实剩余能力,只能从反馈推断。
TCP 拥塞控制的基本思路:
没有明显拥塞时,逐步增加发送量;
检测到丢包或延迟异常时,降低发送量。
发送方维护拥塞窗口 cwnd。实际能发送的在途数据通常受两者共同限制:
可发送窗口 = min(接收窗口 rwnd, 拥塞窗口 cwnd)
rwnd 表示对端接收能力,cwnd 表示网络承载能力。
4. 慢启动:从小心试探开始
TCP 连接刚建立时,发送方不知道网络能承受多少流量,所以不会一开始就全速发送。
慢启动阶段大致是:
初始 cwnd 较小
每收到 ACK,cwnd 增长
每个 RTT 大约翻倍
示意:
RTT 1: 发送 1 份
RTT 2: 发送 2 份
RTT 3: 发送 4 份
RTT 4: 发送 8 份
…
慢启动不是“慢速传输”,而是指数增长的探测过程。它慢在一开始保守,避免突然把未知路径打爆。
5. 拥塞避免:接近上限后谨慎增长
当 cwnd 达到某个阈值 ssthresh 后,TCP 会进入拥塞避免阶段,增长速度变慢:
- 慢启动:每个 RTT 大约翻倍
- 拥塞避免:更接近线性增长
直觉是:
离网络上限很远 -> 增长快一点
接近网络上限 -> 增长谨慎一点
一旦出现丢包,TCP 会认为网络可能拥塞,于是降低 cwnd,再慢慢恢复。
不同 TCP 拥塞控制算法细节不同,但共同目标是:
尽量利用带宽,同时避免长期拥塞和大量丢包。
6. ACK 时钟:为什么 ACK 会影响发送节奏
TCP 不是无限制把所有数据一次性塞进网络。很多时候,ACK 的返回会释放窗口,让发送方继续发。
ACK 像一个节拍器。如果 RTT 很大,ACK 回来慢,发送方释放窗口也慢;如果丢包导致 ACK 异常,发送节奏会被扰乱。
这就是为什么长距离网络即使带宽高,也可能需要更大的窗口才能跑满。
7. 丢包、重传和队头阻塞
TCP 保证有序字节流。即使后面的字节已经到了,只要前面某段缺失,应用层也不能越过缺口按顺序读取完整流。
代码块收起展开
已到达: 1 2 3 _ 5 6 7
缺失: 4应用能连续读取到 3,后面的 5 6 7 要等 4 补齐
这就是 TCP 层的队头阻塞。
丢包的代价不只是“重传一个包”,还包括:
| 代价 | 解释 |
|---|---|
| 等待重传 | 缺失数据补齐前,后续有序交付受阻 |
| cwnd 下降 | 发送方认为拥塞,降低发送速率 |
| RTT 波动 | 排队和重传让延迟更不稳定 |
| 应用超时 | 上层可能等不到响应而失败 |
8. 小包问题:Nagle 与延迟 ACK
如果应用频繁发送很小的数据包,网络上会出现大量小包,头部开销高,中间设备处理压力也大。
Nagle 算法的思想是:
如果还有未确认的小数据,就先攒一攒,等 ACK 或攒到更多数据再发。
延迟 ACK 的思想是:
接收方可以稍等一会儿,看看是否能把 ACK 和返回数据一起发出。
两者有时会叠加出额外延迟:
- 发送方:我等 ACK 再发更多小数据
- 接收方:我等一小会儿再回 ACK
对交互式低延迟场景,需要理解这些机制,不能只看应用代码里的 write 调用。
9. TCP 性能现象对照表
| 现象 | 可能原因 | 检查方向 |
|---|---|---|
| 吞吐远低于带宽 | 窗口太小、RTT 大、丢包、接收方慢 | RTT、rwnd、cwnd、重传、接收缓冲区 |
| 延迟突然升高 | 队列堆积、拥塞、下游处理慢 | 排队时间、丢包、应用处理时间 |
| 重传很多 | 链路质量差、拥塞、设备丢包 | 包捕获、接口错误、路径质量 |
| 连接建立快但传输慢 | 握手没问题,数据阶段受窗口/拥塞限制 | 传输阶段指标 |
| 小请求偶发变慢 | Nagle、延迟 ACK、连接复用策略 | 小包、ACK 间隔、请求批量 |
| 接收端窗口很小 | 应用读慢或缓冲区不足 | 接收进程状态、CPU、缓冲区 |
10. 调优之前先判断瓶颈
TCP 调优很容易变成盲目改参数。更稳妥的顺序是:
- 明确慢的是连接建立、首字节、下载、上传,还是长连接稳定性。
- 测 RTT、丢包、吞吐、重传。
- 看发送端和接收端 CPU、缓冲区、读取速度。
- 判断受限于 rwnd、cwnd、应用处理,还是中间网络。
- 再考虑窗口、缓冲区、连接复用、超时等参数。
参数不是越大越好:
| 参数方向 | 风险 |
|---|---|
| 缓冲区过小 | 高 BDP 场景下吞吐跑不满 |
| 缓冲区过大 | 内存占用增加,排队延迟可能升高 |
| 超时过短 | 正常波动被误判为失败 |
| 超时过长 | 故障恢复慢,资源占用久 |
反例:缓冲区越大不一定越快
很多人看到窗口或缓冲区限制吞吐,就想把缓冲区无限调大。但缓冲区过大可能制造 bufferbloat:
发送方继续塞数据
中间队列不丢包但排得很长
吞吐看起来还行
交互延迟和 P99 变差
这时问题不是“带宽不够”,而是“队列过深”。判断方法:
| 现象 | 可能说明 |
|---|---|
| 吞吐不低但延迟升高 | 队列堆积 |
| RTT 随负载上升明显 | bufferbloat 或排队 |
| 丢包不多但交互变慢 | 队列吸收了拥塞信号 |
| 小请求被大传输拖慢 | 同一路径队列被大流量填满 |
TCP 性能调优要在吞吐和延迟之间取平衡。真正的目标不是“窗口越大越好”,而是让在途数据足够利用链路,同时不制造不可接受的排队。
手推:BDP 为什么决定“窗口够不够”
BDP 是 bandwidth-delay product,直觉是:
链路上最多能同时在路上的数据量 ≈ 带宽 × 往返时间
假设:
带宽 = 100 MB/s
RTT = 50 ms = 0.05 s
那么:
BDP = 100 MB/s × 0.05 s = 5 MB
如果发送窗口只有 512 KB,发送方很快把窗口发满,然后必须等 ACK 回来。链路中同时在飞的数据不足,带宽就吃不满。
如果窗口至少接近 5 MB,发送方才能在等待 ACK 的同时,让链路保持更充分的数据流。
这解释了一个常见现象:
低延迟局域网很快;
高 RTT 链路上同样带宽却跑不满。
因为高 RTT 意味着需要更大的在途数据窗口,才能填满链路。
边界条件:拥塞控制限制的是网络,流量控制限制的是接收方
TCP 发送速度至少受两类窗口约束:
实际可发窗口 = min(拥塞窗口, 接收窗口)
两者含义不同:
| 窗口 | 限制对象 | 典型现象 |
|---|---|---|
| 接收窗口 | 对端接收缓冲区和应用消费能力 | 对端读得慢,本端发送被压住 |
| 拥塞窗口 | 中间网络承载能力 | 丢包/延迟上升后发送节奏变保守 |
如果接收窗口很小,说明对端或应用消费跟不上;如果拥塞窗口变小,说明发送方判断网络路径可能拥塞。
排障时把这两者混在一起,会导致错调:
接收方读慢,却去调网络拥塞参数;
网络丢包严重,却只扩大应用缓冲区。
所以看 TCP 性能时,要问:发送被谁限制,是对端窗口、拥塞窗口、应用写入速度,还是本机调度/缓冲区?
11. 联系实际:为什么“同一个接口,有时快有时慢”
如果同一个请求偶发变慢,可以按时间拆分:
DNS 解析时间
TCP 连接时间
TLS 握手时间
请求发送时间
服务处理时间
首字节等待时间
响应下载时间
TCP 相关问题通常体现在:
| 阶段 | TCP 可能原因 |
|---|---|
| 连接时间长 | SYN 丢包、路径不通、握手重试 |
| 首字节等待长 | 请求到达慢、服务处理慢、响应首包丢失 |
| 下载慢 | cwnd/rwnd 限制、丢包、RTT 大、接收慢 |
| 长连接断开 | 中间设备超时、应用读写异常、RST |
不要用一句“网络慢”结束排查。要把慢拆成阶段,再把阶段映射到机制。
小实验:用 BDP 判断窗口够不够
假设链路条件是:
带宽 = 200 Mbps
RTT = 80 ms
BDP 是:
200 Mb/s * 0.08 s = 16 Mb = 2 MB
这表示如果想让单连接填满链路,网络中大约需要 2 MB 在途数据。然后比较窗口:
| 可用窗口 | 结果 |
|---|---|
| 64 KB | 远小于 BDP,单连接吞吐很难跑满 |
| 512 KB | 仍可能不够 |
| 2 MB 或更高 | 理论上更接近填满链路 |
但窗口大不是万能。如果丢包高、接收应用读得慢、队列排队严重,吞吐仍会下降,延迟还可能升高。
所以 TCP 性能排查要同时看:
RTT / BDP / rwnd / cwnd / retransmission / receiver read speed
12. 学完本章你能解决什么问题
学完本章,你应该能:
- 解释为什么带宽高不代表单连接吞吐一定高。
- 用 BDP 理解高延迟链路为什么需要更大的在途数据量。
- 区分接收窗口 rwnd 和拥塞窗口 cwnd。
- 判断传输慢可能是接收方读慢、网络拥塞、窗口不足还是丢包。
- 解释丢包为什么会导致重传、窗口下降和应用层等待。
- 理解小包、Nagle、延迟 ACK 对交互式请求的影响。
- 把“慢请求”拆成 DNS、连接、握手、处理、传输等阶段逐一定位。