一、船多了堵,船少了亏

大运河从杭州到通州,一千八百余里。河道宽的地方能并排走六条漕船,窄的地方——尤其是山东段的十二道闸口——一次只能过一条。

管山东段漕运的闸官姓柳,人称柳闸官。他手里管着十二道闸,每天有成百条漕船从他面前过——运粮的、运盐的、运铜的、运贡品的。

柳闸官有一本难念的经。

船放少了,京城等着粮,户部发文催。"你山东段一天才过八十条船?后面的船排到徐州了!"

船放多了,全堵在闸口。挤。挤到后面,撞船。一撞船,粮食落水,盐包泡汤。柳闸官得赔。

柳闸官试过各种法子——定死数、看时辰放、看船型放——都不好使。因为河不是死的。春天涨水,河道宽,放得密也不堵。冬天枯水,放得稀还能撞。

他的徒弟阿漕问:"师父,到底一次放几条?"

柳闸官摇头:"我现在只能猜。"

二、先放一条试试

有一天,柳闸官在闸口坐了一整天,看着来来往往的漕船,忽然想到了一个法子。

"阿漕,"他说,"我们不猜了。我们试。"

"怎么试?"

柳闸官指着闸口:"你看,上游排队等着过闸的船有上百条。我把闸门一开,一次放几条出去。它们从这道闸走到下一道闸,再返回来报个信——来回一趟,叫一轮。"

"一轮过后,回来的船都平安——说明河道不挤。一轮过后,有船没回来——说明河道挤了。"

阿漕问:"那第一轮放几条?"

"一条。"

"一条?"阿漕急了,"那后面排着的船要等到什么时候?"

"你先听我说完。"柳闸官蹲下来,在泥地上画了一个数字:1

"第一轮,放一条。这条船回来报平安了——说明河道通着,至少还能再加。第二轮,放两条。两条都回来了?第三轮,放四条。四条都回来了?第四轮,放八条。"

阿漕看着地上的数字:1、2、4、8、16……

"这叫'加倍放'。"柳闸官说,"一开始慢慢探路,探到了河道的底,再加就不是这么个加法了。"

"那加倍加到什么时候停?"

"加到有船没回来为止。有一条没回来——闸口就得马上收。"

三、出了事砍一半,不出事加一条

头三天,柳闸官照新规矩放船,顺得很。

第一轮一条,回来了。第二轮两条,回来了。第三轮四条,回来了。第四轮八条,也回来了。第五轮——柳闸官正要放十六条,阿漕拦住了。

"师父,十六是不是太快了?上一轮才放八条。"

柳闸官想了想:"你说得对。加倍放到一定程度就不能再加倍了——太快了容易翻船。我们定一个'加倍线'。到了这条线,就别加倍了,改成'每次多放一条'。"

他把"加倍线"定在了十六条。低于十六条,加倍放;到了十六条,每轮只多放一条——十七、十八、十九。

这叫"慢加"。柳闸官想,河的脾气变得快,离容量上限越近,越要小心探。

少一条不碍事。翻一条赔大了。

第四天出事了。

柳闸官放出了二十四条船,回来二十三条——有一条没回来。下游的闸官遣人来报:船在窄口翻了。

阿漕紧张地看着师父。

柳闸官沉吟片刻,拿起朱笔,把"二十四"划掉,在旁边写了一个十二

"砍一半。"他说。

"砍一半?!"阿漕急了,"只翻了一条船,你砍一半?那后头排着的船不更堵了?"

"翻一条船,说明河道已经吃紧了。"柳闸官说,"吃紧的时候还加?那不是往火里添柴?砍一半——从一半处重新慢慢加。这叫'出事砍半'。"

阿漕还是不理解。

"你想想,"柳闸官指着河水,"一条船翻了,它后头跟着的船会不会也翻?你现在还不知道河到底窄了多少、下头堵了多少。与其接着赌,不如先缩回来——从十二开始,慢慢再往上加。加到发现新的翻船点,再砍半。"

阿漕不说话了。

新规矩上了。十二、十三、十四……柳闸官在加倍线之内,每次都多放一条。放到第十九条的时候,又翻了一条。他提笔,又砍一半——九条。重新来。

阿漕在旁边看着,忽然想通了一件事。

"师父,你这套法子,翻船砍一半、平安加一条——翻船的时候收得快,平安的时候加得慢?"

"对。"柳闸官说,"收要快,加要慢。河好了,慢慢试探。河坏了,立刻缩手。"

四、放的不是船,是"不知道"

一个月后,柳闸官拿出了他记的船次簿。

簿子上密密麻麻记着:每天每轮放几条、回来几条、什么时候砍半、什么时候慢加。

阿漕翻着簿子,发现一个规律:"师父你看——砍半的日子,和加倍的日子,差不多各占一半。"

"对。"

"那是不是说……河有一半时间是'不对'的?"

"不是河不对。"柳闸官说,"是你永远不知道下一秒的河是什么样的。你的法子不能假设'河是对的'。你得假设'我不知道河接下来会怎样'。"

阿漕愣住了。

柳闸官继续说:"加倍放,是在'不知道河有多宽'的时候,用最快速度找到边界。慢加,是在'离边界不远了'的时候,小心翼翼地多探一步。砍半,是在'河已经翻脸了'的时候,赶紧缩手。"

他看着那条沉在暮色里的运河,说了最后一句话:

"放的不是船,是不知道。不知道的时候,先小步探,再大步走。知道坏了,立刻收。"

技术解读

TCP 拥塞控制(TCP Congestion Control)是互联网可靠传输的基石。没有它,网络会在流量高峰时陷入"拥塞崩溃"——数据包大量丢失,吞吐量骤降至零。1986 年,互联网经历了第一次拥塞崩溃,促使 Van Jacobson 在 1988 年发表经典论文《Congestion Avoidance and Control》,提出了 TCP Tahoe——现代拥塞控制的起点。此后,TCP Reno、NewReno、CUBIC、BBR 等算法不断改进,但核心思想一脉相承。

拥塞控制的本质问题:发送方不知道网络的可用容量,必须通过"探测—反馈—调整"的闭环来逼近最优发送速率。这就像在黑暗中摸索一个不断移动的天花板——既要尽可能利用带宽,又不能把网络压垮。

核心概念回顾

概念 通俗解释
拥塞窗口(cwnd) 发送方允许自己在未收到确认的情况下最多发送的数据包数量(以 MSS 为单位)
慢启动 初始 cwnd 很小(通常 1-10 MSS),每收到一个 ACK,cwnd 增加 1——每个 RTT 内 cwnd 翻倍(指数增长)
慢启动阈值(ssthresh) 慢启动和拥塞避免之间的分界线。cwnd < ssthresh 时指数增长,cwnd ≥ ssthresh 时线性增长
拥塞避免 当 cwnd ≥ ssthresh 后,每个 RTT 只将 cwnd 增加 1(加性增),缓慢探测剩余带宽
AIMD Additive Increase, Multiplicative Decrease:加性增(每 RTT +1)、乘性减(丢包时 cwnd 减半)
超时重传(RTO) 发送方在规定时间内未收到 ACK,判定丢包,将 cwnd 重置为 1 并重传丢失数据
快速重传 / 快速恢复 TCP Reno 的改进:收到 3 个重复 ACK 即判定丢包,不等待超时,立即重传;cwnd 减半进入快速恢复而非重置为 1
RTT 往返时间——数据包从发送方到接收方再返回 ACK 的时间,是拥塞控制的时钟

故事中的隐喻对照

故事元素 映射的技术概念 解释
运河 网络路径(Network Path) 数据包传输的物理/逻辑链路,有有限的带宽和变化的延迟
漕船 TCP 数据包(Segment) 每个数据包承载一段数据,通过 ACK 确认到达
闸官柳闸官 TCP 拥塞控制算法 决定何时、以何种速率发送数据包的逻辑
船回来报平安 ACK(确认包) 接收方确认收到数据包,发送方据此判断"网络通畅"
船没回来(翻了) 丢包(Packet Loss) 数据包在网络中被丢弃,是拥塞的主要信号
"一轮"(放一批船,等下一闸回报) RTT(往返时间) 一批数据包发送出去到收到 ACK 的时间间隔,决定了窗口增长的时钟节奏
第一轮放 1 条 慢启动初始 cwnd = 1 TCP 连接初始拥塞窗口很小,先探路再加速
加倍放:1→2→4→8→16 慢启动:指数增长 每个 RTT 将 cwnd 翻倍,快速探测可用带宽
"加倍线"(十六条) ssthresh(慢启动阈值) 超过此阈值后切换为线性增长(拥塞避免),避免指数增长在容量上限附近引发大量丢包
"每次多放一条"(十六→十七→十八) 拥塞避免:加性增 每个 RTT cwnd += 1,保守地探测剩余带宽
翻船后"砍一半"(二十四→十二) 乘性减(Multiplicative Decrease) 检测到丢包时 cwnd 减半,迅速降低负载以缓解拥塞
"收要快,加要慢" AIMD 的非对称性 检测到拥塞时迅速响应(乘性减),探测带宽时缓慢递增(加性增)——这是 TCP 公平性的基础
砍半后从十二重新慢加 快速恢复后进入拥塞避免 cwnd 减半后从新值开始线性增长,而非重头慢启动
加倍和砍半"差不多各占一半" TCP 的锯齿形行为 cwnd 在长期运行中呈现不断的慢启动/加性增→拥塞→乘性减→加性增的锯齿模式
枯水期 vs 涨水期 网络带宽的动态变化 可用带宽随其他流量、链路状态变化而变化,拥塞控制必须适应动态环境
船的排队 发送缓冲区 / 排队延迟 待发送的数据包在发送方缓冲,等待拥塞窗口允许发送
"放的不是船,是不知道" 拥塞控制的本质:探测未知 发送方不掌握网络容量的先验信息,必须通过探测—反馈循环来学习

为什么这个故事对应 TCP 拥塞控制?

  1. "先放一条试试"是慢启动的起点。 TCP 连接建立后,发送方不知道网络容量,初始 cwnd 很小(在 Linux 中通常是 10 MSS)。慢启动的设计哲学是"先保守探路,确认安全再加速"。

  2. "加倍放"精确对应慢启动的指数增长。 每个 RTT cwnd 翻倍——这是 TCP 能够在几十毫秒内从 1 MSS 冲到数百 MSS 的关键机制。指数增长让 TCP 快速"找到"可用带宽的上界。

  3. "加倍线"(ssthresh)是慢启动和拥塞避免的切换点。 首次 ssthresh 通常设为一个很大的值,首次丢包后 ssthresh 被设为 cwnd/2。之后,cwnd 在 ssthresh 以下是慢启动(指数),到了 ssthresh 就是拥塞避免(线性)。

  4. "每次多放一条"是加性增(Additive Increase)。 在拥塞避免阶段,每个 RTT cwnd 仅增加 1 MSS。这非常保守,但正因为保守,TCP 才能在拥塞边缘稳定运行而不引发大规模丢包。

  5. "翻一条船砍一半"是乘性减(Multiplicative Decrease)。 这是 AIMD 中最激进的部分——一旦检测到拥塞信号(丢包),立即将发送速率减半。AIMD 的非对称性(慢加速、急刹车)经过数学证明是实现公平带宽分配的必要条件。

  6. "收要快,加要慢"道出了 AIMD 的根本智慧。 乘性减确保拥塞快速缓解——如果所有连接同时减半,总负载立即减半。加性增确保各连接公平收敛到均分带宽——慢速增长给了系统时间达到平衡。

  7. "放的不是船,是不知道"是拥塞控制的哲学核心。 发送方对网络状态的认知永远是滞后的、不完整的。拥塞控制本质上是一个在不确定环境中做决策的控制系统——每个 ACK 减少一点不确定性,每次丢包提醒你"边界到了"。

后记:TCP 拥塞控制可能是互联网中最优雅的反馈控制算法之一。它的数学原理(AIMD 收敛到公平分配)和工程智慧(慢启动快速探测、拥塞避免保守试探)结合得天衣无缝。Van Jacobson 在 1988 年的论文只有 25 页,但它挽救了一次互联网的拥塞崩溃,并定义了此后三十年网络传输的基本节奏。下次你下载一个大文件时,看看网速从慢到快再到稳的曲线——那条锯齿形忽高忽低的线,正是运河上每一条漕船翻覆与抵达的起伏。河在变,船只能试探。收要快,加要慢。翻过船的地方,下次记住。