Chapter 10
自测题库
前九章建立了从无状态扩展到有状态切分、复制、一致性、事务、横切手段与前沿的完整工具箱。本章不教新东西,只做一件事:把这套工具从“读得懂”逼到“判得出”——能不能在一个具体场景里说清瓶颈在哪、该牺牲什么。
本章建立的 schema
- 三层梯度:概念层验证术语,原理层验证机制,应用判别层验证选型。
- 真正的检验在判别层——把跨章的两个相似场景放在一起,逼出“为什么这个选 A、那个选 B”。
- 所有答案集中在文末一个折叠块里;做题时先合上答案、写在纸上,再展开核对。
- 最后用“凭记忆画整套主线图”收口——画得出,才算真把这条线长进了脑子。
这一章是回收站,也是体检站。前九章每一章末尾都有自测题与挑战;那些题贴着各自章节的上下文,刚读完时答得出很正常。这里的题刻意把上下文撤掉、把相似的概念并排放,看的是脱离原文之后还剩下多少。
先读这条:三个“假学会”的信号
翻到答案之前,留意这三种自我感觉——它们最会骗人:“这题我一看就会”,多半是题型眼熟,不是机制想透了;“答案我背得出”,背的是结论,遇到换皮场景就接不上;“判别层我也能答”,要分清是真的按“切分/复制”这把尺子推出来的,还是凑巧蒙对了选项。检验办法只有一个:每道题先在纸上写出完整的因果链(因为 X,所以代价是 W,在 V 时失效),再展开答案逐句对。
10.1怎么用这套题
题目分三层,对应三种不同的“会”。这三层不是难度递增那么简单——它们检验的是不同的东西,下面这张图把关系画出来。
- 概念层(对应 01–04):验证术语指的是什么。答这层应当几乎不假思索;卡住说明某个基础术语还是空壳。
- 原理层(对应 05–08):验证机制为什么成立。答这层要能说出“靠什么保证、什么时候失效”,而不只是名字。
- 应用判别层(跨章):验证能不能选型。每题给两个相似但答案不同的场景,逼出区分它们的那条理由。这层是本教程的目的所在。
做法
准备纸笔。每道题先合上文末答案,把因果链写下来再展开核对——只在脑子里“想一遍觉得会”不算数,那正是上面警告里的第一个信号。概念层与原理层应当几乎全对;判别层若有犹豫,回对应章节重读那一节的对比表,而不是直接背答案。
10.2概念层(01–04)
这一层只问“这个词到底指什么、它的边界在哪”。答案全部集中在文末折叠块,先自己写。
- C1. 为什么“无状态 vs 有状态”是整个分布式难度的分水岭?无状态层和有状态层的扩展方式各是什么?(01–02)
- C2. 一致性哈希的“增删节点只搬约 1/N 的 key”这条保证,和虚拟节点带来的“负载均匀”,分别解决的是哪个问题?为什么前者解决不了后者?(03)
- C3. 范围分区和哈希分区各自牺牲了什么、换来了什么?时间序列数据用范围分区会出什么事?(03)
- C4. quorum 写 W 个、读 R 个,为什么 W+R>N 能保证读到最新写入?它在什么情况下这条保证会破?(04)
- C5. 复制延迟(异步从落后于主)会破坏哪三个读保证?各举一个用户能感知到的现象。(04)
- C6. 主从、多主、无主 quorum 三种复制拓扑,哪一种必然要处理写写冲突?为什么?(04)
很多人把“一致性哈希”直接等同于“负载均衡,能把流量均匀分散”。如果裸一致性哈希(不加虚拟节点)真能均匀分散,那虚拟节点存在的意义是什么?想清楚再看 C2 答案。
10.3原理层(05–08)
这一层问“机制靠什么成立、什么时候失效”。能背出结论不够,要能复原那条因果链。
- P1. CAP 到底说了什么、没说什么?“三选二、设计期声称自己是 CP 系统”这个常见说法错在哪?(05)
- P2. 为什么 N 个节点里任意两个多数派一定相交?这个相交保证了什么具体的安全性质?(05)
- P3. 2PC 的协调者为什么是阻塞式的单点?把协调者做成多副本(复制它)能不能解决这个阻塞?为什么?(06)
- P4. 为什么“exactly-once”在传输层根本不存在?工程上所谓的 effectively-once 是靠什么两件东西拼出来的?(07)
- P5. 熔断器真正保护的是什么?它对一个“变慢但没死”的依赖,比单纯的超时多做了什么?(08)
- P6. 为什么带 TTL 的分布式锁本身不安全?fencing token 凭什么是安全的?关键区别在哪一端做校验?(08)
- P7. 纯指数退避(不加随机抖动)为什么会让过载更严重?加 full jitter 之后为什么反而更好?(08)
- P8. PACELC 比 CAP 多补了哪半句?为什么说“很多产品选最终一致其实跟分区无关”?(05)
直觉会说“单点容易挂,那就复制成多副本不就行了”。但 2PC 阻塞的根源是参与者投了 YES 之后进入 in-doubt 持锁等待。复制协调者解决的是“协调者进程消失”,它能不能解决“参与者不知道最终该 commit 还是 abort、只能一直持锁”这个窗口?答案在 P3。
10.4应用判别层(跨章场景)
这是本章的核心,也是检验“真懂”的地方。每道题给两个相似场景,答案不同——能说清把它们分开的那条理由,才算过关。每题都标了涉及哪些章,答不出就回那几章。
先看下面这张图:判别层的每道题不是落在单独一章,而是横跨多章,把前面建立的尺子串起来用。
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)。