Chapter 10

自测题库

前九章建立了从无状态扩展到有状态切分、复制、一致性、事务、横切手段与前沿的完整工具箱。本章不教新东西,只做一件事:把这套工具从“读得懂”逼到“判得出”——能不能在一个具体场景里说清瓶颈在哪、该牺牲什么。

本章建立的 schema

  • 三层梯度:概念层验证术语,原理层验证机制,应用判别层验证选型。
  • 真正的检验在判别层——把跨章的两个相似场景放在一起,逼出“为什么这个选 A、那个选 B”。
  • 所有答案集中在文末一个折叠块里;做题时先合上答案、写在纸上,再展开核对。
  • 最后用“凭记忆画整套主线图”收口——画得出,才算真把这条线长进了脑子。

这一章是回收站,也是体检站。前九章每一章末尾都有自测题与挑战;那些题贴着各自章节的上下文,刚读完时答得出很正常。这里的题刻意把上下文撤掉、把相似的概念并排放,看的是脱离原文之后还剩下多少。

先读这条:三个“假学会”的信号

翻到答案之前,留意这三种自我感觉——它们最会骗人:“这题我一看就会”,多半是题型眼熟,不是机制想透了;“答案我背得出”,背的是结论,遇到换皮场景就接不上;“判别层我也能答”,要分清是真的按“切分/复制”这把尺子推出来的,还是凑巧蒙对了选项。检验办法只有一个:每道题先在纸上写出完整的因果链(因为 X,所以代价是 W,在 V 时失效),再展开答案逐句对。

10.1怎么用这套题

题目分三层,对应三种不同的“会”。这三层不是难度递增那么简单——它们检验的是不同的东西,下面这张图把关系画出来。

三层自测梯度金字塔 应用判别层 跨章场景:在两个相似情形里选型并说清牺牲 检验“真懂” · 全教程的分水岭 原理层 机制为什么成立 · 对应 05–08 概念层 术语指什么 · 对应 01–04 题少 题多 权重递增 能力逐层叠加
图 10.1三层不是难度阶梯,而是三种“会”:概念层会说名字,原理层会讲机制,判别层会做选择。注意:上两层只是地基,真正区分“背了一堆名词”和“真懂”的是最宽的那一层——判别层。地基答得再快,判别层答不出,就是还没学会。
  • 概念层(对应 01–04):验证术语指的是什么。答这层应当几乎不假思索;卡住说明某个基础术语还是空壳。
  • 原理层(对应 05–08):验证机制为什么成立。答这层要能说出“靠什么保证、什么时候失效”,而不只是名字。
  • 应用判别层(跨章):验证能不能选型。每题给两个相似但答案不同的场景,逼出区分它们的那条理由。这层是本教程的目的所在。

做法

准备纸笔。每道题先合上文末答案,把因果链写下来再展开核对——只在脑子里“想一遍觉得会”不算数,那正是上面警告里的第一个信号。概念层与原理层应当几乎全对;判别层若有犹豫,回对应章节重读那一节的对比表,而不是直接背答案。

10.2概念层(01–04)

这一层只问“这个词到底指什么、它的边界在哪”。答案全部集中在文末折叠块,先自己写。

  1. C1. 为什么“无状态 vs 有状态”是整个分布式难度的分水岭?无状态层和有状态层的扩展方式各是什么?(01–02)
  2. C2. 一致性哈希的“增删节点只搬约 1/N 的 key”这条保证,和虚拟节点带来的“负载均匀”,分别解决的是哪个问题?为什么前者解决不了后者?(03)
  3. C3. 范围分区和哈希分区各自牺牲了什么、换来了什么?时间序列数据用范围分区会出什么事?(03)
  4. C4. quorum 写 W 个、读 R 个,为什么 W+R>N 能保证读到最新写入?它在什么情况下这条保证会破?(04)
  5. C5. 复制延迟(异步从落后于主)会破坏哪三个读保证?各举一个用户能感知到的现象。(04)
  6. C6. 主从、多主、无主 quorum 三种复制拓扑,哪一种必然要处理写写冲突?为什么?(04)
先预测 · C2 的常见混淆

很多人把“一致性哈希”直接等同于“负载均衡,能把流量均匀分散”。如果裸一致性哈希(不加虚拟节点)真能均匀分散,那虚拟节点存在的意义是什么?想清楚再看 C2 答案。

10.3原理层(05–08)

这一层问“机制靠什么成立、什么时候失效”。能背出结论不够,要能复原那条因果链。

  1. P1. CAP 到底说了什么、没说什么?“三选二、设计期声称自己是 CP 系统”这个常见说法错在哪?(05)
  2. P2. 为什么 N 个节点里任意两个多数派一定相交?这个相交保证了什么具体的安全性质?(05)
  3. P3. 2PC 的协调者为什么是阻塞式的单点?把协调者做成多副本(复制它)能不能解决这个阻塞?为什么?(06)
  4. P4. 为什么“exactly-once”在传输层根本不存在?工程上所谓的 effectively-once 是靠什么两件东西拼出来的?(07)
  5. P5. 熔断器真正保护的是什么?它对一个“变慢但没死”的依赖,比单纯的超时多做了什么?(08)
  6. P6. 为什么带 TTL 的分布式锁本身不安全?fencing token 凭什么是安全的?关键区别在哪一端做校验?(08)
  7. P7. 纯指数退避(不加随机抖动)为什么会让过载更严重?加 full jitter 之后为什么反而更好?(08)
  8. P8. PACELC 比 CAP 多补了哪半句?为什么说“很多产品选最终一致其实跟分区无关”?(05)
先预测 · P3 的陷阱

直觉会说“单点容易挂,那就复制成多副本不就行了”。但 2PC 阻塞的根源是参与者投了 YES 之后进入 in-doubt 持锁等待。复制协调者解决的是“协调者进程消失”,它能不能解决“参与者不知道最终该 commit 还是 abort、只能一直持锁”这个窗口?答案在 P3。

10.4应用判别层(跨章场景)

这是本章的核心,也是检验“真懂”的地方。每道题给两个相似场景,答案不同——能说清把它们分开的那条理由,才算过关。每题都标了涉及哪些章,答不出就回那几章。

先看下面这张图:判别层的每道题不是落在单独一章,而是横跨多章,把前面建立的尺子串起来用。

判别题到章节的映射 03 分区 04 复制 05 一致性 06 事务 07 缓存异步 08 容错 D1 余额 vs 浏览计数 D2 跨服务 下单事务 D3 分区键 用户 vs 订单 D4 慢下游 线程耗尽 D5 新项目 要不要网格
图 10.2每道判别题(下排红框)都向上连到它涉及的章节(上排)——没有一道只靠单章就能答全。注意:D5 还回扣 09 前沿(图外),D3 把 03 分区和 08 热 shard 串起来——判别力来自跨章串联,不是单点记忆。

D1 | 全球用户的账户余额 vs 商品浏览计数(涉及 04 复制 + 05 一致性)

同一套基础设施上有两份数据:一是用户账户余额,全球用户都要读写;二是商品详情页的浏览次数。两份数据各该选强一致还是最终一致?为什么相似的“都要全球可读”却给出相反的答案?强一致那份的代价落在哪(用 PACELC 说)?

D2 | 下单要扣库存 + 扣余额,跨两个服务(涉及 06 事务,回扣 05 一致性)

下单操作要同时调用库存服务扣库存、账户服务扣余额,这是一次跨两个服务的写。在 2PC、Saga、TCC 之间怎么选?各自牺牲什么(隔离性、锁、开发成本)?如果业务能接受“先扣了库存,余额不足再补偿退回”,倾向哪个?为什么这里几乎不该用跨服务 2PC?

D3 | 用户表按用户 ID、订单表常按时间范围查(涉及 03 分区,回扣 08 热 shard)

用户表的主要访问是“按用户 ID 取单个用户”;订单表的主要访问是“查某段时间内的订单”。两张表各该用哈希分区还是范围分区?如果订单按时间范围分区,写入会集中到哪、为什么会出热点?这个热点该怎么缓解(回扣 08)?

D4 | 一个慢下游导致上游线程耗尽(涉及 08 容错,回扣 07 背压)

某个下游依赖变慢了(没挂,就是响应从几十毫秒涨到几秒),结果上游的线程池被这些慢调用占满,整站跟着雪崩。熔断、舱壁、限流这三样各解决问题的哪一面?该怎么组合?如果上游还在不停往一个无界队列里塞任务等下游,这跟 07 章的背压是什么关系?

D5 | 新项目要不要一上来就上微服务 + 服务网格(涉及 09 前沿)

一个全新项目、团队不大,有人提议“现在都微服务 + 服务网格,我们一开始就按这个架构搭”。该怎么判断这是不是过早?要问哪几个关键问题?“共享一个数据库的多个微服务”为什么往往是最糟的中间态?

亲手画图(合上教程做)

把教程合上,拿一张白纸,凭记忆画出整套主线图:顶部是“无状态 vs 有状态”的分水岭,向下分成“切分(分区)”和“复制”两支,两支汇到“一致性 / CAP”,再往下到“分布式事务”;最后在旁边标出“缓存与异步、容错、可观测性”这三块横切关注点挂在哪里。画完做一件事——回 index 的概念地图对照:你的图里有没有画出“一致性问题是切分 + 复制共同的下游”这条边?如果漏了它,说明这条主线还没真正连起来,回去重读 03→04→05 的衔接段。

综合挑战

把五道判别题串成一个系统

设想一个“全球电商”:要支持全球用户下单(D2)、账户余额强一致(D1)、订单与用户数据要分区(D3)、大促时下游会变慢(D4),团队正在纠结架构粒度(D5)。在一页纸上画出这个系统的数据层与服务层,并对每个关键组件标注:它存的是什么状态、怎么切分、怎么复制、选了什么一致性级别、用什么扛流量与故障。把五道题的答案织进一张图——这一题没有标准答案,能自洽地说清每个权衡就算过。提示见下。

展开思路提示

从“哪些数据是强一致刚需”起手:余额、库存计数是;浏览数、推荐、搜索排序基本是最终一致即可。强一致那部分用 quorum 或分布式 SQL(09)承载,并接受其 PACELC 的 EL/EC 延迟代价。跨服务的下单用 Saga + outbox(06),消费者带幂等键(07)。分区键按访问模式分别选(D3),热点用户/爆款叠前置缓存与加盐(03+08)。每个对外依赖配超时预算 + 舱壁 + 熔断(08),缓存全部带 TTL 兜底(07)。架构粒度先按团队边界粗切、避免共享库(D5/09)。能把这些挂到一张图上且不自相矛盾,就达到了本教程的目标。

10.5对完答案之后

概念层、原理层若有错,回对应章节重读那一节的机制段,别只记结论——结论会忘,因果链不会。判别层若答得犹豫,问题通常不在某一章,而在章与章之间的衔接没打通:04 复制为什么引出 05 一致性、06 事务为什么是 05 的应用、08 的热 shard 为什么回指 03。把这些衔接句重读一遍,比重做十道题有用。

判别层全部能自洽作答、并且能把上面那张主线图凭记忆画出来,就说明这套尺子已经长进脑子——遇到一个没见过的“加机器也扛不住”的场景,能立刻判断瓶颈在无状态层还是有状态层,并说清每个选择牺牲了什么。这正是这份教程要交付的能力。

全部答案

先合上这一节,把每道题的因果链写在纸上,再展开逐句核对。答案按因果链给出(靠什么成立、代价是什么、何时失效),不只给结论。

展开 · 概念层答案 C1–C6

C1. 无状态副本不持有客户端数据,任意副本能服务任意请求,所以扩展它只是“加机器 + 负载均衡”,近线性增长且没有数据迁移;有状态节点拥有数据本身,单机放不下就被迫切分(分区),一台挂了会丢就被迫复制——分布式所有硬问题(热点、再平衡、一致性、共识)都是“把 state 搬到多台机器”的下游。分水岭就在有没有 state。

C2. “少搬”解决的是成员变更时的迁移量:环上增删一个节点只重映射相邻弧段的 key(约 1/N),而不是像 hash-mod-N 那样几乎全部重排。“均匀”解决的是稳态下的负载分布。前者解决不了后者,因为裸一致性哈希只保证“变更时少动”,节点在环上随机落点会让弧段长短不一、负载天然不均;虚拟节点让每个物理节点占 v 个环上点,弧段被打碎后均值收敛,同时节点挂掉时它的负载分摊到多个后继而非整段砸给唯一后继。

C3. 范围分区保持 key 有序,换来范围扫描局部化(一次扫描落在少数分区),牺牲的是均匀性——时间序列/自增 ID 这类单调递增 key 会全部落到“最后一个分区”形成写热点。哈希分区把 key 打散,换来负载均匀、消灭热点,牺牲的是范围扫描局部性(范围查询变成 scatter-gather 打所有分区)。本质是“扫描效率 ↔ 均匀负载”的对换。时间序列用范围分区 = 写全打最后一片。

C4. 写集合大小 W、读集合大小 R,当 W+R>N 时,任意一个读集合与任意一个写集合在 N 个副本里必有交集(鸽巢原理:两个子集大小之和超过全集就必相交),交集里至少一个副本持有最新版本,靠版本号识别即可读到最新写。失效情形:sloppy quorum——分区时写落到了正常 R 集之外的“兜底(hinted handoff)”节点,读写集合不再保证相交,W+R>N 给的是安全错觉;陈旧靠 read-repair + anti-entropy 后台修复。

C5. 破坏三个读保证:① read-your-writes(读到自己刚写的)——用户改了头像,刷新还是旧头像(读到了落后的从);② monotonic reads(读不回退)——连刷两次,第二次反而比第一次旧(两次落到进度不同的从);③ consistent prefix(因果前缀一致)——先看到回复、后看到原帖(不同分区复制进度不一)。缓解:写后读主、按用户粘连路由、因果追踪。

C6. 多主必然要处理写写冲突。因为多个节点都能接受写、再互相复制,两个节点可能并发改同一行,复制汇合时就出现冲突,必须用 LWW / 版本向量 / CRDT 解决。主从只有单个写入点(无冲突,代价是主成写瓶颈 + 切换空窗);无主 quorum 也可能并发写不同副本,因此同样需要冲突解决(这点与多主类似),但其核心机制是 W+R>N 的读时取最新。

展开 · 原理层答案 P1–P8

P1. CAP 说的是:当网络分区(丢消息)真的发生时,一个节点要么用本地可能陈旧的状态回答(保 A 弃 C),要么拒绝回答以免给出不一致结果(保 C 弃 A),不能两者兼得——因为一侧的写传不到另一侧。CAP 没说的是健康时的事,也没说可以“设计期三选二”:P(分区)在真实网络里不是可选项而是迟早会发生的事实,所以真正的取舍只在分区那一刻、在 C 与 A 之间。“我是 CP 系统”当成日常设计标签是误读——分区很罕见,天天疼的是 PACELC 的 else 支(见 P8)。另外 CAP 的 A 定义很严格(每个非故障节点都要响应),多数派系统在少数派侧返回错误按 CAP 定义其实“不可用”,但日常仍称其高可用。

P2. 多数派定义为 ⌊N/2⌋+1 个节点,两个多数派大小之和 = 2×(⌊N/2⌋+1) > N,按鸽巢原理它们必有至少一个公共节点。这个相交是安全机制而非概率:新 leader 的选举多数派与上一次提交多数派相交 → 新 leader 必然看到所有已提交条目 → 已提交数据不丢;同时不可能有两个 leader 各自凑齐多数派(否则两个多数派之和 ≤ N,矛盾)→ 不会脑裂双主;少数派侧凑不齐多数派只能停摆,不能擅自分叉历史。

P3. 阶段一参与者投 YES 后就锁住资源进入 in-doubt 状态,把最终是 commit 还是 abort 的决定权完全交给了协调者;如果协调者在发出阶段二决议前崩溃,参与者无法单方判断该 commit 还是 abort(两种都可能是正确答案),只能一直持锁等协调者回来——这是按协议构造产生的阻塞。复制协调者解决不了:复制能让“协调者进程”不消失,但 in-doubt 的参与者等的是“决议”,只要决议还没产生并送达,持锁窗口就存在;复制顶多缩短协调者不可用时间,消不掉这个语义窗口。这正是跨微服务弃用 2PC、改用 Saga/outbox 的根因。

P4. exactly-once 在传输层不可能,因为“丢了 ack”和“丢了消息”从发送方看无法区分(两将军问题)——既然分不清,就必须重试,重试就可能重复。工程上的 effectively-once = at-least-once(重试到 ack,保证不丢)+ 幂等/去重(保证重复无害)两件东西拼出来的:传输层负责不丢,幂等键 + 去重表负责把重复折叠成一次。Kafka 的 exactly-once 也只在 Kafka 内部成立,一旦消费者写外部 DB/调 RPC,外部副作用仍需自己的幂等键(去重表必须和副作用同事务)。

P5. 熔断器真正保护的是调用方自己的资源(线程、连接),不是检测下游故障。对一个“变慢但没死”的依赖:单纯超时仍会先占住一个线程等到超时才放(慢调用堆积照样能打满线程池);熔断器跳到 OPEN 后连请求都不发、立即本地快速失败,于是线程根本不被占用 → 一个慢依赖不至于拖垮上游整个线程池。HALF-OPEN 再放少量探针试探恢复。一句话:超时是“等不到就放手”,熔断是“预判会等不到,干脆不发”。

P6. 带 TTL 的锁不安全,因为持锁者可能在 TTL 内被 GC / VM 暂停冻住,锁到期被另一客户端拿走,等它恢复时仍以为自己持锁 → 两个客户端同时“持锁”写同一资源 → 损坏。fencing token 安全在于:每次获取锁发一个单调递增的令牌,写请求带上令牌,由存储端(被保护的资源那一端)校验并拒绝比已见最大值更小的令牌——陈旧持锁者带的是旧(更小)令牌,会被存储端主动拒掉。关键区别就在校验发生在存储端而非依赖客户端自觉,且不依赖时钟。

P7. 纯指数退避让所有同时失败的客户端按同一个确定的时间表重试 → 它们被同步到同一个重试时刻,下一拨重试又一起砸过来、再次过载,形成周期性惊群。加 full jitter(在 [0, cap] 区间均匀随机取延迟)把这些重试在时间轴上打散 → 错峰、削平瞬时峰值。反直觉之处在于:这里随机性(不确定)才让系统更好,确定性反而是 bug。

P8. PACELC 补的是 CAP 漏掉的 else 半句:分区时在 A 与 C 间选(同 CAP);否则(Else,网络健康,约 99.99% 的时间)在延迟 L 与一致性 C 间选。强一致要等 quorum 往返 → 贵在延迟;最终一致就近副本立即答 → 快。很多产品选最终一致跟分区无关,是为了砍尾延迟(走 PACELC 的 else 支),分区那半句一年也用不上几次。例:DynamoDB = PA/EL,Spanner/HBase = PC/EC。

展开 · 应用判别层答案 D1–D5

D1(账户余额 vs 浏览计数). 余额选强一致:读到陈旧余额会导致超额扣款、对账错误,错误代价高且不可接受。浏览计数选最终一致:少算/晚算几次无伤大雅,换来低延迟与高可用。相同的“全球可读”却给相反答案,区别在读到旧值的代价——这正是按数据、按操作选一致性的尺子。强一致那份的代价用 PACELC 说是落在 else 支的 L:健康时也要等 quorum/全局时间戳往返,付出延迟(Spanner 这类 PC/EC);分区时还要在少数派侧牺牲可用性。涉及 04(复制拓扑与 quorum)+ 05(一致性模型与 PACELC)。

D2(跨服务下单事务). 首选 Saga + outbox。跨微服务不该用 2PC:协调者是阻塞单点,一个参与者 GC 或协调者重启就秒级持锁、拖垮线程池(见 P3)。Saga 用一串本地事务 + 反向补偿换掉全局锁,可扩展,代价是牺牲隔离(中间状态可见的脏读)且补偿是语义回滚而非真回滚;若业务能接受“先扣库存、余额不足再补偿退回”,正合 Saga。TCC 隔离更强(Try 把资源预留为 pending 藏住中间值),但每个服务要实现三个幂等接口 + pending 簿记,开发成本高,适合对中间态可见性敏感的场景(如金额预留)。双写(DB + 发消息)必须用 outbox 保证原子,消费者带幂等键。涉及 06(2PC/Saga/TCC/outbox),回扣 05(事务是一致性的应用)。

D3(分区键选择). 用户表按用户 ID 哈希分区:主要访问是按 ID 取单个用户,哈希打散负载均匀、单点查询直达一个分区。订单表按时间范围查 → 范围分区能让时间区间扫描局部化,但时间是单调递增的,新写入会全部落到“当前时间”所在的最后一个分区 → 写热点。缓解(回扣 08 热 shard):给分区键加盐 / 复合键(如 时间桶 + 用户ID 前缀,把同一时刻的写打散到多个分区),或写入侧前置缓冲,读取侧并行扫多个子分区再合并。涉及 03(范围 vs 哈希),回扣 08(热 shard 加盐/前置缓存)。

D4(慢下游致线程耗尽). 三样各管一面:舱壁给这个依赖独立的线程/连接池 → 它打满只饿死自己那个池、不拖垮整进程;熔断在错误率/慢调用超阈时跳 OPEN、连请求都不发 → 不再有线程被慢调用占住(见 P5);限流从入口侧控制进来的速率、保护下游不被压垮。组合:入口限流 + 每依赖舱壁隔离 + 对该依赖加熔断 + 超时预算沿链传播 + 降级兜底。如果上游还往无界队列里塞任务等下游,那是缺背压(07)——队列要有界,满了就早拒(429/503)或向上游发“慢点”信号,而不是无界缓冲直到 OOM。涉及 08(熔断/舱壁/限流/超时),回扣 07(背压与有界队列)。

D5(新项目要不要微服务+网格). 多半过早。要问三个问题:① 团队规模与边界——有没有多个团队需要独立部署?没有就先模块化单体,微服务的收益是组织解耦,小团队拿不到却要付全部分布式代价。② 是否真有独立伸缩/独立发布的需求——没有就别拆。③ 能不能保证每个服务有独立数据库——做不到就别拆。“共享一个数据库的多个微服务”是最糟中间态(分布式单体):付了网络延迟 + 部分失败的复杂度,却拿不到独立部署的好处。服务网格同理,先确认有 mTLS/多语言治理/精细流量控制的真实需求,再考虑(且优先 ambient 模式省资源)。涉及 09(微服务边界回潮、分布式单体、服务网格 sidecar→ambient)。

参考资料

  • Martin Kleppmann, Designing Data-Intensive Applications(DDIA)— 全书,本题库的概念与原理底座。
  • Google, Site Reliability Engineering & The SRE Workbook — 容错、SLI/SLO、负载均衡的综合延伸。sre.google/books
  • 各章末尾来源:CAP(Gilbert & Lynch;Brewer 2012)、PACELC(Abadi)、Raft(raft.github.io)、Dynamo 论文、AWS Builders' Library(timeouts/retries/jitter、caching、shuffle-sharding)。
  • 对照练习时可逐题回到本教程对应章节的“备选方案对比表”重读机制段。