TCP拥塞窗口验证实战:从原理到抓包排查慢传输

发布时间:2026/10/14 8:05:24
TCP拥塞窗口验证实战:从原理到抓包排查慢传输
有一次同事跑来找我说一个跨机房的文件同步服务越来越慢3GB 的文件传了一个多钟头还没结束。我第一反应是带宽问题结果用打流工具一测链路利用率不到 10%CPU、内存、丢包率全部正常。后来在发送端扫了一眼内核的 TCP 状态问题忽然就清楚了——拥塞窗口一直打不开传一段就被打回原形。从那天起我排查 TCP 性能问题养成一个固定习惯先做拥塞窗口验证再谈优化。这篇文章把我平时验证拥塞窗口的完整流程整理出来从原理、环境搭建、抓包实操到常见坑一步一步给你拆开。1. 拥塞窗口验证前必须搞懂的三件事1.1 三个窗口cwnd、rwnd、发送缓冲区谁说了算很多人提起 TCP 性能就想到拥塞窗口但实际上一个连接能跑多快从来不是单独由 cwnd 决定的。从发送端的视角看最终发送速度同时受三样东西约束拥塞窗口 cwnd、接收窗口 rwnd以及发送缓冲区的大小。拥塞窗口是发送端自己维护的一个变量代表“我基于当前网络状况判断可以在没有得到确认之前发出的数据量”。这个变量在 Linux 内核里以 MSS 为基本单位来计数但 ss 等工具显示时有的版本直接给 MSS 数有的版本换算成字节看输出时要注意区分。接收窗口则来自对端的 TCP 报文头表示“接收端还有多少缓冲区能收数据”。发送缓冲区是本地内核为这条 socket 预留的内存如果应用写数据的速度跟不上缓冲区空了一样发不出去。最终生效的发送窗口大致等于这三个值中的最小值。可以简单类比成水管系统cwnd 是你自己认为上游水库能供多少水rwnd 是下游蓄水池还有多少空位发送缓冲区是水泵的入口池子。任何一个环节卡住整条水流都会放缓。验证拥塞窗口时如果只看 cwnd 本身很容易被误导所以我会先讲清楚这三者的关系后面的排错才不会纸上谈兵。1.2 窗口增长的几条经典规则慢启动、拥塞避免、快速恢复、超时重传传统 TCP 拥塞控制的核心规则归结起来大概四条慢启动阶段每收到一个 ACKcwnd 大约增加一个 MSS所以每个 RTT 内窗口近似翻倍呈指数增长。拥塞避免阶段当 cwnd 达到 ssthresh 后每经过一个 RTTcwnd 只增加一个 MSS线性增长。快速重传与快速恢复发送端收到三个重复 ACK 时判定网络丢包此时 ssthresh 被减为当前 cwnd 的一半cwnd 调整为 ssthresh 加三个 MSS随后继续增长。超时重传如果等不到 ACK 直到 RTO 超时系统会认为网络严重拥塞cwnd 直接被压到一个 MSSssthresh 减半重新执行慢启动。慢启动的“指数增长”很容易给人造成误解仿佛窗口真的可以无限翻倍。实际上它只是在一个 RTT 内把发送量翻一番而 RTT 受物理链路限制所以真实表现是吞吐率在若干轮之后呈现明显的阶梯爬坡。超过 ssthresh 后 A1MD 行为开始起作用曲线从陡峭变成平缓。验证拥塞窗口时观察这种“先快速翻倍、后线性爬坡”的形态比单独看某个数值更可靠。1.3 为什么要费力验证拥塞窗口而不是直接交给内核我见过不少同事遇到 TCP 传输慢第一反应是加带宽、调 MTU、换硬件一顿操作后问题依旧。原因在于很多慢的真正瓶颈不在带宽而在拥塞窗口能否正常打开。一个跨机房链路的物理带宽可能高达数百兆但单条 TCP 流的拥塞窗口如果一直被压着实际吞吐可能只有几兆链路上大量空闲。验证拥塞窗口的意义主要有三点一是确认 cwnd 的变化是否符合当前拥塞控制算法应有的行为二是判断丢包、延迟、接收窗口、本地缓冲区到底哪一环在拖后腿三是在调优之后能够给出前后对比数据而不是靠“感觉快了”。如果不做验证你永远不会知道一个被压到初始值的窗口白白浪费了多少带宽也不知道是网络丢包还是内核参数导致的问题。2. 验证环境怎么搭从一台机器到两台机器2.1 轻量方案用 network namespace 模拟两台主机最省事的验证方式是在一台 Linux 机器上用 network namespace 加 veth 虚拟网卡搭两条隔离的网络栈。这个方案的优点是快速、可控、不需要额外硬件而且非常适合在本地复现丢包和延迟场景。创建两个命名空间并连通参考以下步骤ip netns add server ip netns add client ip link add veth0 type veth peer name veth1 ip link set veth0 netns server ip link set veth1 netns client ip netns exec server ip addr add 10.0.0.1/24 dev veth0 ip netns exec client ip addr add 10.0.0.2/24 dev veth1 ip netns exec server ip link set veth0 up ip netns exec client ip link set veth1 up然后给 veth0 所在网卡加延迟和丢包模拟真实网络环境ip netns exec server tc qdisc add dev veth0 root netem delay 50ms loss 3%这样一个“服务端”和“客户端”就组成了流量只会经过 veth 设备不会影响同机其他网络连接。在客户端命名空间里跑 iperf3、tcpdump、ss就能在发送端直接观察 cwnd。需要注意tc 的 netem 丢包默认是随机丢包丢包率设太大会导致拥塞控制永远停留在初始窗口附近不方便观察慢启动的爬坡过程验证时要分场景调整。2.2 贴近线上两台真实主机测端到端行为namespace 方案适合验证算法行为但要评估真实链路时还是建议找两台同一网段的测试主机或者至少是同机房的两台虚机。网络路径越接近线上验证结果越有参考价值。真实环境里的验证流程也很简单在服务端启动 iperf3客户端发起单线程 TCP 流持续 30 秒左右。与此同时在客户端实时查看 ss -ti 输出在服务端或链路中间节点抓包。为了避免干扰建议不要在业务高峰做这个测试也不要只跑一次就下结论至少要测三遍。还有一个容易踩的坑两台机器间如果有中间网络设备或防火墙执行了限速或整形即使拥塞窗口正常吞吐也上不去。这时不要急着怀疑拥塞窗口而是要结合整条链路的丢包和延迟特征来判断必要时分段做 iperf3 测试。2.3 工具链与观测指标ss、iperf3、tcpdump 各自盯着什么验证拥塞窗口不是靠某一个工具就能完成的我通常把工具分成三个角色ss直接观察发送端 cwnd、ssthresh、RTT 和重传次数这是最接近内核状态的信息。iperf3制造持续的流量负载同时给出一段时间内的吞吐量曲线。tcpdump Wireshark抓取报文分析 ACK 间隔、重复 ACK、重传行为从网络视角验证窗口变化的外在表现。我曾经只依赖 Wireshark 抓包判断拥塞窗口结果发现它显示的滑动窗口完全是接收窗口和 cwnd 完全是两码事后面会专门讲这个坑。比较合理的组合是先用 ss 看发送端内部状态再用 tcpdump 抓一段包做精细分析最后用 iperf3 的吞吐结果来印证整体性能。三份数据互相验证才不会误判。工具观察重点关键字段或图形ss -ti发送端 cwnd、ssthresh、RTT、重传统计cwnd: xx ssthresh: xx retrans: xxiperf3单流吞吐量、重传计数、时间曲线Transfer、Bitrate、Retrtcpdump重复 ACK、快速重传、RTO、ACK 节奏Flags、seq/ack、时间戳Wireshark综合流量分析和图形化吞吐趋势I/O Graph、Streams 统计3. 三种场景下的拥塞窗口实操实录3.1 场景 A没有丢包时慢启动的爬坡过程先在命名空间里把 netem 只配延迟、不配丢包比如延迟 30ms然后启动 iperf3 客户端打流。此时发送端 cwnd 几乎每个 RTT 翻一倍从初始窗口一直爬到 ssthresh 附近再转为线性增长。在客户端不断执行 ss -ti 能看到类似下面的输出数值会随版本和环境变化重点看趋势cwnd:20 ssthresh:640 bytes_acked: 1000 rtt:29.8 cwnd:40 ssthresh:640 bytes_acked: 2000 rtt:30.1 cwnd:80 ssthresh:640 bytes_acked: 4000 rtt:30.2注意第一秒窗口数值快速翻倍但到达 ssthresh 后变化变得缓慢每个 RTT 只增加一点点。这正是慢启动和拥塞避免的分界点。实际传输中如果文件本身不大连接可能在慢启动阶段就结束了根本到不了拥塞避免阶段这也是小文件传输很难跑满带宽的原因之一。观察这个场景时建议同时记录 iperf3 的实时速率。你会发现速率曲线不是一条直线而是一个一个台阶往上跳台阶的宽度大约就是一两个 RTT。这就是拥塞窗口验证里最有代表性的外在特征。3.2 场景 B人为丢包后窗口收缩与恢复的锯齿给命名空间里的网卡配置 3% 丢包延迟保持 30ms然后重新跑一次传输。这一次 cwnd 不会稳定爬升而是会在某个值附近反复波动。抓包或者看 ss -ti 输出会发现几个典型信号出现重复 ACK、发送端进入快速重传、ssthresh 快速下调。如果丢包比较严重还会看到 RTO 超时cwnd 直接被压回一个 MSS。整体速率曲线变成锯齿形先在慢启动或拥塞避免阶段爬升丢包一来立刻塌方然后再爬起来。这里值得留意的是CUBIC 算法在丢包后并不会马上回到极小的窗口而是会在 ssthresh 减半的基础上利用三次函数快速爬回原来的位置所以表现为“快速塌陷快速恢复”。如果你在验证时看到窗口塌下去之后恢复得很慢可能是 ssthresh 被设置得过于保守或者配置了其他基于丢包更敏感的算法。3.3 场景 CCUBIC 与 BBR两种算法的窗口形态差异现代 Linux 系统一般默认使用 CUBIC但验证拥塞窗口时非常建议顺手对比一下 BBR。先查看当前算法sysctl net.ipv4.tcp_congestion_control切换为 BBR需要保证内核模块已加载modprobe tcp_bbr sysctl -w net.ipv4.tcp_congestion_controlbbr对比两种算法在同样丢包条件下的行为会发现BBR 的 cwnd 波动远比 CUBIC 小传输速率也更平稳。这不是说 BBR 没有拥塞控制而是它的核心思路不是靠丢包来“刹车”而是通过持续探测带宽和最小 RTT 计算发送速率。所以当链路有少量丢包时BBR 的 cwnd 可能依然维持在较高水平从 ss -ti 里看更像一条直线。验证这类差异时不要把注意力完全放在 cwnd 这个数字上。BBR 实际发送节奏由 pacing rate 控制cwnd 更像一个上限约束。只看 cwnd 容易得出“BBR 不响应拥塞”的片面结论。我的习惯是同时观察实际吞吐、带宽占用曲线和 RTT 变化综合判断算法是否生效。4. 抓包数据怎么解读从报文里反推拥塞窗口4.1 为什么报文里永远找不到 cwnd 字段很多第一次做抓包分析的人会在 Wireshark 的报文列表里找“CWND”这个东西结果一无所获。原因是拥塞窗口只在发送端的内核内存里存在它不会写进 TCP 报文的头部对端也无从感知。TCP 报文头部里只有序号、确认号、接收窗口、Flags、时间戳这一类字段没有 cwnd。所以验证 cwnd 有两种途径要么在发送端直接读内核状态也就是 ss -ti 这类接口要么通过报文序列号的增长节奏来间接推测。第二种方式做起来稍复杂但可以在没有发送端权限时派上用场比如你只能拿到链路镜像或中间设备抓包时。4.2 用吞吐的“阶梯”反推窗口变化在 Wireshark 里打开抓包文件先看 I/O Graph横轴时间、纵轴每秒字节数或包数。如果拥塞窗口在正常慢启动图形呈现出明显的阶梯状上升每级约一倍如果拥塞窗口进入拥塞避免图形变成平缓上升斜率接近线性如果出现丢包和重传图形会出现突然的下坠再拉起。更细一步可以按 RTT 切分统计每个 RTT 时间段内发送端发出的数据量。由于每个 ACK 的到达会触发窗口滑动发送端在一个 RTT 内能发出的数据量基本等于当前 cwnd。把这些数据量连成一条曲线就相当于把 cwnd 的动态变化从抓包文件里还原出来了。这个方法不需要访问发送端也能判断窗口是卡住了还是在正常增长。4.3 别把接收窗口和零窗口当成拥塞窗口抓包工具里显示“Window”字段指的是接收窗口 rwnd来源是对端通告和发送端的 cwnd 没有任何直接关系。常见的误判是看到某个报文的接收窗口为 0立刻以为“拥塞窗口被压到 0”。其实零窗口只说明接收端的应用层没及时读取数据或者接收缓冲区满了它触发的是窗口探测机制不是拥塞控制。判断拥塞窗口是否收缩更可靠的信号是重传行为出现大量重复 ACK 或 RTO 超时再结合发送端 ss -ti 的 cwnd 数值变化。如果只有 rwnd 为零而 cwnd 数值很大则优先检查接收端应用的服务能力而不是链路本身。5. 常见问题与排查技巧实录5.1 拥塞窗口始终不涨卡在初始值附近这类现象在加上 netem 丢包后特别容易复现。原因往往是拥塞信号过于频繁cwnd 每次刚涨一点就被丢包打回去。排查时先看 ss -ti 里的 retrans 计数再数一下抓包文件里重复 ACK 和 RTO 的比例。如果重传非常多问题基本锁定在链路质量而不是算法。另一个容易忽略的机制叫拥塞窗口验证Congestion Window ValidationCWV这是 TCP 针对连接空闲时窗口可能过时的一种安全机制。连接空闲超过一定时间后内核会重新评估甚至缩小 cwnd避免把旧状态带到新网络环境下。线上业务如果出现“长期空闲后突然大流量传输速度却上不去”的现象很可能就是 CWV 在起作用此时 cwnd 会从初始值开始重新爬坡看上去像一直没打开实际是“重新验证”了。5.2 iperf3 单线程跑不上去多线程却可以如果 iperf3 用单线程测试吞吐远低于链路带宽而加 -P 参数多线程并发立刻跑满说明链路本身没问题瓶颈大概率就在单条 TCP 流的拥塞窗口上。验证方法是先确认真实的 BDP也就是带宽时延积。如果链路的 RTT 很大即使 cwnd 正常单流吞吐也会受限于“窗口除以 RTT”的理论上限。这时候要判断是网络丢包导致窗口收缩还是线性增长还没爬到足够大。如果确认是长肥网络可以尝试调大初始窗口或者改用 BBR 这类不依赖丢包信号的算法。但调参之前一定要保留修改前后的 cwnd 轨迹否则很难说服团队接受调整。5.3 改了拥塞控制算法ss 里看不到变化最简单的原因是连接已经建立内核不会把已有 TCP 连接的拥塞控制算法动态切换新算法只对新建连接生效。验证时先确认正在观察的连接是不是新建的或者直接重启服务再打流。另外还要检查 sysctl 修改是否在正确的网络命名空间里执行因为在 namespace 实验环境中系统全局的 sysctl 不一定会影响已创建的 netns。如果确认算法已经切换可以看 ss -ti 输出里是否出现算法名字比如 CUBIC、BBR 之类。看不到名字时可以用 /proc/net/tcp 或直接对比窗口曲线形态来间接判断。5.4 一套快速排查的框架思路我个人做拥塞窗口验证时基本遵循这个套路先确认实验场景是单流还是多流再从发送端读 ss -ti 拿 cwnd、ssthresh、retrans 的基线然后抓包看重复 ACK、RTO、接收窗口通告最后结合 iperf3 的吞吐曲线判断窗口行为是否和算法模型匹配。如果 cwnd 曲线和理论行为一致说明网络和内核没有问题性能上不去要检查应用和并发如果曲线异常再深入分析丢包、延迟、缓冲区或中间设备。现象常见原因验证方法建议方向窗口卡初始值频繁丢包、CWV 重新验证检查重传计数、抓包看 RTO优化链路考虑 BBR 或调大 initcwnd单流吞吐低、多流正常cwnd 不够大或 RTT 过大计算 BDP对比多流结果增大窗口或启用 BBRrwnd 小但 cwnd 大接收端应用未及时读取看接收端 socket 缓冲区和应用日志优化接收端消费速度改算法不生效已有连接未重建、netns 隔离确认连接建立时间、检查 sysctl 作用范围重启连接测试踩过几次坑之后我的体会是验证拥塞窗口这事不能指望一个命令给出全部答案。ss 告诉你内核怎么想抓包告诉你网络发生了什么iperf3 告诉你实际体验如何。三个维度对齐了问题定位才靠谱。如果你线上条件不允许抓包至少把 ss -ti 的输出留一份再配合应用端的速率日志已经能解决很大一部分“TCP 很慢”的疑难杂症了。