Skip to content

13 · 规模化的力学:加机器不是免费的

一句话点题:新人以为「扛不住?加机器」是一句结论;架构师知道它是一个开头。 加机器会在你没想到的地方先断——有状态组件搬不动、热点把一台打爆、尾延迟随扇出放大、协调开销让收益递减。本章承接 05 的「复制/分片/缓存」往下挖:规模不是把系统放大,而是换一套力学。 在 AI 几秒就能生成一个能跑原型的今天,知道「规模会在哪先断」,是架构师最不可替代的判断。


一、垂直 vs 水平:加机器到底加在哪

「扛不住了加机器」其实有两条完全不同的路:

   垂直扩展(Scale Up)              水平扩展(Scale Out)
   ──────────────────              ──────────────────
   把一台机器换得更猛               把很多台普通机器并起来
   4 核 → 64 核,16G → 1T 内存       1 台 → 10 台 → 1000 台
   ✓ 简单:代码一行不用改            ✓ 理论上无上限、还顺带容错
   ✗ 有物理天花板(最猛的机器就那样)  ✗ 协调成本:它们要互相通信、同步
   ✗ 单点:这台挂了就全挂            ✗ 一致性、分片、再平衡全来了
   ✗ 越往上越贵(非线性溢价)         ✗ 复杂度从「一台」变成「一群」

架构智慧:垂直扩展是「买时间」——它最简单,在系统还小的时候,升一台机器永远比改架构便宜,别急着上分布式。但它有天花板,且不解决单点。真正的规模化是水平扩展:用一堆便宜机器顶上,但你为此付出的代价,是从「管一台」变成「管一群」——而本章接下来讲的所有难题,都是这「一群」带来的。

关键的分水岭来自 05 章 的第一性原理:无状态好扩,有状态难扩。

   无状态组件(Web/API/推理 worker):    有状态组件(数据库/缓存/连接):
   ┌──┐┌──┐┌──┐┌──┐                     ┌──────────────────┐
   │实例││实例││实例││实例│ ← 想加随便加    │  它"记得"东西       │ ← 想加?那两份
   └──┘└──┘└──┘└──┘   流量随便分        │ (余额/库存/会话)    │   "记忆"怎么对上?
   它们之间不知道彼此                     └──────────────────┘   一加就有同步问题

所以规模化的第一步,永远是把状态从计算里挤出去:让 Web/API 层尽量无状态(可以随便复制),把难搞的状态收拢进少数几个专门管状态的地方(数据库、缓存)精心伺候。于是「加机器」这件事,在无状态层近乎免费,在有状态层贵得吓人——而你的瓶颈,几乎总是在后者。

判断要点:当有人说「加机器就行」,先问一句:加的是哪一层? 无状态层加 10 台是配置改一行;有状态层加 1 台,可能意味着重新分片、搬迁 TB 级数据、几周的迁移窗口。「加机器」从来不是一个均匀的动作。


二、分片策略:范围 vs 哈希,以及一致性哈希为什么是神来之笔

05 讲了分片(sharding)是「扩写」的牌,也点了「分片键选错是灾难」。这一节把怎么分讲透。两大流派:

   范围分片(Range)                    哈希分片(Hash)
   ────────────                       ────────────
   按 key 的区间切:                    按 hash(key) 切:
   [A-F]→片1 [G-M]→片2 [N-Z]→片3       hash%N 决定落哪片
   ✓ 范围查询高效(扫一段在一个片)       ✓ 分布均匀,天然防热点
   ✓ 相邻数据物理相邻                    ✗ 范围查询废了(相邻 key 散到各片)
   ✗ 易热点:新数据总往"最后一段"挤       ✗ 加减节点 → 全量重映射(见下)
     (按时间分片,今天的全压一个片)

范围分片适合「要按区间扫」的场景(时序、按时间翻页);哈希分片适合「均匀打散、随机点查」。但朴素哈希(hash(key) % N)有一个致命缺陷——它把节点数 N 写死进了公式:

   原本 4 台:hash(key) % 4
   加到 5 台:hash(key) % 5
   ──▶ 几乎每一个 key 的归属都变了!N 一变,余数全乱。
       结果:加一台机器,要搬迁 ~80% 的数据。整个集群瞬间被"再平衡"流量打瘫。

一致性哈希(Consistent Hashing) 就是来解这个的。它的核心思想美得像魔术:把哈希空间想象成一个环(0 到 2³²-1 首尾相接),节点和数据都哈希到环上;一个 key 顺时针找到的第一个节点,就归它。

用大白话理解这个「环」:想象一个圆形钟面。 几台机器先各自占住钟面上的几个钟点位置;每个数据也落在某个钟点上,然后顺时针走,撞到的第一台机器就负责它。妙处在于:这时你新加一台机器,等于往钟面上插一个新钟点——只有「新钟点」到「它前一个钟点」之间那一小段的数据需要换东家,钟面上其它位置的数据全都纹丝不动。 这就是为什么加一台机器不再「全集群地震」。

            0/2³²

        节点D │      节点A          • 数据 k 哈希落在这,顺时针
          ●   │   ●                  找到的第一个节点是 A → 归 A
              │  ╱ k(数据)
   ───────────┼───────────
              │      ╲
          ●   │   ●   ╲ 现在加入节点 E(落在 A 和 B 之间)
        节点C │  节点B   ──▶ 只有"原本归 B、但现在落在 E 之前"的那一小段
              │             数据需要搬到 E。其余所有节点纹丝不动!

架构智慧:一致性哈希的全部价值,就一句话——加/减一个节点,平均只需搬动 1/N 的数据(只影响环上相邻的那一段),而不是朴素哈希的几乎全量。 这是从「加机器=全集群地震」到「加机器=局部微调」的质变,也是几乎所有大规模分布式存储(Cassandra、DynamoDB、ScyllaDB)能平滑扩容的地基。

但朴素的一致性哈希还有个问题:节点在环上的位置是随机的,可能分布不均(有的节点管了一大段、有的只管一小段),而且某节点一挂,它的全部负载会压给顺时针的下一个邻居。解法是虚拟节点(virtual nodes / vnodes):

   每个物理节点,在环上放很多个"分身"(比如 256 个虚拟节点):
   物理节点 A ──▶ 散布成 A₁ A₂ A₃ ... A₂₅₆ 遍布整个环
   • 分布更均匀:大数定律抹平了随机性
   • 一台挂了,它的负载被"打散"分给所有其他节点,而不是全压给一个邻居
   • 异构机器好处理:猛的机器多放几个 vnode,弱的少放

判断要点:范围 vs 哈希不是「哪个更好」,是「你的查询形态是什么」——要按区间扫(时序、翻页)用范围;要均匀打散用哈希。而一旦选了哈希、且需要弹性扩容,一致性哈希 + 虚拟节点几乎是标配。这正是 05 里「再分片很痛」那句话的正解。


三、热点:一个 key 就能把整台机器打爆

分片把负载摊到 N 台,前提是负载真的均匀。现实里,负载从来不均匀——它遵循幂律:少数 key 占据绝大多数流量。这就是热点(hot key / hot partition):

   理想(均匀):每片 10% 流量          现实(幂律):一个 key 占 50%
   ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓     ████████████  ▓ ▓ ▓ ▓ ▓ ▓ ▓ ▓ ▓
   分片再多也没用——那个热 key 所在的       ↑ 顶流明星发帖 / 秒杀那一件商品 /
   单台机器被打爆,其余的全在睡觉            突发新闻 / 群里 @everyone

热点的可怕在于:分片本来是来救你的,但热点让分片失效——你加再多机器,流量还是全压在持有那个热 key 的那一台上。识别与打散的手段:

手段怎么做适合
加盐(salting)把热 key 拆成 key#1 key#2key#N,人为散到多片;读时聚合写热点(如计数器)
本地缓存在应用进程内存里缓一份,大部分读根本不出本机读热点(配置、热门内容)
只读副本复制给热点数据多做几个只读副本,把读分摊出去读多写少的热点
请求合并(coalescing)同一瞬间对同一 key 的 N 个请求,只放一个去查后端,其余等结果读击穿(见第四节)

架构智慧:热点是规模化里最反直觉的杀手——它让「我已经分片了/加机器了」的安全感瞬间破灭。识别热点要靠监控(按 key 维度的流量分布,而不只看总量);打散的核心思路只有一个——把"一个点"变成"一片":要么把热 key 物理拆开(加盐),要么把它复制到多处(本地缓存/只读副本),要么把重复的请求归一(合并)。下面的 Twitter 和 Discord 案例,就是这两招的真实战场。


四、多级缓存与缓存踩踏:墙塌的那一刻

05 讲了单层缓存的三大坑(穿透/击穿/雪崩)。真实的大规模系统是多级缓存——数据被复制到一层又一层,离用户越近越快:

   用户 ──▶ ┌─────┐ ──▶ ┌──────┐ ──▶ ┌───────┐ ──▶ ┌──────┐ ──▶ ┌─────┐
            │ CDN  │     │ 边缘  │     │ 应用本地│     │分布式 │     │  DB  │
            │(静态)│     │(区域)│     │ 缓存    │     │缓存    │     │(真相)│
            └─────┘     └──────┘     └───────┘     │Redis  │     └─────┘
            ~10ms        ~20ms        <1ms(进程内) └──────┘       ~10ms+
            离用户最近、命中最快                      ~1ms
            每一层都挡掉一批流量,真正落到 DB 的只剩极少

层级越多,DB 越轻松——但每多一层副本,一致性就多一道难题(改了 DB,这五层怎么一致地失效?)。而最凶险的是缓存踩踏(thundering herd / cache stampede):

   一个超热的 key,缓存里它过期的那一瞬间:
   时刻 T:   缓存有值 ──▶ 1 万 QPS 全部命中,DB 毫无压力
   时刻 T+ε: 这个 key 过期 ──▶ 1 万个请求同时发现"没了"
            ──▶ 1 万个请求"一起"涌向 DB 去重建同一个值
            ──▶ DB 瞬间被同一个查询打 1 万次 ──▶ 雪崩,可能引发连锁宕机

它比 05 的「击穿」更普遍也更隐蔽——不是缓存坏了,而是缓存「正常过期」这件事本身,在高并发下成了同步的洪峰。三道防线:

  • 请求合并(single-flight):同一 key 的并发重建,只放第一个去查 DB,其余的订阅它的结果。1 万次查询塌缩成 1 次。(这正是 Discord 的解法)
  • 过期时间加随机抖动:别让一批 key 在同一秒集体过期(防雪崩)。
  • 逻辑过期 / 提前异步刷新:key 不真的删,后台在它快过期时悄悄刷新,读端永远命中。

判断要点:多级缓存是规模化的标配,但它把「一份数据」变成了「五份副本」——你享受的每一点速度,都在一致性账本上记了一笔。 而缓存踩踏提醒你:在大规模下,连「正常过期」这种例行操作都可能成为同步的灾难。好的缓存设计,一半是命中率,另一半是「失效时别让所有人同时扑向 DB」。


五、多区域 / 多活:把系统铺到地球两端

当用户遍布全球、或要容忍整个机房 / 区域挂掉时,你要把系统部署到多个地理区域。最简单的是「一主多备(一个区域写,其余只读 / 待命)」;最难、也最强的是多区域多活(multi-region active-active)——每个区域都能读也能写:

   ┌─── 美国区 ───┐         跨区 RTT ~70-150ms        ┌─── 欧洲区 ───┐
   │ App + DB副本 │ ◀═══════════════════════════════▶ │ App + DB副本 │
   │ 就近服务美国   │      (光速 + 距离的硬下限)         │ 就近服务欧洲   │
   └──────────────┘                                  └──────────────┘
        ▲                                                   ▲
   美国用户(低延迟)                                    欧洲用户(低延迟)
   ★ 难点:同一条数据,美国和欧洲"同时"被改了,听谁的?(写冲突)

多活买到的是数据本地性(就近读写、延迟低)+ 区域级容灾(一个区域全挂,流量切到另一个)。但它撞上了 10 章 的全部硬道理:

  • 跨区延迟是硬约束:北京到弗吉尼亚一个来回 ~150ms。如果每次写都要等另一个区域确认(强一致),用户就要为光速买单。
  • 写冲突必须有解:两个区域同时改同一条数据,合并时听谁的?这正是 10 因果/并发的实战:
   美国区:把昵称改成 "Alice"  ┐
                              ├─ 几乎同一时刻、跨区还没同步 ──▶ 冲突!
   欧洲区:把昵称改成 "Alicia" ┘
   ─ LWW(最后写入者赢):按时间戳留一个 ── 简单,但另一个"丢更新"了
   ─ CRDT(无冲突数据类型):数学上保证任意顺序合并都收敛到同一结果
                            (协同文档、计数器、购物车常用)── 强,但建模受限
   ─ 业务拆分:让两区天然改不同字段/不同主键 ── 从源头让冲突不可能
  • 就近路由:用 GeoDNS / Anycast 把用户引到最近的区域,这是多活省延迟的前提。

架构智慧:多活不是「把系统复制两份」那么简单,它在 10 的 PACELC 账单上做了一个昂贵选择——为了低延迟和高可用(就近 + 容灾),它几乎必然放松强一致(允许跨区短暂不一致 + 用冲突解决兜底)。 绝大多数业务不需要全球多活;先问自己「我的用户真的分布在多大洲、我真的需要扛整个区域级故障吗」,再决定要不要付这笔极重的复杂度账。


六、尾延迟的数学:为什么 p99 才是真实体验,扇出会放大它

新人看延迟只看平均值;架构师只看尾部(p99、p999)。为什么?

   平均延迟 50ms 听起来很美,但分布可能是:
   99% 的请求:20ms(快)        1% 的请求:2000ms(慢得离谱)
   平均 ≈ 40ms ✓ "达标"          ↑ 但每 100 个用户就有 1 个在转圈 2 秒
   ── 平均数把"少数极慢"稀释没了,可那 1% 恰恰是你最该在意的体验。

真正的杀手是扇出放大(fan-out amplification)。 现代系统里,一个用户请求往往要扇出成几十上百个内部子调用(查 100 个分片、调 100 个微服务),只有等最慢的那个子调用回来,整个请求才算完成

打个比方:这就像一桌 100 个人点的菜,要等最后一道上齐了才能开饭。 单看每道菜,只有 1% 的概率会做得慢;可一桌 100 道菜里,「至少有一道慢」几乎是必然的——于是整桌人(整个请求)几乎每次都得干等。人越多(扇出越大),开饭越难不迟到。

   一个请求扇出到 100 个子调用,每个子调用 p99 = 1%(即 1% 概率慢):
   整个请求要"全部"子调用都快,才算快。
   全部快的概率 = (99%)¹⁰⁰ ≈ 36.6%
   ──▶ 也就是说:有 63% 的概率,这个请求会撞上"至少一个慢子调用"而变慢!
       单个子调用只是 p99 慢,整体请求却几乎"必然"慢 ── 这就是放大。

架构智慧(本章最反直觉的一条):在大扇出系统里,整体的 p99 趋近于子调用的 p999 甚至 p9999。 子组件「偶尔慢一下(p99)」无所谓——这个朴素直觉在扇出面前是错的。一个慢节点,会被 100 倍的扇出放大成「几乎每个请求都慢」。这就是为什么 Google 的工程师说:在规模化系统里,治理尾延迟比优化平均延迟重要一个数量级。

对冲请求(hedged requests)是治尾延迟的利器:同一个请求发给两个副本,谁先回用谁。 但全发两份会让负载翻倍,所以更聪明的做法是 Google 的「延迟对冲」——

   先发给副本 A;若 A 在"p95 时间"内没回,才补发给副本 B,用先回的那个。
   ──▶ 只对最慢的那 5% 请求才发第二份 ──▶ 额外负载仅 ~5%
       却能把长尾砍掉一大截(见下方 Google 真实数据)。

判断要点:延迟是分布,不是数字。对外承诺和内部 SLO 都要用 p99/p999,而不是平均。 越是高扇出的系统(搜索引擎 一次查询扇出到成百上千个索引分片、社交信息流 组装一屏要拉很多源),越要把尾延迟当头等大事——对冲请求、超时预算、慢副本剔除,都是为它准备的武器。


七、排队的直觉:为什么逼近 100% 利用率时,系统会突然爆炸

💧 深水区(公式可全部跳过,只记一个生活直觉):这一节有几个数学名字(Little 定律、USL、Amdahl),但它们想说的就一件你天天体验的事——高速公路。 路上车不多时,你想多快开多快;可一旦车流逼近「路面塞满」的程度,只要再多几辆车,就会从「顺畅」瞬间变成「堵死」,而且越堵越死。服务器和高速公路一模一样:利用率(路有多满)一旦逼近 100%,延迟(你被堵多久)就会爆炸式飙升。 这就是为什么「看起来闲着 30% 没跑满」的服务器不是浪费——那 30% 是给突发车流留的缓冲带。下面把这个直觉讲细。

最后一个、也是最深的力学——排队论。它解释了一个让无数人栽跟头的现象:系统在 70% 利用率时一切正常,加一点流量到 95%,延迟突然飙升十倍。

先记住 Little 定律(简洁到不像真的,却永远成立)——别被公式吓到,它无非是说「排队的人数 = 来人的速度 × 每人平均待多久」,你在奶茶店排队时早就懂这个道理了:

   L = λ × W
   队列里平均的请求数 = 到达率 × 每个请求平均停留时间
   ── 它的用处:你能不能从"并发数/吞吐"反推出延迟,或反过来估容量。

而最该刻进骨子里的,是利用率与排队长度的关系(M/M/1 直觉):

   平均排队时间 ∝ 1 / (1 − ρ)      (ρ = 利用率)
   ρ=50% → 因子 2        延迟还好
   ρ=80% → 因子 5        开始肉眼可见地变慢
   ρ=90% → 因子 10       明显卡顿
   ρ=95% → 因子 20       开始排长队
   ρ=99% → 因子 100      ★ 雪崩:延迟爆炸式飙升 ★

        延迟
         │                                    ╱← 逼近 100% 时
         │                                  ╱   曲线near-垂直拉起
         │                              ___╱
         │              ________──────
         │_____─────────
         └──────────────────────────────────▶ 利用率 ρ
         0%        50%      80%   90%  95% 99% 100%

为什么?因为请求到达是随机的(有波峰波谷),而高利用率下系统没有任何余量去消化突发的波峰——波峰一来,请求开始排队,队列里的请求又拖慢后面的,正反馈。利用率越接近 100%,这个正反馈越剧烈,直到延迟趋于无穷。

架构智慧:「留余量」是设计,不是浪费。 那个看起来「闲着 30% 没跑满」的服务器,买的是应对突发的能力可控的尾延迟——把利用率压到 100% 省下来的机器钱,会在第一波流量尖峰里以「系统雪崩」的形式十倍奉还。这也是为什么 在线票务 这种「开售即洪峰」的系统,必须按峰值而非均值预留容量。

而当你想靠加机器来提升容量时,还有最后一个冷水——通用可扩展性定律(USL)与 Amdahl 定律。还是用大白话:就像往一个厨房里塞厨师。 一开始多一个厨师多一份产出;但厨师太多,大家开始抢灶台、互相打招呼、等对方让路——加到某个点,再多塞人反而更慢。机器之间也一样,越多越要「互相对齐、互相协调」,这份开销会吃掉、甚至吃穿你加机器的收益:

   理想线性:    10 台 = 10 倍                      实际:
   ↑加速比                                          ↑加速比
   │        ╱ 理想(线性)                          │      ╱‾‾‾╲___ ★ 加到一定数量后
   │      ╱                                        │    ╱        反而下降!
   │    ╱                                          │  ╱ 实际(USL)
   │  ╱                                            │╱
   └──────────▶ 机器数                             └──────────▶ 机器数
   • Amdahl:串行部分(必须排队的那点)限制了上限
   • USL 更狠:节点间"协调开销(coherency)"随规模增长 ──▶ 加机器收益递减,甚至变负

判断要点(收束全章):加机器的收益不是线性的,甚至可能变负。 机器越多,它们之间「对齐状态、互相协调」的开销越大(这正是第一节「管一群」的代价、10 章 共识与协调的成本)。这就是为什么「无状态、少协调」的设计能近线性扩展,而「重协调、强一致」的设计加到一定规模就撞墙。架构师的活,就是尽量减少需要协调的部分——好的规模化设计,本质是「让加进来的机器尽量不需要互相说话」。


📌 真实案例:三个把本章力学顶到极限的大型系统

① Discord:用一致性哈希 + 请求合并,扛住「@everyone」的热点(第二、三、四节)

Discord 存了万亿级消息,按 (channel_id, bucket时间窗) 分片。问题来了:一个大服务器里有人 @everyone 发公告,那一个分片瞬间被海量并发读打爆——这就是教科书级的「热点分区(hot partition)」。Discord 的两招正是本章第三、四节的实战:① 请求合并(request coalescing)——同一瞬间对同一行的成千上万个读,只放第一个去查数据库,其余的订阅它的结果,「上万次查询塌缩成一次」;② 一致性哈希路由——同一个 channel 的所有请求,都被路由到同一个数据服务实例,合并才有意义。后来又从 Cassandra 迁到 ScyllaDB(shard-per-core 架构,工作负载隔离更强,防止单个热分区拖垮整个节点):集群从 177 个 Cassandra 节点缩到 72 个 ScyllaDB 节点,读 p99 从 40–125ms 降到 15ms,写 p99 从 5–70ms 降到 5ms。迁移用 Rust 重写后,从预估 3 个月压到 9 天(峰值 320 万条/秒)。

📎 Discord 官方工程博客:How Discord Stores Trillions of Messages

② Twitter / X:名人 fan-out 与「Justin Bieber 问题」(第三节热点)

Twitter 的时间线靠写时扇出(fan-out on write)——你发一条推,系统把它"推"进所有粉丝的时间线缓存。这对普通人很高效,但碰上几千万粉丝的顶流就成了灾难:发一条推 = 几千万次写入,这就是著名的**「Justin Bieber 问题」——它不是某个明星的事,而是幂律热点的代名词**(早期 Bieber 的海量粉丝甚至能把他「关注数」那行的 MySQL 行锁压出错误率飙升)。解法是混合(hybrid)策略:普通用户走写扇出(发文时预算好);少数超级大 V 走读时拉取——不预先推给所有粉丝,而是你刷新时现去拉他们的推、再和你的时间线合并。对热点,把"写时打散到千万处"换成"读时从一处拉取"。

📎 The Tail at Scale (Dean & Barroso, CACM 2013) 同样指出,Twitter 这类高扇出服务的尾延迟治理是规模化的核心命题之一。

③ Google《The Tail at Scale》:尾延迟与对冲请求的奠基(第六节)

这篇 Jeff Dean 与 Luiz Barroso 2013 年发表于 CACM 的论文,是尾延迟工程的圣经。它点破:在动辄扇出到上千个叶子节点的系统里,单个组件「偶尔慢一下」会被扇出放大成「整体几乎必然慢」,所以治理 p99/p999 至关重要。它给出的**延迟对冲(hedged requests)**实测数据极有说服力:等第一个请求超过 p95 才补发第二份,只增加约 2–5% 的额外负载,却能把 1000 个取值的 p999 从 1800ms 砍到 74ms。 这正是第六节的理论原型。

📎 The Tail at Scale 论文(PDF) | CACM 版本

④ 一致性哈希的起源(第二节):这个让大规模存储平滑扩容的核心思想,来自 David Karger 等人 1997 年的论文《Consistent Hashing and Random Trees》——最初是为「缓解万维网热点(relieving hot spots on the Web)」而生,正好对应本章「分片 + 热点」两大主题。次年(1998),论文作者 Lewin 与 Leighton 创办了 Akamai,把它变成了今天全球 CDN 的基石。而 Amazon Dynamo(承接 05)则把一致性哈希 + 虚拟节点用进了生产级 KV 存储,成为 Cassandra / DynamoDB / ScyllaDB 这一脉的源头。

📎 Consistent Hashing and Random Trees (Karger et al., STOC 1997)


🤖 AI / vibe coding 视角:原型在规模面前最先崩的地方

LLM 推理服务的扩展,本身就是本章力学的活案例:

  • 尾延迟敏感 + 扇出:模型推理服务TTFT(首字延迟)和 TPOT(每字延迟) 本质就是 p99 体验;而一个 agent 把任务拆成 N 个并发子调用(同时查多个工具、跑多个子代理),就是第六节的扇出放大——只要有一个子调用撞上长尾,整个 agent 的这一步就被拖慢。子任务越多,整体尾延迟越逼近单个子调用的 p999。
  • 热点 + 排队:推理服务的连续批处理正是在和第七节的排队论搏斗——GPU 利用率拉得越满,吞吐越高,但单请求的尾延迟(排队等批)也越高;把利用率逼到 100% 同样会让尾延迟爆炸。而一个爆火的应用,会让某个模型 / 某个 prompt 前缀成为热点。
  • 召回-延迟权衡:向量数据库 的 ANN「用一点精度换巨大速度」,和本章「用一致性 / 精确性换规模」是同一种交易——规模化处处是这种「放弃一点完美换数量级提升」的取舍。

架构智慧:vibe coding「先跑起来」的原型,在 demo 和小流量下完美无瑕——因为它从没遇到过热点、扇出、和逼近满载的排队。但它会在真实规模面前最先崩,而且崩在你最意想不到的地方:不是那个最复杂的模块,而是那个被名人 / 爆款打成热点的单一分片,那个利用率悄悄爬到 95% 的队列,那个被 100 倍扇出放大的慢副本。AI 能瞬间生成「能跑」的代码,但它不知道你的流量长什么样、热点会出现在哪、你愿意为尾延迟留多少余量——这些都是 质量属性与取舍 意义上的、依赖业务判断的取舍。预判「规模会在哪先断」,是 AI 给不了、只能由架构师下的判断。


🎯 随堂检验

🤔一个按 user_id 哈希分片的社交 Feed,某大 V 发帖后其 fan-out 列表里一个分片 CPU 飙到 100%,其它分片很闲。最对症的架构判断是?
  • A再加 10 台机器,整体扩容即可
  • B这是热点问题:分片键选错或热 key 未打散——需加盐拆分、本地缓存、只读副本或请求合并等「把一个点变成一片」的手段
  • C改回单库单表,分片本身就不该用

本章小结

  • 核心论断:加机器不是免费的,而且不是均匀的。 无状态层近乎免费,有状态层贵得吓人;而规模化不是把系统放大,是换一套力学——热点、尾延迟、排队、协调开销,会在你没想到的地方先断。
  • 垂直 vs 水平:垂直扩展简单但有天花板、不解决单点;水平扩展才是真规模化,代价是从「管一台」变成「管一群」。第一步永远是把状态从计算里挤出去。
  • 分片与一致性哈希:范围分片利于区间扫但易热点;哈希分片均匀但范围查询废。朴素哈希一加节点就全量重映射;一致性哈希让加/减节点只搬 1/N 数据,虚拟节点再抹平不均与故障冲击——这是大规模存储平滑扩容的地基。
  • 热点:负载遵循幂律,一个热 key 能让分片失效、打爆单台。打散的唯一思路是「把一个点变成一片」:加盐拆分、本地缓存、只读副本、请求合并。
  • 多级缓存:CDN→边缘→应用→分布式缓存→DB,层层挡流量,但每层副本都加一道一致性债;缓存踩踏让「正常过期」都能成洪峰,用 single-flight、随机抖动、提前刷新来防。
  • 多区域多活:买到本地性与容灾,代价是撞上跨区延迟、写冲突(LWW/CRDT/字段拆分)、就近路由——在 PACELC 上几乎必然放松强一致。多数业务不需要,先问清楚再付账。
  • 尾延迟的数学:延迟是分布不是数字,p99/p999 才是真实体验;扇出放大让整体 p99 趋近子调用的 p999——对冲请求(等 p95 再补发)只加 ~5% 负载就能砍掉长尾。
  • 排队的直觉:Little 定律(L=λW);利用率逼近 100% 时排队长度按 1/(1−ρ) 爆炸式增长——「留余量」是设计而非浪费。USL/Amdahl:协调开销让加机器收益递减甚至变负,好的规模化设计让机器尽量不需要互相说话
  • AI 时代主线:LLM 推理(尾延迟、连续批处理排队)、agent 并发子任务(扇出放大)、向量库(精度换速度)处处是本章力学;vibe coding 原型在真实规模 / 热点 / 尾延迟面前最先崩——预判「规模会在哪先断」是架构师不可替代的判断

承上启下:这一章讲的是「同一个系统怎么变大(scale up/out)」——加机器、分片、缓存、治尾延迟。但有一类规模问题,加机器解决不了:当系统功能太多、团队太大、一个代码库谁都不敢动时,你要做的不是「加机器」,而是「拆系统」。下一章 14 · 演进与拆分大型系统,我们从「机器的规模化」走向「系统与组织的规模化」:单体何时该拆、怎么沿着业务边界拆、绞杀者模式如何安全演进,以及为什么「康威定律」决定了你的架构终将长成你组织的样子。

💬 评论