漕运上的闸官
一、船多了堵,船少了亏
大运河从杭州到通州,一千八百余里。河道宽的地方能并排走六条漕船,窄的地方——尤其是山东段的十二道闸口——一次只能过一条。
管山东段漕运的闸官姓柳,人称柳闸官。他手里管着十二道闸,每天有成百条漕船从他面前过——运粮的、运盐的、运铜的、运贡品的。
柳闸官有一本难念的经。
船放少了,京城等着粮,户部发文催。"你山东段一天才过八十条船?后面的船排到徐州了!"
船放多了,全堵在闸口。挤。挤到后面,撞船。一撞船,粮食落水,盐包泡汤。柳闸官得赔。
柳闸官试过各种法子——定死数、看时辰放、看船型放——都不好使。因为河不是死的。春天涨水,河道宽,放得密也不堵。冬天枯水,放得稀还能撞。
他的徒弟阿漕问:"师父,到底一次放几条?"
柳闸官摇头:"我现在只能猜。"
二、先放一条试试
有一天,柳闸官在闸口坐了一整天,看着来来往往的漕船,忽然想到了一个法子。
"阿漕,"他说,"我们不猜了。我们试。"
"怎么试?"
柳闸官指着闸口:"你看,上游排队等着过闸的船有上百条。我把闸门一开,一次放几条出去。它们从这道闸走到下一道闸,再返回来报个信——来回一趟,叫一轮。"
"一轮过后,回来的船都平安——说明河道不挤。一轮过后,有船没回来——说明河道挤了。"
阿漕问:"那第一轮放几条?"
"一条。"
"一条?"阿漕急了,"那后面排着的船要等到什么时候?"
"你先听我说完。"柳闸官蹲下来,在泥地上画了一个数字: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 拥塞控制?
"先放一条试试"是慢启动的起点。 TCP 连接建立后,发送方不知道网络容量,初始 cwnd 很小(在 Linux 中通常是 10 MSS)。慢启动的设计哲学是"先保守探路,确认安全再加速"。
"加倍放"精确对应慢启动的指数增长。 每个 RTT cwnd 翻倍——这是 TCP 能够在几十毫秒内从 1 MSS 冲到数百 MSS 的关键机制。指数增长让 TCP 快速"找到"可用带宽的上界。
"加倍线"(ssthresh)是慢启动和拥塞避免的切换点。 首次 ssthresh 通常设为一个很大的值,首次丢包后 ssthresh 被设为 cwnd/2。之后,cwnd 在 ssthresh 以下是慢启动(指数),到了 ssthresh 就是拥塞避免(线性)。
"每次多放一条"是加性增(Additive Increase)。 在拥塞避免阶段,每个 RTT cwnd 仅增加 1 MSS。这非常保守,但正因为保守,TCP 才能在拥塞边缘稳定运行而不引发大规模丢包。
"翻一条船砍一半"是乘性减(Multiplicative Decrease)。 这是 AIMD 中最激进的部分——一旦检测到拥塞信号(丢包),立即将发送速率减半。AIMD 的非对称性(慢加速、急刹车)经过数学证明是实现公平带宽分配的必要条件。
"收要快,加要慢"道出了 AIMD 的根本智慧。 乘性减确保拥塞快速缓解——如果所有连接同时减半,总负载立即减半。加性增确保各连接公平收敛到均分带宽——慢速增长给了系统时间达到平衡。
"放的不是船,是不知道"是拥塞控制的哲学核心。 发送方对网络状态的认知永远是滞后的、不完整的。拥塞控制本质上是一个在不确定环境中做决策的控制系统——每个 ACK 减少一点不确定性,每次丢包提醒你"边界到了"。
后记:TCP 拥塞控制可能是互联网中最优雅的反馈控制算法之一。它的数学原理(AIMD 收敛到公平分配)和工程智慧(慢启动快速探测、拥塞避免保守试探)结合得天衣无缝。Van Jacobson 在 1988 年的论文只有 25 页,但它挽救了一次互联网的拥塞崩溃,并定义了此后三十年网络传输的基本节奏。下次你下载一个大文件时,看看网速从慢到快再到稳的曲线——那条锯齿形忽高忽低的线,正是运河上每一条漕船翻覆与抵达的起伏。河在变,船只能试探。收要快,加要慢。翻过船的地方,下次记住。

