tcp - flow - congestion

07. TCP 流量控制、拥塞控制与性能

0. 本章先解决什么问题

理解 TCP 基础以后,还会遇到一个更现实的问题:

连接已经建立,数据也没有完全丢,为什么传输还是慢?

答案通常不在“TCP 可不可靠”,而在三个限制:

  1. 接收方能不能处理得过来。
  2. 网络路径能不能承受这么多在途数据。
  3. 发送方有没有把链路填满,又有没有造成排队和丢包。

本章讨论 TCP 的两个核心控制机制:

机制控制对象目标
流量控制接收方缓冲区和处理能力不把对端撑爆
拥塞控制中间网络路径不把网络打爆

然后把它们和真实性能现象联系起来:吞吐低、延迟高、重传多、窗口小、长距离传输慢。

TCP 流量控制与拥塞窗口

这张图怎么读

这张图把 TCP 数据阶段的核心约束画出来:发送方真正能放在路上的数据量受 min(rwnd, cwnd) 限制。rwnd 代表接收方还能接多少,cwnd 代表发送方估计网络路径还能承受多少。

1. 发送速度不是只由带宽决定

很多人看到“带宽 100 Mbps”就以为应用可以稳定传 100 Mbps。真实吞吐还受 RTT、窗口、丢包、接收能力、拥塞控制影响。

先看一个关键概念:带宽时延积,Bandwidth-Delay Product,简称 BDP。

BDP = 带宽 × 往返时间

它表示如果要把链路“填满”,网络中大约需要同时有多少数据在路上。

例子:

代码块PLAINTEXT · 2 行收起展开
带宽: 100 Mbps
RTT:  100 ms

BDP = 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 时钟:发送方发出一批数据,接收方回 ACK 释放窗口,发送方才继续发新数据

ACK 像一个节拍器。如果 RTT 很大,ACK 回来慢,发送方释放窗口也慢;如果丢包导致 ACK 异常,发送节奏会被扰乱。

这就是为什么长距离网络即使带宽高,也可能需要更大的窗口才能跑满。

7. 丢包、重传和队头阻塞

TCP 保证有序字节流。即使后面的字节已经到了,只要前面某段缺失,应用层也不能越过缺口按顺序读取完整流。

代码块PLAINTEXT · 2 行收起展开
已到达: 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 调优很容易变成盲目改参数。更稳妥的顺序是:

  1. 明确慢的是连接建立、首字节、下载、上传,还是长连接稳定性。
  2. 测 RTT、丢包、吞吐、重传。
  3. 看发送端和接收端 CPU、缓冲区、读取速度。
  4. 判断受限于 rwnd、cwnd、应用处理,还是中间网络。
  5. 再考虑窗口、缓冲区、连接复用、超时等参数。

参数不是越大越好:

参数方向风险
缓冲区过小高 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. 学完本章你能解决什么问题

学完本章,你应该能:

  1. 解释为什么带宽高不代表单连接吞吐一定高。
  2. 用 BDP 理解高延迟链路为什么需要更大的在途数据量。
  3. 区分接收窗口 rwnd 和拥塞窗口 cwnd。
  4. 判断传输慢可能是接收方读慢、网络拥塞、窗口不足还是丢包。
  5. 解释丢包为什么会导致重传、窗口下降和应用层等待。
  6. 理解小包、Nagle、延迟 ACK 对交互式请求的影响。
  7. 把“慢请求”拆成 DNS、连接、握手、处理、传输等阶段逐一定位。

延伸阅读