TCP 的脾气:握手、窗口、拥塞与重传
为什么带宽没跑满却觉得慢?多半是窗口、重传和排队在起作用。
学完这节你能做到
- 解释拥塞窗口、接收窗口、慢启动与拥塞避免的关系
- 区分快速重传与超时重传,判断哪种对业务危害更大
- 判断重传率、RTT、缓冲区膨胀(bufferbloat)三类症状
建议先学跳过这几节会看不懂本节的部分推导
一条连接建立时的两个队列
三次握手在内核里对应两个队列,它们满了的表现完全不同:
| 队列 | 存什么 | 满了会怎样 | 怎么看 |
|---|---|---|---|
| SYN 队列(半连接) | 收到 SYN、还没完成握手 | 丢 SYN 或回 SYN cookie | nstat -az | grep -i listendrop |
| accept 队列(全连接) | 握手完成、等应用 accept() | 丢连接,客户端表现为超时 | ss -lnt 的 Recv-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 -lnt 和 nstat,比抓包快得多。
两个窗口,谁在限速
发送方一次能放出去多少数据,取决于两个窗口的较小值:
- 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_codel、cake)主动早丢一点,而不是把队列继续加深。
检查点
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