Chapter 01

地基:为什么分布式系统这么难

这是整套教程的第一章,所以没有上一章可回顾——但有一件事必须先摆上桌:单机时代你不用操心的事,分布式里全要重新算账。进程内一次函数调用永不失败、永不超时、永不返回到一半,内存里的数据永远是最新的;可一旦把这次调用拉到网线两端,它随时可能丢失、迟到、或者只成功了一半而你不知道。这一章的任务,是先认清这种代价从哪来,再给后面八章准备好四把量尺。

本章建立的认知

  • 分布式系统的难,根子不在"机器多",而在状态(数据)必须跨机器存在。
  • 网络、节点、时钟都不可靠——八条经典谬误各自藏着一条没人替你处理的故障路径。
  • 会用延迟数量级阶梯和容量估算给一个设计粗算账,而不是等上线翻车才知道。
  • CAP / PACELC 是贯穿全书的权衡总框架——本章先把框架立起来,05 章再精读。

1.1单机的便宜,是分布式的账单

单机程序里有一组隐性福利,写代码的人几乎从不为它们买单:函数调用要么返回、要么抛异常,不存在"调用出去了但不知道有没有成功"这种第三态;进程内读到的变量就是最新值,没有"别的机器上其实已经改过了"的问题;时间由一个时钟说了算,事件先后一目了然。这些福利的总价,在单机上是零。

把同一段逻辑拆到两台机器之间,账单立刻到期。一次远程调用可以发出去却收不到回应——这时它到底执行了没有,调用方无法区分"请求没送到"和"响应没回来",而这两种情况的正确补救动作恰好相反(前者该重试,后者重试就会重复执行)。这个"不知道成没成功"的灰色地带,是单机世界里根本不存在的新故障类别,也是后面几乎所有难题的源头。

本章主线

真正把账单推高的,是状态。无状态的东西可以随便克隆、随便扔——它本身不存数据,丢一个再起一个就是了。一旦某个节点拥有数据,问题就质变:数据太大要切分(分区,03 章),数据不能只有一份要复制(04 章),而切分带来热点、复制带来不一致。本教程后面所有章节,都是在"切分 + 复制状态"这两件事上做权衡。这一章先把这条主线点明,再交给你算账的工具。

1.2分布式计算的八大谬误

1994 年起,Sun 的 Peter Deutsch 与 James Gosling 等人陆续总结出八条"新手默认相信、但事实相反"的假设,合称分布式计算八大谬误(Fallacies of Distributed Computing)。它们之所以叫"谬误",不是因为偶尔出错,而是因为只要系统跑得够久、规模够大,每一条都一定会被现实违反一次。

把每条谬误翻过来读,它就变成一条设计清单:每个被默认相信的假设,背后都对应一条没人替你处理的故障路径;工程师的活,就是为这条路径的"反面"提前准备超时、重试、限额或降级。

八大谬误 → 它否认的现实 → 对应的设计动作
谬误(默认相信)现实(一定会发生)设计动作(为反面准备)
网络是可靠的丢包、断连、半开连接随时发生;调用方分不清"没送到"和"没回来"给每次远程调用配超时;重试要配合幂等(07 章)
延迟为零跨机房、跨地域的往返被物理钉死,不会随 CPU 变快而消失合并请求、就近读、缓存(见 1.3)
带宽无限大 payload、扇出查询会打满链路,引发排队和超时分页、压缩、限制扇出、背压(07 章)
网络是安全的链路会被窃听、伪造、重放默认加密(mTLS)、鉴权、最小权限
拓扑不会变节点扩缩容、IP 漂移、容器重调度天天发生服务发现、健康检查、不写死地址
只有一个管理员多团队多组件各自变更,配置会互相打架配置即代码、版本化、灰度发布
传输成本为零序列化、加解密、跨网调用都要花 CPU 和钱批量、就近、按成本选协议
网络是同质的不同链路的协议、MTU、延迟特性各不相同不假设统一行为,按最差链路设计超时

这张表会在后面反复回来。08 章讲容错时,"网络可靠"这一条的反面会变成熔断与舱壁;07 章讲异步时,"带宽无限"的反面会变成背压;而"延迟为零"是下一节的主角。

1.3无状态 vs 有状态:难度的分水岭

扩一个无状态服务,只是"加机器 + 负载均衡";扩一个有状态服务,被迫切分它、复制它——分布式所有硬问题都长在这道分水岭的右边。

无状态(stateless)指一个服务在两次请求之间不保留任何与某个客户端绑定的数据。它需要的状态要么在请求里带着,要么去外部存储(数据库、缓存)现取。这样的副本彼此完全可互换:任意一个副本都能服务任意一个请求,结果都一样。可互换意味着可自由克隆——要扛更多流量,复制出 N 个一模一样的副本、前面放个负载均衡器分流即可,容量近线性增长。这就是 02 章的全部主题,也是整套教程里"容易的一半"。

有状态(stateful)指节点本身拥有一份别处没有的数据——典型就是数据库、缓存、消息队列。有状态节点不可互换:用户 A 的数据存在节点 3 上,请求就只能去节点 3,去节点 5 拿不到。一旦不可互换,"再克隆一个"就不再是答案:

  • 数据大到单机放不下,就必须把它切成若干份分到多台机器——这是分区(03 章),代价是跨分区查询变难、某一片可能成为热点。
  • 数据只放一台机器,机器一挂就全没了,就必须复制成多份——这是复制(04 章),代价是多份副本之间会出现不一致。
  • 切分加复制之后,同一份数据在多台机器上有了多个版本,于是引出"还能拿到什么一致性保证"——这是 05 章,也是 CAP / PACELC 真正发力的地方。
无状态副本可互换,有状态节点不可互换 无状态:副本可互换 负载均衡器 副本 无数据 副本 无数据 副本 无数据 任意请求 → 任意副本,结果一样 扩容 = 加机器 有状态:节点不可互换 节点 1 数据 A–H 节点 2 数据 I–P 节点 3 数据 Q–Z 查 key="M" 只能去节点 2 扩容 = 切分 + 复制 切分→热点 · 复制→不一致
图 1.1左边三个副本完全相同、可任意替换,扩容就是加机器;右边三个节点各持一段数据、不可替换,扩容被迫切分加复制。注意:分水岭不是"机器多少",而是节点是否拥有别处没有的数据——右边那条"切分→热点 · 复制→不一致"是后面整本书要还的债。
常见错误

把"有状态服务难扩"误当成"性能问题",于是堆配置、加内存。真正的难点是状态的位置:数据在哪台机器上、有几份、哪份是新的。02 章会讲一个关键手法——把伪装成"有状态"的服务(比如把 session 留在本机内存)无状态化,把状态外移到数据库或缓存,让它重新落回分水岭的左边、变得可自由克隆。

1.4延迟数量级阶梯:被光速钉死的那堵墙

"延迟为零"是八大谬误里最贵的一条。要替它准备反面,先得知道各级操作到底差几个数量级。下面这组数字源自 Jeff Dean 广为流传的"每个工程师都该知道的延迟数字",量级比精确值更重要——关键是它们彼此之间相差几个零。

延迟数量级阶梯(对数刻度) 延迟(对数刻度,越往上越慢) L1 缓存引用 ≈ 1 ns 内存引用 ≈ 100 ns(约 100×L1) SSD 随机读 ≈ 16 µs 同机房网络往返 ≈ 0.5 ms 跨洲网络往返 ≈ 150 ms 光速墙:跨洲 RTT 降不下来 缓存命中比同机房调用快约 1000×
图 1.2每往上一格,延迟通常跳一个或几个数量级;最上面那条跨洲往返被光速钉在地上。注意:NVMe 让 SSD 这一格降了 5–10×,但跨洲那一格不会随硬件进步而下降——地理距离是物理墙,所以"就近 + 缓存"才会统治延迟预算,而不是"换更快的盘"。

这张阶梯里藏着两条会贯穿全书的机制:

① 跨洲往返是一堵搬不走的墙

北京到弗吉尼亚单程约 1.1 万公里,光在光纤里大约 20 万公里/秒,单程理论下限就有约 55 ms,往返翻倍——再加上路由跳数和排队,150 ms 量级是被物理定律而非工程水平决定的。换更快的 CPU、更新的网卡,都动不了这个数。这就是为什么"多地域部署"是一堵不可移动的延迟墙:任何要求"强一致 + 全球低延迟"的设计,都在和光速对赌(05 章会看到 Spanner 用原子钟硬刚这堵墙的代价)。

② 缓存命中为什么能快约 1000×

内存引用约 100 ns,同机房一次网络往返约 0.5 ms(= 500,000 ns)——差了约 3 个数量级。这意味着,把一次"跨网去数据库取"换成"本地内存命中",延迟省下的不是几个百分点,而是上千倍。这条数量级差,就是 07 章缓存与"就近原则"得以统治延迟预算的根本原因:缓存不是锦上添花的优化,它是用一个数量级差去对冲八大谬误里"延迟为零"那一条。

1.5容量估算:上线前先算一遍账

八大谬误里"带宽无限"和"延迟为零"的反面,最终都要落到一个具体数字上:这套系统要扛多少 QPS、存多少数据、占多少带宽。架构评审里区分"拍脑袋"和"算过账"的,就是这套信封背面(back-of-envelope)估算。四条公式不需要精确,只需要量级对。

四条信封背面估算公式
要估什么公式关键点
平均 QPS日活 × 人均每日操作数 ÷ 86400一天 86400 秒,记牢这个常数
峰值 QPS平均 QPS × 峰值比峰值比:SaaS ≈ 2×、有突发的 ≈ 5–10×、秒杀 ≈ 100×
存储记录数 × 单条大小 × 保留期 × 副本数 × 1.3×1.3 给索引和元数据留余量;副本数别漏
带宽QPS × 单条报文字节 × 8×8 是把字节换成比特;按峰值 QPS 算

上面这行被标红的"峰值 QPS"是最常翻车的一格:按平均容量备机、却被峰值打爆,是经典事故。平均值告诉你"全天摊下来的负载",但机器是在某一瞬间同时被打的——晚高峰、活动开始、明星发动态,流量会在几分钟内冲到平均值的数倍甚至上百倍。容量必须按峰值备,不是按平均备。

先预测,再展开

一个 Feed 产品日活 100 万,人均每天 50 次操作(刷新、点赞、发帖等)。平均 QPS 大约是多少?如果峰值按 10× 备,要准备多少 QPS 的容量?先在纸上算,再展开。

展开估算过程与答案

平均 QPS = 1,000,000 × 50 ÷ 86400 ≈ 50,000,000 ÷ 86400 ≈ 约 580 QPS。

峰值按 10×:580 × 10 ≈ 约 5,800 QPS。

差距就在这里:如果只按 580 QPS 备容量,晚高峰一来直接被打穿。注意 86400 这个常数——很多人把它记成 100000 凑整,结果系统性低估约 16%。量级对了,下一步才轮到压测校准。

走一遍存储这一格,把"副本数"和"保留期"的威力看清楚:还是这个 Feed,假设每天产生 200 万条帖子记录、单条平均 1 KB、保留 2 年、3 副本。粗算 = 2,000,000 × 1 KB × 730 天 × 3 × 1.3 ≈ 约 5.7 TB。同一份原始数据(约 1.5 TB/2年),仅仅因为"复制成 3 份"就涨到近 3 倍——这正是 1.3 节那条"复制是有代价的"在存储账单上的具体体现,也预告了 04 章要回答的问题:到底要不要 3 份、能不能少一份。

先预测,再展开

同一个系统,把"保留期"从 2 年改成永久保留、把副本数从 3 提到 5,存储估算会怎么变?这两个旋钮哪个更危险?

展开

保留期从有限改成"永久",存储就从一个有上限的盘子变成一条只涨不停的曲线——它没有稳态,迟早撑爆,这是更危险的旋钮。副本数 3→5 只是把账单乘以 5/3 ≈ 1.67 倍,是一次性的常数放大。结论:保留期决定形状(线性增长 vs 有界),副本数只决定系数。设计时先把"数据生命周期/归档策略"定下来,再谈副本数。

1.6CAP / PACELC:全书的权衡总框架(预览)

切分加复制之后,同一份数据有了多份,麻烦随之而来:网络一旦把集群分区(partition,指节点之间消息互通中断,集群被切成两半),分处两侧的节点谁也同步不到谁的写,此时只能二选一——要么用本地这份可能已经过期的数据继续回答(保住可用性 A),要么干脆拒绝回答、等网络恢复(保住一致性 C)。鱼和熊掌不能兼得,因为一侧的写根本到不了另一侧。这就是 CAP 定理的核心,由 Gilbert 与 Lynch 在 2002 年给出严格证明。

CAP:网络分区发生时,只能在一致性(C)与可用性(A)之间二选一。PACELC:再补一句——就算没分区(Else),仍要在延迟(L)与一致性(C)之间二选一。

这里先按下两个最常见的误读,05 章会展开:第一,CAP 不是"三选二、平时挑两个"。分区 P 在真实网络里不可选——它是会发生的客观事实,不是你能放弃的设计选项;C 和 A 的取舍只在分区真正发生的那一刻才被强制。第二,平时(网络健康,占了 99.99% 的时间)真正天天约束你的,是 PACELC 补上的那半句:强一致要等多数派副本往返确认,贵在延迟;最终一致就近答一份,快但可能给你旧数据。很多产品选"最终一致"其实是为了砍掉尾延迟,跟分区压根没关系。

把它当一把尺子用

从这一章起,每遇到一个有状态设计,就拿这把尺子量一次:这份数据,分区时我选 C 还是 A?平时我选低延迟 L 还是强一致 C?答得上来,说明真懂了它牺牲什么;答不上来,多半只是背了名词。05 章会把 CAP 的精确定义、PACELC 的四象限(如 DynamoDB 属 PA/EL、Spanner 属 PC/EC)和多数派为何安全一次讲透;此处只需记住一句:没有银弹,只能按数据、按操作选择牺牲什么。

这条主线会一直贯穿到第九章:无状态怎么扩(02)→ 有状态怎么切(03)→ 怎么复制(04)→ 复制后还能拿到什么保证(05)→ 多节点操作怎么保正确(06)→ 怎么扛流量(07)→ 怎么在故障中活下来(08)→ 这个领域现在走到哪(09)。每一站,都是在"切分 + 复制状态"这两件事上,用 CAP / PACELC 这把尺子,决定牺牲什么。

自测

先合上教程、在纸上写出答案,再展开对照——能复述出"机制和代价"才算过关,认出名词不算。

  1. 从八大谬误里任举 3 条,分别说出它否认的现实,以及这条谬误对应的、没人替你处理的那条故障路径。
  2. 为什么"无状态 vs 有状态"是分布式难度的分水岭?请用"可互换"和"切分/复制"两个词解释,而不是只说"有状态更难"。
  3. 跨洲网络往返延迟为什么不会随硬件进步而下降?这对"全球强一致"的设计意味着什么?
展开参考答案

1. 举例(任选 3):网络可靠——现实是会丢包断连,故障路径是调用发出后分不清"没送到"还是"没回来",不配超时就会永久挂住线程。延迟为零——现实是跨地域往返被光速钉死,故障路径是同步扇出调用把请求总延迟拖到不可接受。带宽无限——现实是链路会被打满,故障路径是大 payload 或大扇出引发排队、级联超时。每条的设计动作见 1.2 表格。

2. 无状态副本不持有别处没有的数据,所以可互换——任意副本服务任意请求结果一样,扩容就是克隆 + 负载均衡,容量近线性增长。有状态节点拥有独占的数据,不可互换——请求只能去持有该数据的节点;于是数据太大要切分(带来热点、跨片查询),数据不能只一份要复制(带来不一致)。分布式所有硬问题都长在分水岭右边,根子是状态而非机器数。

3. 跨洲往返由地理距离 ÷ 光速决定(北京↔弗吉尼亚单程约 55 ms 理论下限,往返翻倍),这是物理定律不是工程水平,换更快的 CPU / 网卡都动不了。对"全球强一致"的含义:每次强一致写都要等远端多数派往返确认,必然付出跨洲 RTT 量级的延迟;想同时要"强一致 + 全球低延迟"就是在和光速对赌,要么牺牲一致性(最终一致 + 就近读),要么牺牲延迟(等多数派),二者只能选一(05 章 PACELC 的 else 支)。

进阶挑战

给一个秒杀场景估算峰值 QPS 与缓存层

某电商搞限时秒杀:一款商品,10 万件库存,活动开场后 1 分钟内预计有 500 万用户涌入抢购详情页 + 下单。请你:① 用 1.5 节的公式估算开场瞬间的峰值 QPS(提示:秒杀峰值比按 ~100× 不再适用,这里要直接按"1 分钟内集中爆发"来算,别用全天平均);② 说明为什么这种流量绝不能直接打到数据库,需要在前面架一层什么;③ 这个热点商品恰好踩中 1.3 节哪条"有状态的代价",03 章会怎么称呼它?

提示(不直接给答案)

①把"500 万请求集中在 60 秒"直接除:5,000,000 ÷ 60 ≈ 8.3 万 QPS 是详情页读的量级,下单写另算;真实峰值往往更尖(多数人卡在前 10 秒),按更短窗口算会更大。②单个商品的所有读全压在数据库里持有该行的那一个节点上——这正是"有状态节点不可互换"的极端形态:复制能多扛读,但所有写仍要回到那一个点。③它是一个单一热 key,03 章会叫它 hot shard / 热点分区,会讲到分区救不了单个热 key,得靠前置缓存 / key 加盐 / 库存预扣等手段。把这三问连起来,就是一条从估算到机制的完整推理。

参考来源

  • Martin Kleppmann,《Designing Data-Intensive Applications》(DDIA) 第 1 章——可靠、可扩展、可维护的系统。
  • Fallacies of distributed computing(Deutsch / Gosling),Wikipedia 词条。
  • Jeff Dean, "Latency Numbers Every Programmer Should Know"——gist.github.com/jboner/2841832。
  • ByteByteGo, "Back-of-the-envelope Estimation"——容量估算速查。
  • Gilbert & Lynch (2002), "Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services"——CAP 的严格证明(05 章精读)。