NNetpath
原理预计 30 分钟

TCP 的脾气:握手、窗口、拥塞与重传

为什么带宽没跑满却觉得慢?多半是窗口、重传和排队在起作用。

学完这节你能做到

  • 解释拥塞窗口、接收窗口、慢启动与拥塞避免的关系
  • 区分快速重传与超时重传,判断哪种对业务危害更大
  • 判断重传率、RTT、缓冲区膨胀(bufferbloat)三类症状

建议先学跳过这几节会看不懂本节的部分推导

一条连接建立时的两个队列

三次握手在内核里对应两个队列,它们满了的表现完全不同:

队列存什么满了会怎样怎么看
SYN 队列(半连接)收到 SYN、还没完成握手丢 SYN 或回 SYN cookienstat -az | grep -i listendrop
accept 队列(全连接)握手完成、等应用 accept()丢连接,客户端表现为超时ss -lntRecv-Q / Send-Q
# LISTEN socket 上:Recv-Q = accept 队列当前积压,Send-Q = backlog 上限
ss -lnt
State   Recv-Q  Send-Q  Local Address:Port
LISTEN  128     128     0.0.0.0:8080

Recv-Q 顶到 Send-Q 就是 accept 队列满了。这不是网络问题,是应用 accept 得太慢—— 调 somaxconn 只能缓冲更久,治不了根。

×连接超时不等于网络不通

客户端 connect 超时,很多人第一反应是查网络。但 accept 队列满、SYN 队列满、 conntrack 表满这三种情况,症状都是"连不上"而链路完全正常。 先在服务端看 ss -lntnstat,比抓包快得多。

两个窗口,谁在限速

发送方一次能放出去多少数据,取决于两个窗口的较小值

  • rwnd(接收窗口) —— 对端说"我还能收这么多"。对端应用读得慢,它就缩小。
  • cwnd(拥塞窗口) —— 发送方自己判断"网络还能吃这么多"。丢包就砍半。
实际可发送量 = min(rwnd, cwnd) − 已发出未确认

这个式子决定了排障方向:

现象判断该去哪查
cwnd 小,rwnd 大网络在丢包,拥塞控制压着查路径丢包、重传率
rwnd 小,cwnd 大对端应用消费不过来查对端 Recv-Q 和应用
两个都大但发不快应用没喂够数据,或受 BDP 限制查应用逻辑、并发数
# 一条连接的真实状态:cwnd、rtt、重传、发送速率全在这
ss -tim dst 10.10.1.103
ESTAB 0 1173600  10.10.0.51:44212  10.10.1.103:8080
   cubic wscale:7,7 rto:204 rtt:3.6/0.5 mss:1448 cwnd:210
   bytes_sent:8419283 bytes_retrans:12984 retrans:0/9
   send 675Mbps  pacing_rate 810Mbps  delivery_rate 640Mbps

读法:cwnd:210 × mss:1448 ≈ 304 KB 在途上限;retrans:0/9 是"当前 0 次、累计 9 次"; delivery_rate 是实测送达速率,比 send 更可信。

慢启动、拥塞避免与算法选择

cwnd
 │        ┌── 拥塞避免(线性增长)
 │       /
 │      /  ← 丢包,cwnd 砍半
 │     /╲
 │    /  ╲___/‾‾‾
 │   / 慢启动(指数增长)
 └──────────────────────► 时间
  • 慢启动:每收到一个 ACK 就涨,指数上升,直到丢包或达到阈值
  • 拥塞避免:转为线性缓增,试探极限
  • 丢包:Cubic 之类的算法把 cwnd 砍掉一部分,重新往上爬

算法的差别在于"用什么信号判断拥塞":

算法判断依据适合
Cubic(默认)丢包数据中心内、常规场景
BBR实测带宽与最小 RTT长肥管道、有随机丢包的链路

BBR 不等丢包才降速,所以在跨地域链路上通常明显更快。但它在同机房短 RTT 场景下未必有优势, 换算法要用业务流量实测,别照抄结论

sysctl net.ipv4.tcp_congestion_control     # 当前算法
sysctl net.ipv4.tcp_available_congestion_control

重传:两种,危害差一个数量级

类型触发代价
快速重传收到 3 个重复 ACK(或 SACK 指明空洞)几个 RTT,业务基本无感
RTO 超时重传定时器到点还没收到 ACK最少 200 ms 起步,业务一定有感知

Linux 的 TCP_RTO_MIN 是 200 ms。所以一次超时重传就等于一次 200 ms 的卡顿, 在要求 P99 延迟的场景里这是致命的。

# 重传总量与分类
nstat -az | grep -iE 'RetransSegs|TCPLostRetransmit|TCPTimeouts|TCPFastRetrans'

# 每秒重传率:retrans/s ÷ oseg/s
sar -n ETCP 1 5

判断口径:持续超过 0.1% 就该查,超过 1% 业务一定有感知

!重传率不是丢包率

重传率是"发送方认为需要重发的比例",它会被乱序、ACK 丢失、 延迟确认放大。看到 0.5% 重传不代表链路真丢了 0.5%, 要结合 ethtool -S 的网卡计数和交换机端口计数一起判断。

Bufferbloat:带宽正常,延迟爆炸

一个反直觉的现象:把队列做得很深,丢包变少了,但延迟变得极差。

因为 TCP 靠丢包判断拥塞。队列深 → 迟迟不丢包 → 发送方一直加速 → 数据全堆在队列里排队 → 带宽还是那么多,但每个包多等了几十毫秒

症状识别:

# 空载时的 RTT
ping -c 10 10.10.1.103          # rtt min/avg/max = 0.1/0.12/0.15 ms

# 满载时再量一次
ping -c 10 10.10.1.103          # rtt min/avg/max = 0.1/45/120 ms  ← 排队

空载延迟正常、满载延迟涨几百倍,就是典型的 bufferbloat。解法是用带 AQM 的 qdisc (fq_codelcake)主动早丢一点,而不是把队列继续加深。

检查点

检查点单选

ss -tim 显示某条连接 cwnd 很小、rtt 正常、retrans 累计很高。瓶颈最可能在哪?

检查点单选

业务 P99 延迟偶发跳到 200 ms 以上,平均延迟正常。最先该查什么?

检查点多选

关于 accept 队列,下面哪些说法正确?(多选)

这节课的落点

  • 握手对应两个队列:SYN 队列与 accept 队列,满了都表现为"连不上"但都不是链路问题
  • 发送量 = min(rwnd, cwnd);哪个小就往哪个方向查
  • ss -tim 一条命令能看到 cwnd、rtt、重传、delivery_rate
  • 快速重传影响小,RTO 超时重传最少 200 ms 起步,是 P99 尖刺的头号嫌疑
  • 重传率 0.1% 是警戒线,1% 业务必有感知;但重传率 ≠ 丢包率
  • 空载延迟正常、满载延迟暴涨 = bufferbloat,用 AQM 而不是加深队列

延伸资料

  • ·Systems Performance, 2nd Edition — Brendan Gregg