卷 VI · 处处是环CH 22深度 22/24

你的代码里全是环

如果你写后端,那你大概率正在维护好几个反馈环——自动扩容、重试退避、限流、熔断、连接池、缓存预热。它们从来没被这么叫过,所以出问题时只能靠猜。这一章把它们一个个点出来,并让其中最典型的那个当场演示一条你现在应该能预测的规律。

自动扩容震荡重试是正反馈把环写进设计文档

▷ 先拧一下

一个标准的自动扩容器:目标 CPU 利用率 60%,规则是最常见的那条——想要的台数 = 现在的台数 × 看到的利用率 ÷ 目标利用率。机器启动需要 120 秒。流量在某一刻从 400/s 跳到 900/s,需要 15 台才能撑住。

现在比较两种情况,其他配置完全相同

甲:指标延迟 60 秒(比启动时间短)
乙:指标延迟 180 秒(比启动时间长)

A 都能稳定在 15 台,乙只是慢一点 B 乙会超调到 20 台左右,然后回落 C 甲稳定在 15 台纹丝不动;乙一路冲到 200 台(配额上限) D 两个都会震荡,因为这条规则本身就不稳定

先认出它们

下面这些东西,全都是反馈环。左边是你熟悉的叫法,右边是它在这本书里的五个零件:

它平时叫目标被控量控制量延迟从哪来
自动扩容目标利用率实际利用率机器数指标聚合 + 机器启动
限流 / 并发控制目标延迟实测延迟并发上限请求本身的耗时
熔断器错误率阈值实测错误率开/关(继电!)统计窗口
连接池伸缩等待时间实测等待连接数建连时间
TCP 拥塞控制不丢包丢包 / 延迟发送窗口一个 RTT
缓存预热 / 淘汰命中率实测命中率缓存内容TTL
重试 + 退避请求成功失败与否重试频率超时时间
GC 的堆大小调节停顿时间 / 占用实测停顿堆大小一个 GC 周期

每一行都可以套用这本书的全部工具:算它的延迟、算它的可用增益、问它的相位裕度还剩多少、检查有没有积分饱和。

而现实是,这些系统的参数几乎全是拍出来的,然后靠线上事故一点点修。不是因为工程师不认真,是因为没人告诉过他们这是一个反馈环。

那个扩容器

配置机器数范围摆动扩缩方向来回过载时长
零延迟(理想)15–1500 次1 s
只有启动延迟 120 s15–1500 次120 s
指标晚 60 s,启动 120 s15–1503 次180 s
指标晚 180 s,启动 120 s1–20019910 次1196 s
指标晚 60 s,机器秒起1–20019938 次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 秒)之所以看起来那么保守,正是因为指标延迟通常不可控——所以只能在另一头留足余量。

✗ 这个直觉是错的
自动扩容抖动,是阈值没调好。把扩容阈值和缩容阈值拉开一点就行了。 拉开阈值(也就是加回差,第 16 章)确实能减少抖动,但它对付的是「在阈值附近来回」这一类抖动。如果根因是指标延迟超过了执行延迟,那么任何阈值设置都救不了——因为那是相位问题,不是阈值问题。

怎么区分这两种?看波形,而且很好认:

  • 阈值问题:机器数在两个相邻值之间来回(比如 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%。