Skip to main content

分布式系统

📅 2026-04-02 ✏️ 2026-04-07 CS

1 · 分布式系统

分布式系统就是把计算和数据放到多台机器上协同完成任务,用来解决单机在性能、容量、可用性和规模化演进上的瓶颈;

代价是必须面对网络不可靠、多节点协调和一致性这些复杂问题。

S 单机搞不定:性能瓶颈、单点故障、地理延迟、业务复杂度 C 拆成多台机器后,机器之间的通信不可靠、时钟不同步、随时可能崩溃 Q 怎么让多台不可靠的机器协同工作,对外表现得像一个整体? A 通过复制、分区、一致性协议、共识算法等手段,在不可靠的基础设施上构建可靠的系统

1.1 · 协调:三个视角

Three Lenses on Coordination

分布式系统的协调难题可以从三个视角来看,它们指向同一个底层张力:

  • 等待:何时能继续前进?
  • 排序:需要多少顺序?
  • 提交:什么时候承诺不可撤回?

容易的情况:对已有证据做反应 — 消息到了就处理,因果序自然产生,暴露观察到的事实 困难的情况:排除尚未发生的可能性 — 确认没人再发消息,强制全序,暴露”最终结论”

如果你想排除的未来会被信息的到来自动排除,系统保持正确很容易; 如果不会,系统就需要额外的协调机制。

1.2 · 1. 等待:何时能继续前进?#

两种根本不同的等待:等一件事发生(容易) vs 等确认什么都不会再发生(困难)

1.2.1 · 等待某件事发生

因果依赖:输入到了,就能继续。消息到了就处理,ACK 回来就释放资源。 这和单机的函数调用没有本质区别 — 分布式只是把依赖拉长到网络上。

1.2.2 · 等待确认什么都不会再发生

“没有事发生” 无法被观察到。要确认”没有”,必须听到所有人的回复。 等待”无”悄悄变成了等待”所有人”。

这带来两个麻烦:

  • 谁算”所有人”?如果参与者动态变化,目标在移动
  • 某人慢了/挂了/不可达怎么办?能不能不等它?凭什么?

1.2.3 · 不可靠的网络:等待的根源

分布式系统的根本困难:网络是异步的,你无法区分”对方挂了”和”消息还在路上”

网络问题的表现:

  • 消息丢失(请求或响应)
  • 消息延迟(到达时间不确定)
  • 消息乱序
  • 网络分区(一部分节点无法与另一部分通信)

你发了一个请求,没收到响应 — 可能请求丢了、对方挂了、对方处理了但响应丢了、对方处理慢还没回。 你无法区分这些情况 — 这就是”等待确认什么都不会再发生”的困难场景。

应对手段:

  • 超时(timeout):等一段时间没回就认为失败,但超时时间怎么定?太短误判,太长浪费
  • 重试(retry):失败就重试,但操作必须是幂等的,否则重复执行会出问题
  • 心跳(heartbeat):周期性探测对方是否存活
  • 故障检测器(failure detector):不给”是/否”的结论,给”可能挂了”的概率

1.2.4 · 同步复制 vs 异步复制:等谁?#

把同一份数据放到多个节点上(复制),核心问题就是等不等从节点确认:

  • 同步:主节点等所有从节点确认后才返回 — “等所有人”,强一致但慢,任一从节点故障就阻塞
  • 异步:主节点写完立即返回 — “不等”,快但从节点可能落后(复制滞后 replication lag)
  • 半同步:一个从节点同步,其余异步 — 折中

复制方式的选择也是在做等待的取舍:

方式思路取舍
单主复制一个主节点写,从节点复制简单,但主节点是瓶颈和单点
多主复制多个节点都能写写入吞吐高,但要处理写冲突
无主复制任何节点都能写,读取多个节点高可用,但一致性更复杂

1.2.5 · 复制滞后:不等带来的代价

异步复制时,用户写完立刻读从节点,可能读不到自己刚写的数据:

  • 读己之写(read-your-writes):保证用户能读到自己写入的数据
  • 单调读(monotonic reads):保证用户不会看到时间倒流(先看到新数据再看到旧数据)
  • 一致前缀读(consistent prefix reads):保证因果顺序不被打破

1.2.6 · 2PC 的阻塞:等协调者#

两阶段提交中,参与者投了”能提交”之后,必须等协调者的最终决定。 协调者挂了 → 参与者永远等待。这是”等某件事发生”退化为”不知道在等什么”的典型场景。

1.3 · 2. 排序:需要多少顺序?#

偏序自然产生,全序需要付出代价

1.3.1 · 偏序:因果关系自然形成

因果事件构成一个 DAG(偏序):有 happened-before 边的事件有先后,其余并发。 偏序只捕获必须有的顺序,不多不少。

https://en.wikipedia.org/wiki/Happened-before

happened-before 关系(A → B):

  • 同一进程内:A 在 B 之前执行,则 A → B
  • 跨进程:A 是发送消息,B 是接收该消息,则 A → B
  • 传递性:A → B 且 B → C,则 A → C
  • 如果 A 和 B 之间没有因果关系,则它们是并发的(A ‖ B)

1.3.2 · 全序:代价高昂的承诺

全序不仅说”a 在 b 之前”,还说”a 和 b 之间没有别的事件”。 第二句是对未见事件的声明 — 包括未来事件和延迟到达的事件。

偏序可以随着事件发生自然增长;全序强制对并发事件做出排序决定,即使没有因果理由,即使还没看到所有相关事件。一旦决定,不能反悔。

这就是线性一致性和可串行化代价高的原因:成本不在于维护因果已经要求的顺序,而在于过早提交 — 在系统还没看到足够信息时就决定全序。

1.3.3 · 不可靠的时钟:排序的陷阱

每台机器有自己的时钟,它们不一样快,也不一样准:

  • 墙上时钟(wall clock):和真实世界对应,可以用 NTP 同步,但可能跳变(回拨)
  • 单调时钟(monotonic clock):只增不减,适合测量间隔,但不同机器之间无法比较

如果用时间戳来排序事件(比如 last-write-wins),时钟偏差会导致数据丢失: 机器A写入 t=100,机器B写入 t=99(B的时钟慢了),B的写入被A覆盖,但实际上B更晚

1.3.4 · 排序工具

  • 逻辑时钟(Lamport clock):不关心物理时间,只关心因果顺序 — 维护偏序
  • 向量时钟(vector clock):能判断两个事件是因果关系还是并发关系 — 精确偏序
  • 混合逻辑时钟(HLC):结合物理时钟和逻辑时钟
  • TrueTime(Google Spanner):用 GPS + 原子钟,给出时间的置信区间 [earliest, latest] — 逼近全序
  • 全序广播(total order broadcast):所有节点以相同顺序收到相同消息 — 直接实现全序

1.3.5 · 一致性模型:对排序的承诺等级

一致性 = 系统承诺维护多少顺序。越强的一致性,排序越严格,代价越高:

线性一致性(Linearizability) 系统表现得像只有一个副本,所有操作按实际时间顺序排列 — 全序

  • 一旦某个读返回了新值,后续所有读都必须返回新值
  • 代价:需要协调,延迟高,网络分区时不可用

顺序一致性(Sequential Consistency) 所有操作有一个全局顺序,且每个进程内的操作顺序被保留 — 全序,但不要求和实际时间一致

因果一致性(Causal Consistency) 只保证有因果关系的事件顺序一致,并发事件可以乱序 — 偏序

  • 用向量时钟或因果依赖追踪实现
  • 是”不牺牲可用性的最强一致性”

最终一致性(Eventual Consistency) 如果不再写入,所有副本最终会收敛到一致状态 — 最弱的排序承诺

  • 最高可用
  • “最终”是多久?不保证

1.4 · 3. 提交:什么时候承诺不可撤回?#

暴露观察到的事实是安全的;暴露”最终结论”需要协调

1.4.1 · 安全的暴露 vs 危险的暴露#

安全:“这条消息到了”、“a 在 b 之前” — 这些是对已观察证据的报告。 不同副本可能在不同时间看到,但看到的内容只会被补充,不会被推翻。 副本可以独立暴露这些事实,观察者看到的视图只会增长(单调)。

危险:“不会再有别的了”、“这个结果是最终的”、“这就是顺序” — 这些排除了未来观察的可能性。 一旦对外暴露,系统就被绑定了。未来的事件必须兼容这个声明 — 即使它们还没被所有节点看到。

一个副本可能基于自己的信息做出声明,而另一个副本还有可能观察到与之矛盾的事件。 此时系统无法简单”更新”观察者的视图而不违反已经说过的话。

1.4.2 · 共识:多节点对一个值做出不可撤回的承诺

共识 = 分布式系统最核心也最困难的问题

FLP 不可能定理 在异步网络中,即使只有一个节点可能崩溃,也不存在一个确定性的共识算法能保证终止

  • 意义:完美的共识是不可能的,实际算法都做了妥协(如引入超时、随机化)

Paxos Lamport 提出,理论上最经典:

  • 角色:proposer、acceptor、learner
  • 两阶段:prepare → accept
  • 保证:只要大多数 acceptor 存活,就能达成共识
  • 问题:难理解、难实现、活锁

Raft 详见 raft

为可理解性而设计,问题拆分为:

  1. Leader Election
  2. Log Replication
  3. Safety

ZAB(ZooKeeper) 和 Raft 类似,也是基于主节点的共识协议

1.4.3 · 共识的等价问题

这些问题本质上都等价于共识 — 都需要做出不可撤回的承诺:

  • 原子广播(全序广播):所有节点以相同顺序收到相同消息
  • 领导选举:选出唯一的领导
  • 分布式锁 / 租约
  • 原子事务提交

1.4.4 · 事务提交:跨节点的原子承诺

事务 = 把多个操作打包成一个原子操作 单机事务已经很成熟,分布式事务是另一个难度级别

操作涉及多个节点,要么全成功,要么全失败 — 系统对外承诺”这组操作是原子的”。

两阶段提交(2PC)

  1. Prepare:协调者问所有参与者”你能提交吗?”
  2. Commit/Abort:所有参与者都说能,协调者说”提交”;否则”中止”

问题:

  • 参与者投了”能”之后,必须等协调者的决定,此时参与者被锁住(等待问题)
  • 协调者挂了 → 参与者永远等待(阻塞问题)
  • 性能差:需要两轮通信 + 持久化日志

三阶段提交(3PC) 在 2PC 基础上加了一个 pre-commit 阶段,减少阻塞窗口 但在网络分区场景下仍然有问题,实际使用少

Saga 长事务拆成一系列本地事务,每个本地事务有对应的补偿操作:

  • 正向执行:T1 → T2 → T3
  • 某步失败,反向补偿:C2 → C1
  • 最终一致性,不保证隔离性
  • 本质上放弃了原子提交的承诺,换取可用性

1.4.5 · 写冲突:并发时的提交决策

多主 / 无主复制时,两个节点同时修改同一条数据 — 系统必须决定承诺哪个版本:

  • Last-Write-Wins(LWW):时间戳大的赢,简单但会丢数据(依赖有问题的全序)
  • 合并(merge):应用层合并冲突(推迟提交,让用户决定)
  • CRDT:数据结构本身支持自动合并,无冲突 — 避免了提交决策,因为所有并发写入都能单调合并

1.5 · 4. 工程取舍:三个视角的汇合#

1.5.1 · CAP 定理#

https://en.wikipedia.org/wiki/CAP_theorem

在网络分区(P)发生时,系统只能在一致性(C)和可用性(A)之间选一个:

  • CP:分区时拒绝服务,保证一致性(如 ZooKeeper、etcd) — 宁可等待,也要正确排序和提交
  • AP:分区时继续服务,但数据可能不一致(如 Cassandra、DynamoDB) — 不等待,放松排序,推迟提交

CAP 是一个取舍框架,不是非黑即白的分类 实际系统在不同操作上可能做不同选择

1.5.2 · PACELC#

https://en.wikipedia.org/wiki/PACELC_design_principle

对 CAP 的扩展:

  • 如果有 Partition:选 A 还是 C?(和 CAP 一样)
  • Else(正常情况):选 Latency 还是 Consistency?— 即使没分区,全序和强提交也要付出延迟代价
系统P 时正常时
DynamoDBAL(低延迟优先)
MongoDBCL
SpannerCC(用 TrueTime 付出延迟代价换一致性)
  1. https://martin.kleppmann.com/2015/09/17/critique-of-the-cap-theorem.html
  2. https://martin.kleppmann.com/2015/05/11/please-stop-calling-databases-cp-or-ap.html
  3. https://aphyr.com/posts/281-jepsen-on-the-perils-of-network-partitions
  4. https://raft.github.io
  5. https://understandingdistributed.systems
  6. https://www.oreilly.com/library/view/designing-data-intensive-applications/9781491903063
  7. https://www.atlassian.com/zh/microservices/microservices-architecture/distributed-architecture