Skip to content

元数据卡

  • 前置知识:Vol 4 网络(TCP/IP、HTTP)、Vol 5 数据库(事务、ACID)
  • 预计时间:45 分钟
  • 核心难度:进阶
  • 阅读模式:高度专注
  • 完成标志:能够列出分布式系统的 8 条谬误,说清 CAP 三角取舍,理解 FLP 不可能性的核心含义,区分逻辑时钟与物理时钟的适用场景

你的进度

从工匠之都出来后,你和一队装备齐全的工程师一起,被派往远征军前线。在这里,你发现单机时代的武器——一把剑、一面盾——面对集群战场时完全不够用。

林将军站在战报地图前,指着一条传输线路说:"网络可能断,机器可能挂,消息可能会迟到。你要学会在这种环境下打仗。"

从今天开始,你的战场不是单核 CPU 和一块硬盘,而是一群不确定、不可靠、随时可能出问题的机器。你需要在它们之上构建出可靠的服务。


你的任务

理解分布式系统为什么比单机难了不止一个数量级。不是代码写得更复杂了,是你面对的基本假设变了——网络不是可靠的、时钟不是可信的、部分失败不是异常,是常态。这一章不是为了赶你走,是为了告诉你前方有什么坑,每道坑有多深,然后带你绕过去的策略。


破局 · 溯源


为什么分布式系统比单机难那么多?

一台机器上,函数要么返回结果,要么抛异常。你不需要考虑"函数执行了一半,远程机器挂了怎么办"。进程之间的共享内存是确定的——两个线程竞争同一个变量,至少有锁可以控制。网络在单机里是 localhost,不出机房。

但当你把系统部署到 4 台、40 台、400 台机器上时,所有确定性都瓦解了。

你调用一个远程服务。等了三秒,没返回。

是因为网络延迟?对端进程卡住了?消息丢了?对端已经挂了?还是消息正在路上但慢了一点?——你无法区分。这就叫部分失败

Fallacies of Distributed Computing

先看一张清单。早在 1994 年,Sun 公司的工程师总结了分布式系统的 8 条错误假设:

  1. 网络是可靠的
  2. 延迟为零
  3. 带宽是无限的
  4. 网络是安全的
  5. 网络拓扑不会变
  6. 有一个管理员
  7. 传输成本为零
  8. 网络是同质的

这 8 条你全信了,你的系统就一定会在某个凌晨 3 点崩掉。

大多数刚接触分布式系统的开发者,第一条就过不去。"TCP 有重传,消息不会丢"——但 TCP 只管端到端的字节流,不管你的业务消息是否被正确处理了。你的服务器在处理请求时挂了,客户端的 TCP 连接收到 RST,它只知道"连接断了",不知道"那笔转账到底成功了没有"。


CAP 定理

CAP 是分布式系统中最常被引用也最常被误用的出发点。它描述的是分布式数据系统在面对网络分区时必须做的取舍。

C:Consistency(一致性)——所有节点看到同一份数据,写完后读立刻能看到最新值 A:Availability(可用性)——每个请求都能收到非错误的响应(但不保证是最新数据) P:Partition Tolerance(分区容忍)——即使节点之间的网络断了,系统还能继续工作

CAP 说的是:网络分区一定会发生。在 P 的前提下,你在 C 和 A 之间只能选一个。

这不是"三选二"——因为 P 不是可选条件,它总会发生。真正的取舍是:当网络分区时,你选一致还是选可用?

选 CP(一致优先):分区发生时停止写入,保证数据一致。代价是部分节点不可用,直到分区恢复。 举例:ZooKeeper、etcd。

选 AP(可用优先):分区发生时各节点继续独立工作,接受短暂的不一致,等分区恢复后再同步。 举例:DNS、Cassandra。

大多数人以为 CAP 是 24 小时全局决策。实际上,CAP 只在分区发生的那个时间窗口内起作用。网络正常时,C 和 A 可以满足。

深入一层:CAP 的 C 是什么一致性?

CAP 的 C 是"线性一致性"(Linearizability)——这是最强的一致性模型。写操作生效后,所有后续读操作立刻看到这个值,就像只有一个副本一样。

但在实践中,你很少需要这么强的一致性。

线性一致性(CAP 的 C):写完后任何读都能看到最新值 --- 最强
顺序一致性:                     每台机器内部的操作顺序一致 --- 中等
最终一致性:                     只要不写新值,最终所有拷贝一致 --- 最弱

多数分布式数据库实作的其实是"最终一致性"或"可调一致性"。Cassandra 让你选读多少个副本才返回——读所有副本(ALL)获得强一致,读一个副本(ONE)获得弱一致。


FLP 不可能性

FLP 是分布式系统理论中一个让人不安的结果。三位计算机科学家(Fischer、Lynch、Patterson)在 1985 年证明:在一个异步分布式系统中,只要有一个进程可能崩溃,就不存在一个确定性的算法能在有限时间内让所有节点达成共识。

"异步"的意思是:消息传输没有上限,你无法判断一个节点是挂了还是只是慢。

这个定理听起来让人绝望——但现实世界的分布式系统每天都在达成共识(ZooKeeper、etcd、Raft)。矛盾吗?

不矛盾。现实系统做了两件事之一:

  1. 引入超时机制,把异步系统变成"部分同步"(partially synchronous)模型
  2. 使用随机化算法

Raft、Paxos、ZAB 都在做一件事:用超时和 Leader 选举来规避 FLP 不可能性。它们不保证永远不死锁,但保证在绝大多数情况下能在有限时间内达成共识。

FLP 的实际意义不是"共识不可能",而是"在没有任何额外假设的情况下,共识不可能"。工程实践给了系统"心跳""超时""Leader 选举"这些假设,所以共识可行。


时钟与时间

分布式系统中,时间是最容易被轻视的问题。

你在一台机器上记录了一个事件的时间戳:

2026-06-24 16:00:01.234

另一台机器上的时间戳:

2026-06-24 16:00:01.233

你能断定第一个事件发生在第二个事件之前吗?不能。

NTP(Network Time Protocol)同步误差通常在 1-50ms。即使你用了昂贵的 PTP 硬件时钟,误差也在微秒级。更糟的是:NTP 同步可能把机器的时间往回拨——如果一台机器的时间在同步前快了 5 秒,NTP 会让它突然回到正确时间,你的时间戳序列就出现了"穿越"。

逻辑时钟

既然物理时间不可靠,Lamport 在 1978 年提出了逻辑时钟(Lamport Clock)。它不关心"现在几点",只关心"事件 A 是否在事件 B 之前发生"。

每台机器维护一个单调递增的整数计数器:

  • 发生内部事件时,自己的计数器 +1
  • 发送消息时,带上自己的计数器值
  • 收到消息时,把自己的计数器设为 max(本地计数器, 消息中的计数器) + 1

这样你至少能获得"happened-before"关系。如果 clock(A) < clock(B),A 可能在 B 之前发生或两者无关;如果 clock(A) < clock(B) 且我们知道有因果链路,则 A 一定在 B 之前。

但 Lamport Clock 有个问题:它不能检测冲突。两个事件如果 clock(A) == clock(B),你无法判断它们是并发的还是相关的。

向量时钟

向量时钟解决了这个问题。每台机器维护一个向量(数组),长度为节点数:

机器 M1 的向量时钟: [M1: 3, M2: 2, M3: 1]

每个分量表示"这台机器所知的其他机器上的事件数量"。当事件 A 的向量各分量都小于等于事件 B 的对应分量(且至少有一个严格小于),那么 A 发生在 B 之前。否则,A 和 B 是并发事件。

Cassandra 冲突检测用的就是向量时钟的变体。

何时用什么时钟?

场景推荐原因
顺序 ID 生成Lamport Clock只需要偏序
冲突检测向量时钟(或版本向量)需要判断并发
业务审计日志物理时间 + NTP 容差窗口需要与人类时间对齐
分布式数据库 Snapshot 隔离TrueTime(Spanner)或混合逻辑时钟需要全局一致的时间戳

深入:Google Spanner 的 TrueTime

Spanner 不依赖传统 NTP。它使用 GPS + 原子钟,把时钟误差范围(clock uncertainty)暴露给上层。TrueTime 返回的不仅是一个时间戳,而是一个区间 [earliest, latest]——"真实时间在 earliest 和 latest 之间"。

当 Spanner 需要为写操作分配时间戳时,它等待 TT_interval = latest - earliest 的时间再提交。这保证了所有节点的读操作都能观察到一个全局一致的快照。

代价是写延迟增加了一个 clock uncertainty(约 1-7ms)。


常见陷阱

  1. 把 CAP 当成 7x24 小时的设计原则。CAP 只在网络分区发生时成立。99.9% 的时间网络是好的,这时 C 和 A 可以满足。不要在架构文档里写"我们的系统是 CP 系统"——这不是一个全局标签。

  2. 把最终一致性当成"最终会一致,我不用管"。最终一致性的时间窗口内,用户可能看到过期数据。如果你的业务不能接受过期数据,要么上强一致性(付性能代价),要么在应用层做冲突处理。

  3. 依赖物理时间戳做因果判断。"A 日志时间比 B 早 1ms,所以 A 先发生"——在分布式系统中这种推断不可靠。NTP 漂移、调度延迟、IO 抖动都会打乱你的时间线。

  4. 认为 FLP 不可能性证明"分布式系统不可靠,没必要做了"。FLP 证明的是理论下限,不是工程上限。超时、Leader 选举、部分同步模型在实践中已经把共识变成了可工程化的问题。


通关挑战

  • 热身:读一遍 8 条 Fallacies,用你自己的真实项目经历给每条配一个真实事故场景。如果你没有做过分布式系统,至少给每条写一句"为什么这条是错的"。
  • 挑战:用 Java 实现一个简单的 Lamport Clock:
java
// LamportClock.java
// 一个简单的逻辑时钟实现
// 编译: javac LamportClock.java
// 运行: java LamportClock
// 预期输出: 每次 tick() 后时钟单调递增

public class LamportClock {
    private int clock;
    
    public LamportClock() {
        this.clock = 0;
    }
    
    // 发生内部事件时调用
    public int tick() {
        return ++clock;
    }
    
    // 发送消息时,带上当前时钟值
    public int send() {
        return ++clock;
    }
    
    // 收到消息时,传入消息中的时钟值
    public void receive(int sentTimestamp) {
        clock = Math.max(clock, sentTimestamp) + 1;
    }
    
    public int getClock() {
        return clock;
    }
    
    public static void main(String[] args) {
        LamportClock c1 = new LamportClock();
        LamportClock c2 = new LamportClock();
        
        // 模拟交互
        int msg = c1.send();                         // c1: 1
        System.out.println("c1 sends with clock: " + msg);
        
        c2.receive(msg);                             // c2: max(0,1)+1 = 2
        System.out.println("c2 clock after receive: " + c2.getClock());
        
        System.out.println("c1 clock: " + c1.tick()); // c1: 2
    }
}

修改这个实现,让它支持多个节点交互,打印每一步的时钟变化。

  • 观察:启动两个进程,一个写 NTP 时间到文件,另一个读。在 WSL/容器中模拟 100ms 的时钟偏差,观察两个进程看到的系统时间差异。

旅人笔记

分布式系统的本质困难不是写多线程代码,而是面对部分失败、不可靠的时钟和网络分区时,你的确定性假设全部瓦解。CAP 告诉你取舍,FLP 告诉你理论下限,时钟告诉你不能信"时间"。这些不是劝退你,是给你画地图上的暗礁。


下一站预告

你的程序现在需要远程调用另一个服务。但你发现,用 HTTP + JSON 已经不够了——传输效率、类型安全、流式通讯,这些需求把 RPC 推到台前。下一章,你会接上 gRPC 和 Thrift,让服务之间高效地说话。

Built with VitePress | Software Systems Atlas