你的代码里全是环
如果你写后端,那你大概率正在维护好几个反馈环——自动扩容、重试退避、限流、熔断、连接池、缓存预热。它们从来没被这么叫过,所以出问题时只能靠猜。这一章把它们一个个点出来,并让其中最典型的那个当场演示一条你现在应该能预测的规律。
一个标准的自动扩容器:目标 CPU 利用率 60%,规则是最常见的那条——想要的台数 = 现在的台数 × 看到的利用率 ÷ 目标利用率。机器启动需要 120 秒。流量在某一刻从 400/s 跳到 900/s,需要 15 台才能撑住。
现在比较两种情况,其他配置完全相同:
甲:指标延迟 60 秒(比启动时间短)
乙:指标延迟 180 秒(比启动时间长)
先认出它们
下面这些东西,全都是反馈环。左边是你熟悉的叫法,右边是它在这本书里的五个零件:
| 它平时叫 | 目标 | 被控量 | 控制量 | 延迟从哪来 |
|---|---|---|---|---|
| 自动扩容 | 目标利用率 | 实际利用率 | 机器数 | 指标聚合 + 机器启动 |
| 限流 / 并发控制 | 目标延迟 | 实测延迟 | 并发上限 | 请求本身的耗时 |
| 熔断器 | 错误率阈值 | 实测错误率 | 开/关(继电!) | 统计窗口 |
| 连接池伸缩 | 等待时间 | 实测等待 | 连接数 | 建连时间 |
| TCP 拥塞控制 | 不丢包 | 丢包 / 延迟 | 发送窗口 | 一个 RTT |
| 缓存预热 / 淘汰 | 命中率 | 实测命中率 | 缓存内容 | TTL |
| 重试 + 退避 | 请求成功 | 失败与否 | 重试频率 | 超时时间 |
| GC 的堆大小调节 | 停顿时间 / 占用 | 实测停顿 | 堆大小 | 一个 GC 周期 |
每一行都可以套用这本书的全部工具:算它的延迟、算它的可用增益、问它的相位裕度还剩多少、检查有没有积分饱和。
而现实是,这些系统的参数几乎全是拍出来的,然后靠线上事故一点点修。不是因为工程师不认真,是因为没人告诉过他们这是一个反馈环。
那个扩容器
| 配置 | 机器数范围 | 摆动 | 扩缩方向来回 | 过载时长 |
|---|---|---|---|---|
| 零延迟(理想) | 15–15 | 0 | 0 次 | 1 s |
| 只有启动延迟 120 s | 15–15 | 0 | 0 次 | 120 s |
| 指标晚 60 s,启动 120 s | 15–15 | 0 | 3 次 | 180 s |
| 指标晚 180 s,启动 120 s | 1–200 | 199 | 10 次 | 1196 s |
| 指标晚 60 s,机器秒起 | 1–200 | 199 | 38 次 | 1175 s |
第四行和第五行值得放在一起看,因为它们揭示的规律很反直觉:
决定这个环稳不稳的,不是延迟的绝对大小,而是两个延迟的相对大小:
指标延迟 > 机器启动时间 → 震荡
- 第 3 行:指标 60 s,启动 120 s(60 < 120)→ 稳。
- 第 4 行:指标 180 s,启动 120 s(180 > 120)→ 冲到 200 台。
- 第 5 行:指标 60 s,启动 0 s(60 > 0)→ 也是 200 台。
机器起得更快,反而炸了。
为什么?因为一个正经的扩容器会记住自己已经下过的单(「还有 8 台正在启动,别重复下单」)。而这个记账只在机器还没上线的时候有效。
一旦机器上线了,它就从待办列表里划掉了——可如果此时指标还没反映出这些机器的贡献,扩容器就会看着那个仍然很高的旧利用率,认为「还不够」,于是再下一单。下一轮同样,再下一单。一路冲到配额上限。
这是本书里第二次出现「记账」这个主题,而且是同一个毛病:控制器对着一个已经解决了的问题继续用力。第 15 章那个积分饱和是一模一样的结构——积分器对着一个物理上到不了的目标继续攒账。两者的解药也是同一类:让控制器知道「你要的那个动作,其实已经在路上了」。
而第 5 行还有一个更有用的推论:「让机器启动更快」这个听起来纯粹是好事的优化,可能把一个稳定的环推过那条线。如果你的团队正在把容器冷启动从两分钟优化到五秒,同时没有动指标链路——那你在无意中把这个环从第 3 行推到了第 5 行。
冷却时间是干什么用的
那第四行怎么救?最常见的做法是加冷却时间(cooldown):扩容之后,强制等一段时间才允许下一次动作。
在 demo 里把冷却拖到 300 秒,第四行那个「1–200 台」会变回 15–15 台,一次抖动都没有。
冷却时间的作用,用这本书的话说就是:它把控制器的采样周期强行拉长到超过环路延迟,于是每次决策时,上一次动作的效果已经完整地反映在指标里了。它相当于把一个连续的反馈环,改成一个「动一下、等结果、再动一下」的离散决策过程。
但它不是白拿的:冷却时间也是延迟。在 demo 里把冷却继续拖到 600 秒,过载时长反而从 300 秒涨回 420 秒——因为真正需要扩容的时候,它在那儿等着。又是一个 U 形曲线,又是同一笔交易。
重试:一个几乎没人当环看的正反馈
最后单独说重试,因为它是这些环里唯一一个正反馈的,而正反馈是会自我放大的。
# 重试环:
下游变慢 ──▶ 请求超时 ──▶ 客户端重试 ──▶ 下游负载增加
▲ │
└──────────────────────────────────────────┘
这是一个**正**反馈环
★ 每绕一圈,负载更大,超时更多,重试更多。
# 而它的「增益」就是重试次数:每层重试 3 次,
# 三层微服务串起来就是 3³ = 27 倍的放大。
这就是所谓的「重试风暴」。它的可怕之处在于:每一层的重试逻辑单独看都非常合理(「网络抖动嘛,重试一下就好了」),而合起来是一个增益 27 的正反馈环。
标准的对策,用这本书的语言可以一句话说清:
- 指数退避 + 抖动——降低这个环的增益,并打散它的频率成分(避免所有客户端同步共振);
- 重试预算(比如「重试量不超过总请求的 10%」)——给这个正反馈环加一个硬上限,也就是第 15 章那个饱和,但这次是故意的;
- 只在最外层重试——把 3³ 变回 3,也就是拆掉串联(第 20、21 章);
- 断路器——在环路增益要超过 1 的时候,直接把环剪断。
最后那条特别值得体会:熔断器的本质不是「保护下游」,是「在正反馈失控之前把回路断开」。这也是它必须有半开状态的原因——你需要一个安全的方式去试探环路增益是不是降回 1 以下了。
缓存踩踏(cache stampede)。缓存同时过期 → 大量请求穿透到数据库 → 数据库变慢 → 重建缓存更慢 → 更多请求穿透。同样是正反馈。对策(单飞、随机化 TTL、提前异步刷新)全都可以用这一章的语言重述:降增益、去同步、加前馈。
GC 死亡螺旋。堆快满了 → GC 更频繁 → CPU 被 GC 占用 → 处理变慢 → 请求堆积 → 对象更多 → 堆更满。经典的正反馈,而且它的收敛点是 OOM。
Kubernetes 的 HPA 抖动。官方文档里那些「稳定窗口」「缩容策略」「行为策略」参数,全部是在给这个环加阻尼和冷却。而它们的默认值(比如缩容稳定窗口 300 秒)之所以看起来那么保守,正是因为指标延迟通常不可控——所以只能在另一头留足余量。
怎么区分这两种?看波形,而且很好认:
- 阈值问题:机器数在两个相邻值之间来回(比如 6 和 7),幅度很小,频率高。这是第 16 章那个极限环。→ 加回差、加冷却。
- 相位问题:机器数大幅摆动(1 台 ↔ 200 台),周期比较长,而且往往一次比一次夸张。这是第 10 章那个正反馈。→ 缩短指标延迟,或者把冷却时间拉到超过环路延迟。
而最有价值的一条诊断是这一章开头那个不等式:去量一下「指标延迟」和「执行延迟」这两个数,看谁大。这个判断花五分钟,而它能替你省掉一整轮盲目调参。
顺便,这也解释了为什么很多团队的扩容配置是「历史事故的沉积岩」:每次出事就把某个数字往保守里改一点,从没人问过那个环的延迟结构是什么。结果是一套没人敢动、也没人说得清为什么的参数。
C甲稳定在 15 台纹丝不动(摆动 0),乙一路冲到 200 台(摆动 199)。其他配置完全相同,只差指标延迟从 60 秒变成 180 秒。
分界线不是任何一个阈值,而是那个不等式:指标延迟 > 机器启动时间 → 震荡。因为一旦越过它,扩容器就会对着已经上线但还没反映在指标里的机器重复下单。
而最有意思的是第五行:把机器启动时间从 120 秒优化到 0 秒(纯粹的改进!),同一个指标延迟 60 秒也会冲到 200 台。让执行变快,可以把一个稳定的环推过那条线。
A 「乙只是慢一点」——这是第 1 章那个错觉的最后一次现身,也是全书最顽固的一个:把延迟当成「推后」。在闭环里它不是推后,是换一个结局。 B 「超调到 20 台左右」——这个答案假设了「有限的超调」,而实际的机制是每一轮都在旧读数的基础上再乘一次。它是指数式的,不是加法式的,所以它会一路撞到配额墙上。如果没有配额,它不会停在任何一个数上。 D 「这条规则本身就不稳定」——这条规则本身没问题。第 1、2、3 行说明它在合适的延迟结构下稳得很好,机器数一步到位、纹丝不动。让它变坏的不是规则,是规则之外的两个时间常数。这一点很重要,因为它决定了你该去改什么——改规则(换个扩容算法)通常不如改延迟结构。这一章的一句话
你的系统里早就有一堆反馈环了;它们没有名字,所以没有人算过它们的延迟、增益和裕度——而那三个数决定了它们会不会在某个凌晨自己晃起来。
下一章把镜头再拉远一点:身体、组织和经济里的环。而重点会落在一个所有工程师都该知道的现象上——当你的传感器只测得到你想要的东西的一部分时,那个环会非常出色地把测得到的部分顶到目标,同时把测不到的部分踩进泥里。测得到的涨 90%,你真正想要的跌 24%。