背景:分布式系统到底难在哪
在进入论文之前,我们先来搞清楚一个前置问题:分布式系统到底难在哪?
你可以这么理解------单机系统就像一个人在书房工作,所有东西都在手边,想查资料查资料,想写东西写东西,所有状态都在你脑子里。但分布式系统像一群人在不同城市的办公室里协作,你们要同步进度、共享文件、还得保证有人掉链子活儿不能停。
这里面有几个根本性的困难:
没有全局时钟。 每台机器有自己的物理时钟,但它们之间会有偏差。你在北京的时间戳和在纽约的时间戳,没法直接比较先后。那怎么确定两个事件谁先谁后?
通信不可靠。 消息可能丢失、可能延迟、可能重复。你发了一条消息给对方,你不知道对方是收到了没回,还是根本没收到。
节点可能故障。 一台机器可能突然宕机,更麻烦的是它可能"半死不活"------还在运行但返回错误的结果,你甚至不知道它是不是出问题了。
规模带来复杂度。 几台机器的分布式和几千台机器的分布式完全是两个问题。规模越大,上面这些问题就越严重。
后面每一篇论文,本质上都在解决上面这些问题中的一个或几个。我们顺着时间线一篇篇看下来。
第一章:理论基石------一切的起点
1.1 Lamport 逻辑时钟(1978)
论文: Leslie Lamport, Time, Clocks, and the Ordering of Events in a Distributed System , 1978 链接: lamport.azurewebsites.net/pubs/time-c...
用事件的发生顺序来定义时间的先后顺序
它解决了什么问题
一句话:分布式系统里,怎么确定两件事谁先谁后?
单机上这根本不是问题------操作系统有全局时钟,事件 A 的时间戳是 10:00:01,事件 B 是 10:00:02,那 A 肯定在 B 前面。
但分布式系统里每台机器都有自己的时钟,而且这些时钟走得不一样快。A 机器说"我在 10:00:01 收到了请求",B 机器说"我在 10:00:00 处理了这个请求"------你能说 B 比 A 早吗?不能,因为 B 的时钟可能比 A 快了 30 秒。
所以 Lamport 问了一个很本质的问题:我们真的需要知道"几点发生的"吗?还是说,我们只需要知道"谁在谁前面"就够了?
答案是后者。绝大多数分布式算法(选举、锁、事务)只关心事件的先后顺序,不关心具体几点。那问题就变成了:不用物理时钟,怎么给事件排个序?
核心思路
Lamport 的想法特别直觉------用因果关系来定义顺序。
什么算"因果"?两种情况:
第一种:同一台机器上的两件事,先发生的就在前面。 这个没争议,单机上事件本来就有先后。
第二种:如果 A 给 B 发了条消息,那"发消息"这件事一定在"收消息"前面。 因为 B 只有收到了消息才能做出响应,消息就是因果链。
把这两条规则连起来,就能串出一条完整的因果链。比如 A 发了消息给 B,B 收到后又发了消息给 C------那 A 的发 → B 的收 → B 的发 → C 的收,这四个事件的顺序就确定了。
但反过来,如果 A 上发生了一件事,B 上也发生了一件事,它们之间没有任何消息往来,那这两件事就没有因果关系------我们叫它并发。并发的事件没有先后之分,谁在前谁在后都行。
好,现在我们知道怎么判断两个事件有没有因果关系了。但还有一个问题:怎么把这个关系变成一个具体的数字? 因为算法里需要比较大小,不能只说"A 在 B 前面",得给每个事件一个数字,让 A 的数字比 B 小。
这就是 Lamport 时钟算法要做的事。规则只有三条,非常简单:
规则一: 每个进程维护一个计数器,初始值为 0。
规则二: 进程每发生一个事件(包括发消息、收消息、本地计算),计数器加 1。这个计数器的值就是这个事件的"逻辑时间戳"。
规则三: 发消息的时候,把当前计数器值附在消息里一起发出去。收消息的时候,先把本地计数器更新为 max(本地值, 消息里的值) + 1,然后再把这个新值作为"收到消息"这个事件的时间戳。
就这三条。我们用一个具体例子走一遍:

你看,关键就在收消息那一步------max(本地值, 消息里的值) + 1。这个操作保证了:如果 A 的某个事件通过消息影响了 B,那 A 的时间戳一定小于 B 的时间戳。 因为 B 收到消息时,会把自己的计数器至少拉到跟 A 一样高,然后再加 1。
反过来呢?如果两个事件的时间戳分别是 2 和 6,能说时间戳 2 的事件一定在时间戳 6 的事件前面吗?不一定。 它们可能只是并发的,碰巧计数器值不同而已。
所以 Lamport 时钟的性质是:有因果关系 → 时间戳一定有大小关系;但时间戳有大小关系 → 不一定有因果关系。 这是一个单向的保证。
业内影响
说白了,这是分布式理论的"第零章"。没有这篇论文,后面几乎所有东西都无从谈起------Paxos 的提案编号、Raft 的任期号、向量时钟,全都建立在"用逻辑计数器代替物理时钟"这个思想之上。后来的向量时钟(Vector Clock)就是在这个基础上扩展的,加入了因果关系的完整追踪能力,被 Dynamo、Cassandra 等系统广泛使用。论文很短,思路很清晰,建议直接去读原文,半小时能读完。
1.2 拜占庭将军问题(1982)
论文: Leslie Lamport, Robert Shostak, Marshall Pease, The Byzantine Generals Problem , 1982 链接: lamport.azurewebsites.net/pubs/byz.pd...
只要我们能保证多数人是正确的,那么就可以做出正确的决策
它解决了什么问题
假设你是一个将军,带着几个师围攻一座城市。你们需要在同一时间发起进攻才能赢。将军们通过信使传递消息来协商进攻时间。但问题来了------其中可能有叛徒。叛徒可能会给一些人发"进攻",给另一些人发"撤退",故意制造混乱。
这就是拜占庭将军问题要形式化的场景:在一组需要达成一致的参与者中,如果有人不仅可能故障,还可能故意作恶(发送矛盾的信息、伪造消息),还能达成正确的一致吗?
注意这里跟普通的故障不同。普通的故障是节点"不工作了",但拜占庭故障是节点"还在工作但给出错误的结果"------这比简单的宕机危险得多,因为你不知道谁在说谎。
核心思路
论文的形式化描述很精炼,核心就是两个结论:
第一,如果叛徒数量达到或超过总数的三分之一,就不存在能达成一致的正确算法。 这是一个不可能性定理------它告诉你问题的边界在哪。比如 4 个将军中有 1 个叛徒,可以解决;3 个将军中有 1 个叛徒,就无解了。
第二,在叛徒不超过三分之一的条件下,存在算法可以达成一致。 论文给出了一个递归算法(OM(m),Oral Messages),能在 n ≥ 3m+1 个节点中(m 为叛徒数)达成正确一致。

我们用一个简单的例子来感受为什么三分之一是边界。假设 3 个将军,1 个叛徒:
arduino
将军A(忠诚):给B发"进攻",给C发"进攻"
将军B(叛徒):给A发"撤退",给C发"进攻"
将军C(忠诚):给A发"进攻",给B发"进攻"
A 收到:自己的"进攻" + B的"撤退" + C的"进攻" → 多数是"进攻" → 决定进攻
C 收到:自己的"进攻" + A的"进攻" + B的"进攻" → 全是"进攻" → 决定进攻
看起来没问题?但如果叛徒更聪明一点:
将军B(叛徒):给A发"撤退",给C发"撤退"
A 收到:自己的"进攻" + B的"撤退" + C的"进攻" → 2:1 → 进攻
C 收到:自己的"进攻" + A的"进攻" + B的"撤退" → 2:1 → 进攻
还是没问题。但如果是 3 个将军 2 个叛徒呢?
叛徒可以故意让忠诚的将军做出不同的决定,这就是为什么不能超过三分之一。
业内影响
这篇论文是容错计算的理论源头,定义了"拜占庭故障"这个概念。更重要的是它直接催生了整个 BFT(拜占庭容错)领域------后面 1999 年的 PBFT 把它从理论拉到了工程实现,区块链的共识机制(PoW、PoS 等)本质上也都在解决拜占庭将军问题。可以说没有这篇论文就没有后面整个分布式容错领域。
顺便说一句,这篇论文本身篇幅不长(大约 10 页),比喻生动,是入门分布式理论非常好的第一篇,比 Paxos 那篇好读太多了。
1.3 CAP 定理(2000/2002)
论文: Eric Brewer, Towards Robust Distributed Systems (2000 Keynote)+ Seth Gilbert & Nancy Lynch, Brewer's Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services (2002) 链接: Brewer Keynote | Gilbert & Lynch 证明
在网络中断发生的时候,不管是接受用户请求,还是拒绝用户请求,都会存在分布式系统问题
它解决了什么问题
设计分布式系统时,我们有三个看起来很合理的目标:
-
一致性(Consistency): 所有节点看到的数据是一样的。你在 A 节点写了个值,去 B 节点读,应该能读到最新的值。
-
可用性(Availability): 每个请求都能得到响应,不管系统状态如何。不会出现"请求超时无响应"的情况。
-
分区容错(Partition tolerance): 即使节点之间的网络断了(形成分区),系统仍然能运行。
这三个目标看起来都不过分,对吧?但 CAP 定理告诉你------三者不可能同时满足,最多只能满足两个。
核心思路
为什么三者会冲突?我们用一个具体的场景来理解。假设你有一个简单的分布式系统,两个节点 A 和 B,它们之间通过网络通信。正常情况下没问题,A 写数据,B 同步,大家一致。
现在网络分区了------A 和 B 之间的网络断了。这时候客户端向 A 发起一个写请求,要写入一个新值。A 面临一个选择:
-
接受写入。 这样 A 是"可用的"(能响应请求),系统也容忍了"分区"(网络断了还能工作)。但问题是 B 不知道这个新值,A 和 B 的数据不一致了------牺牲了一致性。
-
拒绝写入。 这样 A 和 B 的数据保持一致,系统也容忍了分区。但客户端的请求被拒绝了------牺牲了可用性。
你没有办法既接受写入(保证可用+分区容错),又让 B 也知道这个新值(保证一致),因为网络已经断了。
这就是 Gilbert 和 Lynch 在 2002 年给出的形式化证明的核心思路。Brewer 在 2000 年的 Keynote 中最早提出了这个猜想,两年后被正式证明。
需要注意的是,CAP 定理不是说"你永远不能同时拥有 C 和 A",而是说当网络分区发生时,你必须在 C 和 A 之间做选择。正常情况下(没有分区),C 和 A 是可以同时满足的。
还有一个常见的误解:CAP 不是"三选二"。分区容错性(P)在分布式系统中基本上是必须的------因为网络分区不可能完全避免。所以实际上是在 C 和 A 之间选择:
-
CP 系统: 发生分区时,拒绝服务以保证一致性。比如 Zookeeper、HBase。
-
AP 系统: 发生分区时,继续服务但允许数据暂时不一致。比如 Cassandra、Dynamo。

业内影响
CAP 牛逼的地方在于,它不是告诉你"怎么做",而是告诉你"别做梦了,没有完美方案"。后面我们聊到任何分布式系统的架构选型,都可以回到 CAP 来讨论取舍。Dynamo 的 R/W 参数、Zookeeper 的一致性模型、Cassandra 的调优策略,背后都是 CAP 的思维框架。
比较有意思的是,Brewer 2000 年的 Keynote 只是一个工程直觉,Gilbert 和 Lynch 2002 年的论文才把它严格证明了。看一个工程直觉怎么变成一个数学定理,这本身就是科学研究的一个经典过程。后来 Brewer 自己又发了篇文章补充说 CAP 的实际含义比"三选二"要微妙得多------分区发生时不是简单地放弃 C 或 A,而是要在分区恢复后处理数据的一致性问题。
第二章:Google 三驾马车------大规模分布式系统的工程实践
理论铺垫完了,接下来看 Google 怎么把这些东西落地。2003 到 2006 年间,Google 连续发了三篇论文,分别解决了分布式存储、分布式计算和结构化数据存储三个问题。说实话,这三篇论文直接定义了后面整个大数据时代的技术栈。
2.1 GFS------Google 文件系统(2003)
论文: Sanjay Ghemawat, Howard Gobioff, Shun-Tak Leung, The Google File System , 2003 链接: static.googleusercontent.com/media/resea...
大文件存不下,那就分成小片存
它解决了什么问题
2003 年前后,Google 的搜索业务已经需要处理海量数据------几千台服务器、PB 级别的数据。传统的文件系统根本扛不住:单台机器的磁盘不够大,磁盘坏了数据就丢了,多台机器之间的数据同步太复杂。
Google 需要一个文件系统,满足几个条件:能跑在成千上万台普通服务器上(不是昂贵的专用硬件),能自动处理硬件故障,能支持大文件的高效读写,而且要让几百台机器同时读一个文件时不会成为瓶颈。
核心思路
GFS 的设计说白了就一句话:把大文件切成固定大小的 chunk(64MB),分散存储在多台机器上,由一个中心节点管理元数据。
系统里有两种角色:
一个 Master 节点,负责管理元数据------哪个文件被切成了哪些 chunk,每个 chunk 存在哪些机器上,chunk 的副本信息等等。Master 不存储实际数据,只存储"地图"。
多个 Chunk Server,每台机器上存着若干个 chunk 的实际数据。每个 chunk 默认有 3 个副本,分布在不同机器上,保证某台机器挂了数据不会丢。
读写流程是这样的:客户端要读文件,先问 Master"文件的 chunk 0 在哪几台机器上",Master 返回 chunk server 的地址列表,客户端直接去 chunk server 读数据。写操作类似,先问 Master 找到主副本,然后写入。
css
客户端 → Master:"chunk 0 在哪?"
Master → 客户端:"在 Server A、B、C 上,A 是主副本"
客户端 → Server A:写入数据
Server A → Server B, C:复制数据
这个设计有几个地方我觉得很值得学习:
Master 很轻。 因为不存数据,只存元数据,所以一台机器就够了。元数据存在内存里,响应很快。即使 Master 挂了,也可以从持久化存储中恢复(后面 Bigtable 用 Chubby 来解决这个问题)。
数据流和控制流分离。 客户端读数据不经过 Master,直接跟 chunk server 通信。这样 Master 不会成为数据读写的瓶颈。
大 chunk 设计。 64MB 的 chunk 大小减少了客户端跟 Master 的交互次数(一个 1GB 的文件只有 16 个 chunk),也减少了元数据的大小。
业内影响
GFS 是工业界第一次系统性地解决大规模分布式存储问题。它的设计思想直接催生了 HDFS,而 HDFS 又是整个 Hadoop 生态的存储基础,后面 Spark、Hive 这些组件全都跑在 HDFS 上面。可以说 GFS 定义了大数据时代的存储范式。回头看 GFS 的设计,最让人佩服的是它的简洁------Master 只存元数据、数据流和控制流分离、大 chunk 减少交互,每一个决策都有明确的工程理由。后来 Ceph 走了去中心化路线,其实是针对 GFS 中心化 Master 这个设计的反思和改进。
2.2 MapReduce------分布式计算框架(2004)
论文: Jeffrey Dean, Sanjay Ghemawat, MapReduce: Simplified Data Processing on Large Clusters , 2004 链接: research.google/pubs/mapred...
一次性数1000枚硬币太慢,那么我们十个十个地数,数完加起来就好了
它解决了什么问题
有了 GFS 能存海量数据了,但下一个问题来了:怎么在几千台机器上并行处理这些数据?
比如你要统计 Google 索引的所有网页中每个词出现了多少次。数据量太大,一台机器算不完,必须分到几千台机器上并行计算。但写分布式程序太痛苦了------你要处理数据分片、任务分配、并行执行、错误重试、结果汇总......光是处理"某台机器算到一半挂了怎么办"就能写几百行代码。
MapReduce 要解决的就是这个问题:让开发者只关心"我要算什么",不用管"怎么算"。
核心思路
MapReduce 的做法是把复杂的分布式计算抽象成两个简单的函数------Map(映射) 和 Reduce(归约)。
Map 函数接收一条输入数据,输出一组键值对。比如统计词频,Map 函数对每个网页的处理就是:"把网页内容拆成单词,每个单词输出一个 (单词, 1) 的键值对。"
Reduce 函数接收同一个键的所有值,把它们合并成一个结果。比如词频统计中,Reduce 函数收到 ("hello", 1, 1, 1, 1, 1) 后,把它们加起来,输出 ("hello", 5)。
整个执行流程是这样的:
javascript
第1步:输入数据被自动切分成多个分片,分配到不同的 Worker 节点
第2步:每个 Worker 对自己的数据分片执行 Map 函数,产出中间键值对
第3步:框架自动把相同键的键值对汇聚到同一个 Reduce Worker
第4步:Reduce Worker 对每组键值对执行 Reduce 函数,输出最终结果

用词频统计的完整例子来看:
javascript
输入:三台机器分别存储了一些网页内容
Map 阶段(在每台机器上并行执行):
机器1:"hello world hello" → ("hello",1), ("world",1), ("hello",1)
机器2:"hello java" → ("hello",1), ("java",1)
机器3:"world java hello" → ("world",1), ("java",1), ("hello",1)
Shuffle 阶段(框架自动完成,把相同 key 汇聚到一起):
"hello" → [1, 1, 1, 1]
"world" → [1, 1]
"java" → [1, 1]
Reduce 阶段(并行执行):
"hello" → 4
"world" → 2
"java" → 2
开发者只需要写 Map 和 Reduce 两个函数,框架自动处理数据分片、任务调度、容错(某台机器挂了会自动重试)、中间结果的排序和汇聚(Shuffle)等所有脏活累活。
业内影响
MapReduce 最牛的地方在于它把分布式计算这件事的门槛降到了极低------在它之前,写分布式程序是少数高手的事;在它之后,一个普通工程师只要写两个函数就能处理 PB 级数据。
Hadoop 就是 MapReduce 的开源实现,后来整个大数据生态(Hive、Pig、Spark 早期的 MapReduce 模式)都跟它有关系。Google 内部的 FlumeJava、Mesa 等系统也是在这套编程模型上演进出来的。不过说实话,MapReduce 的批处理模型在很多场景下已经被 Spark 这类内存计算框架替代了------MapReduce 的 Shuffle 阶段要写磁盘,性能瓶颈明显。但它的核心思想------把复杂计算抽象成简单的编程模型------这个设计哲学至今仍然很有影响力,Spark 的 RDD 模型本质上也是这个思路的延续。
2.3 Bigtable------结构化数据存储(2006)
论文: Fay Chang et al., Bigtable: A Distributed Storage System for Structured Data , 2006 链接: research.google/pubs/bigtab...
一个excel太大,那就拆成多个excel来存
它解决了什么问题
GFS 解决了文件存储,MapReduce 解决了分布式计算,但 Google 还需要一个东西:一个能存储结构化数据的分布式数据库。
比如 Google 搜索的网页索引,每条记录包含 URL、网页内容、页面排名、索引信息等字段。这种数据需要随机读写(根据 URL 查信息)、需要高效扫描(遍历所有索引)、需要支持海量数据(千亿条记录),还要能动态扩展。GFS 只能存文件,MySQL 在单机上跑不了这么大的量。
核心思路
Bigtable 设计了一个数据模型:一个稀疏的、分布式的、持久化的多维 Map。
用数学化的定义来看:
go
map[row_key : string][column_family : string][timestamp : int64] → string
翻译成人话就是:你用行键(row key)、列族(column family)和时间戳(timestamp)三个维度来定位一个单元格,里面存的是一个字符串值。
举个例子,假设我们要存储网页数据:
ini
行键: "com.cnn.www"
列族 "contents:" → 网页HTML内容
列族 "anchor:" → 指向这个URL的锚文本
列族 "rank:" → 页面排名
具体数据:
"com.cnn.www" | contents:html = "<html>..."
| anchor:cnnsi.com = "CNN"
| anchor:my.look.ca = "CNN.com"
| rank:20230101 = "0.95"

几个关键的设计选择值得注意:
行键排序。 数据按行键的字典序排列,这意味着相近的行键在物理存储上也是相邻的。你可以高效地按前缀扫描数据(比如扫描所有 "com.cnn" 开头的行)。
列族隔离。 不同的列族存储在不同的文件中,可以独立控制访问权限和压缩策略。不常用的列族不会拖累常用列族的性能。
SSTable 存储引擎。 底层数据用 SSTable(Sorted String Table)格式存储------数据按 key 排序后写入磁盘,查询时用二分查找或布隆过滤器快速定位。SSTable 是不可变的(immutable),更新数据时写入新的 SSTable,后台定期合并(compaction)。
动态扩展。 数据按行键范围被切分成多个 tablet,每个 tablet 存在不同的 tablet server 上。当某个 tablet 太大时,自动拆分成两个。
但 Bigtable 有一个问题:它依赖一个叫 Chubby 的分布式锁服务来保证元数据的一致性。Chubby 挂了,Bigtable 就挂了。这其实也引出了后面 Paxos/Raft 等共识算法的重要性。
业内影响
Bigtable 定义了"列式存储"这个范式。HBase 几乎是它的开源翻版,Cassandra 的数据模型也深受其影响。更直接地说,后面 Google 自己的 Spanner、Facebook 的 Cassandra、Apache 的 HBase,数据模型全都有 Bigtable 的影子。LSM-Tree + SSTable 这套存储引擎设计也被广泛采用------RocksDB、LevelDB 都是这条路线上的产物。我个人觉得 Bigtable 论文最精彩的地方是它的数据模型定义------map[row_key][column_family][timestamp] → string,一行公式就说清楚了,简单但足够强大,能支撑 Google 的全部业务。这种抽象能力值得学习。
另外值得一提的是,Bigtable 依赖 Chubby 这个分布式锁服务来保证元数据一致性------Chubby 挂了 Bigtable 就挂了。这其实也说明了共识算法有多重要,直接引出了后面 Paxos/Raft 的话题。
第三章:共识与一致性------分布式系统最核心的难题
Google 三驾马车解决了存储和计算的问题,但它们有一个共同的前提------需要一个机制来保证多个节点对"当前状态是什么"达成一致。这就是共识问题,也是分布式系统中最核心、最硬的骨头。
3.1 Paxos(1989/1998)
论文: Leslie Lamport, The Part-Time Parliament , 1989 写成,1998 发表 链接: lamport.azurewebsites.net/pubs/lampor...
它解决了什么问题
假设你有一个分布式系统,里面有多个节点需要对某个值达成一致。比如一个配置项要从 v1 改成 v2,所有节点必须同意"新值是 v2",不能出现一部分节点认为新值是 v2、另一部分认为是 v3 的情况。
如果所有节点都可靠,这很简单------投票就行了,多数通过就确定。但如果节点可能故障(发消息发到一半挂了、收到消息后还没来得及回复就宕机了),怎么保证在所有故障情况下都不会达成冲突的一致?
核心思路
Paxos 定义了两种角色:Proposer(提案者) 和 Acceptor(接受者)。一个节点可以同时扮演两种角色。
整个协议分为两个阶段:
Prepare 阶段(提案请求): Proposer 向所有 Acceptor 发送一个 Prepare 请求,附带一个提案编号 N(必须是递增的)。 Acceptor 收到 Prepare(N) 后,如果之前没有响应过编号更大的 Prepare 请求,就回复"我答应你",并且把自己之前已经接受过的最大编号的提案(如果有的话)也带上。
Accept 阶段(提案接受): Proposer 如果收到了多数 Acceptor 的回复,就发起 Accept 请求。请求的值怎么选?如果回复中有 Acceptor 带回了之前接受的提案,就选其中编号最大的那个提案的值;如果没有,就用自己的值。 Acceptor 收到 Accept(N, V) 后,如果之前没有响应过编号更大的 Prepare 请求,就接受这个提案。

这样为什么就能保证安全了?关键在于两个不变性:
-
只有被提出的值才能被通过。 不会凭空冒出一个值。
-
只有一个值能被通过。 如果值 v 被通过了,不可能再有另一个不同的值 v' 被通过。
第二个不变性是通过"提案编号的全序关系"来保证的------编号大的提案会覆盖编号小的,而 Acceptor 通过"不再接受更小编号的 Prepare"来锁定自己的承诺。
业内影响
Paxos 是分布式共识的"标准答案",Google 的 Chubby 用的就是 Paxos 变体,Bigtable 的元数据一致性就靠它保证。后来 ZooKeeper 的 ZAB 协议、etcd 之前的旧版本也都受了 Paxos 影响。Multi-Paxos 这个优化版本被 OceanBase 直接采用。关于 Paxos 的正确性证明,网上已经有很多很好的材料了,这里就不展开。
比较值得一提的是 Paxos 论文本身的写作风格------Lamport 用了一个希腊村庄 Paxon 的议会来做比喻,充满了考古学式的叙述。很多工程师读完之后的感受是"每个字都认识,连在一起不知道在说什么"。这种写作风格也直接催生了后面 Raft 的出现------Raft 论文标题就叫"寻找可理解的共识算法",明显是在怼 Paxos 的可读性。
3.2 PBFT------实用拜占庭容错(1999)
论文: Miguel Castro, Barbara Liskov, Practical Byzantine Fault Tolerance , 1999 链接: pmg.csail.mit.edu/papers/osdi...
PBFT三阶段协议 ≈ TCP三次握手
它解决了什么问题
还记得拜占庭将军问题吗?Lamport 在 1982 年证明了"叛徒不超过三分之一时可以达成一致",也给出了算法,但那个算法的通信复杂度是指数级的------节点数多了根本跑不动。
Castro 和 Liskov 要解决的就是:怎么设计一个实用的拜占庭容错算法,让通信复杂度降到多项式级别,能在实际的分布式系统中运行。
核心思路
PBFT 采用了一种"三阶段协议"的方式,通过三轮消息交换来达成共识:
Pre-prepare 阶段: 主节点(Leader)收到客户端请求后,向所有备份节点广播一个 Pre-prepare 消息,包含请求内容和提案编号。
Prepare 阶段: 备份节点收到 Pre-prepare 后,向所有其他节点广播一个 Prepare 消息。每个节点需要收集到 2f+1 个不同的 Prepare 消息(f 是可能的叛徒数),才能进入下一阶段。
Commit 阶段: 节点广播 Commit 消息。同样需要收集到 2f+1 个 Commit 消息,才能最终执行请求。
为什么是 2f+1?因为要保证在任何情况下,忠诚节点收集到的消息中至少有 f+1 个来自忠诚节点(多于叛徒可能伪造的数量),从而确保信息的真实性。总节点数 n ≥ 3f+1,这就是"叛徒不超过三分之一"的具体体现。
业内影响
PBFT 的意义在于它把拜占庭容错从理论拉到了工程层面。在它之前,BFT 算法基本只存在于论文里;在它之后,BFT 真正可以跑起来了。早期区块链项目(比如 Hyperledger Fabric 的早期版本)直接采用了 PBFT 或其变体。后面很多 BFT 的改进版本也都是基于 PBFT 的框架------比如 BFT-SMaRt、HotStuff(Libra/Diem 用的共识协议)都是在这个基础上做优化,主要解决 PBFT 通信复杂度 O(n²) 的性能瓶颈。可以说 PBFT 是整个实用 BFT 领域的起点。
3.3 Raft------看得懂的共识算法(2014)
论文: Diego Ongaro, John Ousterhout, In Search of an Understandable Consensus Algorithm , 2014 链接: raft.github.io/raft.pdf
Paxos因为没有leader的概率导致情况太复杂,Raft只需要考虑怎么定义leader就够了
它解决了什么问题
Paxos 虽然是共识问题的"标准答案",但它有一个严重的问题------太难理解了。Ousterhout 在斯坦福教分布式系统课程时发现,学生们花了大量时间也没能把 Paxos 搞清楚。
Raft 的目标很明确:设计一个跟 Paxos 一样强大、但能让人看懂的共识算法。
核心思路
Raft 的做法是把共识问题拆开来看------拆成三个相对独立的子问题,一个一个解决:
-
Leader 选举: 选一个 Leader 来负责管理日志复制。
-
日志复制: Leader 把客户端的请求记录到日志中,并复制给其他节点。
-
安全性保证: 保证不会出现"两个 Leader 同时对外服务"的情况。
Leader 选举
Raft 中的每个节点有三种状态:Leader(领导者)、Follower(跟随者)、Candidate(候选人)。
系统启动时所有节点都是 Follower。如果 Follower 在一段时间内没有收到 Leader 的心跳,就认为 Leader 挂了,把自己变成 Candidate 发起选举。每个 Candidate 会向其他节点拉票(RequestVote),一个节点在一个任期内只能投一票。获得多数票的 Candidate 成为新的 Leader。
为了防止选票分散,每个任期有一个递增的任期号(Term),类似 Lamport 的逻辑时钟。Candidate 拉票时带上自己的任期号,节点只会投给任期号不低于自己的 Candidate。
日志复制
Leader 收到客户端请求后,把请求追加到自己的日志中,然后并行发送给所有 Follower。当多数 Follower 确认写入后,Leader 把这条日志标记为"已提交",执行对应的操作并返回结果给客户端。
ini
Leader 的日志: [x=1] [y=2] [z=3]
Follower1 的日志:[x=1] [y=2]
Follower2 的日志:[x=1] [y=2] [z=3]
Leader 发送 [z=3] 给 Follower1 和 Follower2
Follower1 确认 → 多数(2/3)已确认 → Leader 提交 z=3
Follower2 确认 → Follower2 也提交 z=3

如果 Follower 的日志跟 Leader 不一致(比如缺了几条、或者值不同),Leader 会强制用自己的日志覆盖 Follower 的。这就是 Raft 的"强 Leader"设计------Leader 说了算。
跟 Paxos 的对比
| 维度 | Paxos | Raft |
|---|---|---|
| Leader | 无固定 Leader,任何节点都可以提案 | 强 Leader 模型,只有 Leader 能处理请求 |
| 可理解性 | 低,论文出了名难读 | 高,论文标题就叫"寻找可理解的共识算法" |
| 工程实现 | 复杂,细节多 | 相对简单,状态少 |
| 性能 | 理论性能相当 | 实际性能相当或略优 |
业内影响
Raft 现在是工业界用得最多的共识算法------etcd(Kubernetes 的核心组件)、Consul(服务发现)、TiKV(TiDB 的存储层)全都用的 Raft。可以说现在云原生生态的基石就是 Raft 在撑着。
我自己的体会是,如果你要理解共识算法,先读 Raft 再回头看 Paxos 会容易很多。Raft 通过强 Leader 模型把问题简化了很多,虽然牺牲了一些灵活性,但在工程实现上友好太多了。"Paxos 更灵活但难实现,Raft 通过强 Leader 简化了问题但牺牲了一些灵活性"------这个对比本身就是一个很好的技术认知。
3.4 一致性哈希(1997)
论文: David Karger et al., Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web , 1997 链接: dl.acm.org/doi/10.1145...
又一个用环来解决的问题
它解决了什么问题
假设你有一个分布式缓存系统,有 N 台缓存服务器。最简单的做法是用 hash(key) % N 来决定一个 key 存在哪台机器上。
这在服务器数量固定时没问题。但如果某台服务器挂了(或者加了一台新服务器),N 变了,几乎所有 key 的映射都会改变。这意味着 N 台机器中有 N-1 台的数据突然失效了,需要重新从后端加载------这在大规模系统中是不可接受的。
一致性哈希要解决的就是:在节点增减时,最小化数据的迁移量。
核心思路
把哈希值想象成一个环------范围从 0 到 2^32-1。
第一步,把每台服务器通过哈希映射到环上的一个位置。比如 3 台服务器 A、B、C 分别在环上的 100、5000、9000 位置。
第二步,对于一个 key,先算它的哈希值,然后在环上顺时针找最近的服务器。比如 key 的哈希值是 3000,顺时针最近的服务器是 B(5000),所以这个 key 存在 B 上。
现在关键的部分来了:如果服务器 B 挂了,只有原本存在 B 上的 key 需要迁移(迁移到顺时针的下一台 C),A 和 C 上的数据完全不受影响。同理,如果加了一台新服务器 D 在 7000 的位置,只有原本存在 C 上、哈希值在 5000-7000 之间的那部分数据需要迁移到 D。
scss
普通哈希取模(N=3 → N=2):
hash(key1) % 3 = 1 → hash(key1) % 2 = 0 ← 变了!
hash(key2) % 3 = 2 → hash(key2) % 2 = 1 ← 变了!
几乎所有 key 都变了
一致性哈希(B 下线):
A 上的 key → 不变
C 上的 key → 不变
B 上的 key → 迁移到 C(只有 B 的数据受影响)
实际使用中还有一个技巧:虚拟节点。如果服务器数量少,它们在环上的分布可能不均匀,导致数据倾斜。解决方法是给每台服务器创建多个虚拟节点(比如 A-1、A-2、A-3),让它们更均匀地分布在环上。
业内影响
一致性哈希是那种"原理五分钟就能看懂,但设计思路真的很巧妙"的东西。把一个线性空间弯成一个环,这个思路本身就很有启发性。Dynamo 用它做数据分片,Memcached 用它做缓存路由,Cassandra 的 token ring 也是基于这个思想。Amazon Dynamo 论文里的"一致性哈希 + 虚拟节点"方案直接就是这篇论文的工程落地。属于那种面试必问、实际也必用的知识点。
第四章:工程范式------影响深远的架构设计思想
前面几章讲的是理论和核心算法。这一章换换口味,看两篇深刻影响了工程实践的论文。它们不是某个具体系统的实现方案,更像是一种设计哲学------告诉你分布式系统"应该怎么做"和"不应该怎么做"。
4.1 Amazon Dynamo(2007)
论文: Giuseppe DeCandia et al., Dynamo: Amazon's Highly Available Key-value Store , 2007 链接: www.allthingsdistributed.com/files/amazo...
站在前人肩膀上,CAP理论的实践,分布式高可用数据库的优秀案例
它解决了什么问题
Amazon 的电商系统有一个硬性要求:购物车必须永远可用。想象一下,你正在网上购物,往购物车里加了几件商品,突然系统返回"服务不可用"------你大概率会直接关掉页面去别的平台买。
Amazon 的分布式环境面临几个挑战:网络分区不可避免、节点随时可能故障、不同商品对一致性的要求不同(购物车可以暂时不一致,但支付金额必须准确)。传统的关系数据库在这种场景下太"刚性"了------要么强一致但可能不可用,要么为了可用性牺牲太多。
核心思路
Dynamo 的做法是把可用性放在第一位,通过一系列工程技巧在一致性和可用性之间灵活取舍。 它把好几个经典技术串了起来,形成了一套完整的方案:
一致性哈希 + 虚拟节点做数据分片。每个 key 映射到环上的 N 个节点(N 个顺时针最近的节点),而不是一个。这样即使某个节点挂了,还有 N-1 个副本可以提供服务。
向量时钟处理并发写入的冲突。每次写入时带一个版本向量 节点A:序号, 节点B:序号, ...,读取时通过比较向量来判断两个版本谁新谁旧,或者是否冲突。
Gossip 协议做节点发现和故障检测。每个节点定期跟随机选择的邻居交换状态信息,像"八卦传播"一样,最终所有节点都会知道整个集群的状态。
Sloppy Quorum做读写仲裁。不要求数据一定写到哈希环上的前 N 个节点,只要写到任意 N 个健康节点就行。这保证了在网络分区时仍然可用。
Merkle Tree做数据同步。当节点恢复后,用 Merkle Tree 快速比较数据差异,只同步不同的部分。

最有意思的是 Dynamo 对一致性的处理方式------它把一致性级别做成了可配置的参数 R 和 W:
-
N = 总副本数
-
R = 读操作需要确认的节点数
-
W = 写操作需要确认的节点数
如果 R + W > N,读写一定有重叠,保证强一致。如果 R + W ≤ N,允许暂时不一致,但可用性更高。开发者可以根据业务需求灵活调整。
业内影响
Dynamo 这篇论文的影响力远超 Dynamo 系统本身。据说 Facebook 的工程师读完这篇论文后,结合自己的需求写出了 Cassandra------Cassandra 的很多设计(一致性哈希、无中心节点、可调一致性级别)都能看到 Dynamo 的影子。整个 NoSQL 运动都受到 Dynamo 思想的启发。
它展示了一个很重要的工程哲学:不要追求完美的一致性,而是根据业务需求在一致性和可用性之间做取舍。这个思想跟 CAP 定理一脉相承,但 Dynamo 把它落地成了可以操作的工程方案------R、W 两个参数一调就能切换一致性级别,这种设计很实用。后面 Riak、Voldemort 等系统也都是直接受 Dynamo 启发做出来的。
4.2 Stonebraker - One Size Fits All?(2007)
论文: Michael Stonebraker, One Size Fits All? An Idea Whose Time Has Come and Gone , 2007 链接: xueshu.baidu.com/ndscholar/b...
不可能存在一种数据库适配所有的场景,不用浪费时间设计大而全的引擎
它解决了什么问题
从 1980 年代到 2000 年代,数据库行业的默认思维是:一个通用的关系数据库(比如 Oracle、DB2、SQL Server)能解决所有问题------不管是在线交易(OLTP)、数据分析(OLAP)、文本搜索还是流处理,用同一个数据库就行了。
Stonebraker 说:这个想法是错的。
核心思路
Stonebraker 用实验证明了一件事:通用数据库在每种场景下都不如专用系统。
他对比了几种典型场景和对应的最优架构:
OLTP 场景(大量短小的读写事务,比如银行转账):需要的是高并发、低延迟的行存储引擎。传统数据库的通用架构里有很多为其他场景设计的组件(比如复杂的查询优化器、列存储),在 OLTP 场景下反而是负担。
OLAP 场景(复杂的分析查询,比如"统计过去一年每个地区的销售趋势"):需要的是列存储,因为分析查询通常只读少数几列但扫描大量行。传统数据库的行存储在这种场景下效率很低。
文本搜索场景:需要的是倒排索引,而不是 B+树。
流处理场景:需要的是持续运行的查询,而不是请求-响应模式。

Stonebraker 的结论是:与其用一个通用架构勉强覆盖所有场景,不如为每种场景设计专用的存储引擎。
业内影响
这篇论文是对数据库行业"大一统"思维的一次深刻反思,直接影响了后来的 NoSQL 运动和新一代数据库的设计思路。它奠定了"专用系统优于通用系统"这个工程认知,后面出现的 Redis(键值存储)、MongoDB(文档数据库)、Elasticsearch(搜索引擎)、Kafka(流处理)这些专用系统,从理念上都跟 Stonebraker 的观点一脉相承。
放在分布式系统里讲,它解释了一个很重要的问题:为什么会有这么多不同的分布式存储方案?GFS、Bigtable、Dynamo、Cassandra、Redis......它们不是重复造轮子,而是因为不同的业务场景确实需要不同的设计取舍。说白了就是没有银弹。Stonebraker 这篇论文读起来非常爽,观点鲜明,论证有力,推荐直接去读原文。
第五章:现代演进------从云原生到国产实践
最后一章看几个近几年的发展。分布式系统从 Google 三驾马车出发,经过十多年演进,已经到了云原生和国产化的阶段。
5.1 Google Spanner(2012/2015)
论文: James Corbett et al., Spanner: Google's Globally-Distributed Database , 2012 + Spanner: Becoming a SQL System , 2015 链接: Spanner 2012 | Spanner: Becoming a SQL System 2015
分布式系统只存在事务顺序没有时间顺序?那我把时间也定义为全局统一就好了
它解决了什么问题
Bigtable 虽然强大,但有几个局限:只支持简单的键值操作,不支持 SQL;不支持跨行的事务;客户端需要自己处理数据的拆分和索引。Google 需要一个全球分布、支持强一致事务和标准 SQL 的数据库。
核心思路
Spanner 在 Bigtable 的基础上加入了关系数据库的能力。它最核心的创新是 TrueTime API。
TrueTime 利用 Google 数据中心里同时部署的 GPS 接收器和原子钟,提供高精度的全局时间信息。每台机器都能知道"现在的准确时间是什么",误差在几微秒以内。有了这个能力,Spanner 可以给每个事务分配一个全局唯一的、严格递增的时间戳,从而实现跨洲际的分布式事务------不需要节点之间来回通信来协调顺序,直接看 TrueTime 就行了。
ini
数据中心A(东京):TrueTime = T1
数据中心B(纽约):TrueTime = T2
因为 TrueTime 精度在微秒级,所以可以确定 T1 和 T2 的先后关系
→ 两个数据中心的事务不需要通信就能确定全局顺序
架构上,Spanner 把数据按 key 范围拆分成多个 tablet,每个 tablet 用 Paxos 协议管理多副本。支持标准 SQL 语法,包括 JOIN、索引、事务(包括分布式事务)。
2015 年的第二篇论文 Spanner: Becoming a SQL System 详细讲述了 Spanner 是怎么从一个 NoSQL 系统演化成支持完整 SQL 的数据库的------包括查询优化器、类型系统、索引等。
业内影响
Spanner 是分布式数据库的标杆项目。它证明了"全球分布 + 强一致 + SQL"这三件事是可以同时实现的------在此之前很多人觉得这是不可能三角。后面很多系统都深受 Spanner 启发:CockroachDB 几乎是 Spanner 的开源复刻,TiDB 的架构设计也参考了 Spanner 的思路,OceanBase 在解决类似问题时走了不同的技术路线但目标一致。可以说 Spanner 定义了现代分布式数据库的基本范式------"按 key 范围分片 + 共识协议保证一致性 + TrueTime 做全局事务协调"。
5.2 Ceph------开源分布式存储(2006)
论文: Sage Weil et al., Ceph: A Scalable, High-Performance Distributed File System , 2006 链接: www.usenix.org/legacy/even...
把所有可能引起单点故障的节点都设计成分布式的,比如分布式文件系统中的master
它解决了什么问题
GFS 是 Google 的内部方案,不开源。开源社区需要一个能跑在大规模集群上的分布式文件系统。HDFS 虽然借鉴了 GFS,但它有一个中心化的 NameNode------NameNode 挂了整个集群就挂了。
Ceph 要解决的是:怎么做一个没有单点故障的大规模分布式文件系统?
核心思路
Ceph 最核心的创新是 CRUSH 算法(Controlled Replication Under Scalable Hashing)。
回顾一下 GFS 的设计:有一个中心化的 Master 来管理"数据在哪个节点上"的映射关系。Ceph 说------我们不需要中心节点,让客户端自己算就行了。
CRUSH 算法的工作方式是:客户端用数据的哈希值和集群的拓扑结构(哪些机器、哪些磁盘)作为输入,通过一个确定性的伪随机算法,计算出数据应该存在哪些节点上。"确定性"意味着不同的客户端对同一份数据会算出相同的结果------所以不需要中心节点来告诉你在哪。
vbscript
GFS:客户端 → Master(查元数据)→ chunk server
Ceph:客户端 → CRUSH算法(自己计算)→ OSD(直接读写)
CRUSH 还支持加权存储------容量大的磁盘存更多数据。当某台机器挂了或新加了一台机器,只需要更新拓扑结构,只有受影响的那部分数据需要迁移(跟一致性哈希的思想类似)。
除了文件系统,Ceph 还提供了 RBD(块设备)和 RADOS(对象存储)接口,三合一的架构让它成为了 OpenStack 等云平台的默认存储后端。
业内影响
Ceph 是开源分布式存储的代表作,证明了"去中心化"的分布式存储不仅理论上可行,工程上也能跑得很好。它现在是 OpenStack 的默认存储后端,在企业级私有云场景中被广泛部署。跟 GFS 对比着看很有意思------中心化简单但有单点,去中心化更健壮但客户端逻辑更复杂。两种设计哲学各自的取舍,在 GFS 和 Ceph 这对比中体现得很清楚。后面 MinIO 等新一代对象存储也在不同方向上延续了这个讨论。
5.3 OceanBase------蚂蚁的分布式数据库(2021)
论文: Guoliang Li et al., OceanBase: A Distributed Relational Database System , VLDB 2021 链接: www.vldb.org/pvldb/vol14...
它解决了什么问题
金融级场景对数据库的要求极其苛刻:既要分布式(扛住双十一的流量洪峰),又要强一致(账务数据不能出错),还要支持 SQL(业务开发效率)。Google 的 Spanner 虽然满足这些条件,但它不开源也不对外。
蚂蚁需要一套自研的、自主可控的分布式关系数据库。
核心思路
OceanBase 的设计融合了多个经典论文的思想:
Paxos 协议保证强一致。 每个数据分区(Partition)用多副本 Paxos 协议管理,保证在少数节点故障时数据不丢不错。跟 Spanner 不同的是,OceanBase 用的是 Multi-Paxos(Paxos 的优化版本),减少了共识过程中的通信轮次。
LSM-Tree 存储引擎。 写入先写到内存中的 MemTable,满了之后刷到磁盘上的 SSTable。后台定期合并多个 SSTable。这种"先写内存再批量落盘"的方式天然适合写密集型场景。
轻量级分布式事务。 基于 Percolator 模型(Google 为 Bigtable 设计的分布式事务方案),OceanBase 实现了支持跨分区、跨节点事务的能力。
HTAP 混合负载。 同一套系统同时支持 OLTP(在线交易)和 OLAP(分析查询),通过行列混合存储来优化不同场景的性能。
2021 年,OceanBase 在 TPC-C 基准测试中刷新了世界纪录,超过了之前由 Oracle 保持了 9 年的纪录。
业内影响
OceanBase 是蚂蚁自研的分布式数据库,在金融级场景经过了多年双十一的实战检验,2021 年 TPC-C 测试刷了世界纪录,超过了 Oracle 保持了 9 年的成绩。从论文阅读的角度看,它有意思的地方在于把前面我们聊过的 Paxos、LSM-Tree、Percolator 这些经典思想都融到了一起,算是一个"集大成者"。读 OceanBase 的论文能帮你把前面学的零散知识点串起来。在国内分布式数据库领域,OceanBase 跟 TiDB 形成了两条不同路线的竞争------一个商业主导,一个开源社区驱动,后面会展开对比。
5.4 TiDB------开源的 Raft 数据库(2023/2024)
论文: TiDB: A Raft-based HTAP Database 链接: www.researchgate.net/publication...
OceanBase的简易开源版
它解决了什么问题
跟 OceanBase 类似,TiDB 也想做一个分布式 HTAP 数据库。但路线不同------TiDB 是开源的,走的是社区路线。
核心思路
TiDB 的架构分两层:
TiDB Server(SQL 层): 负责解析 SQL、生成执行计划、返回结果。本身不存储数据,是无状态的,可以水平扩展。
TiKV(存储层): 一个分布式的键值存储引擎。核心是用 Raft 协议来保证数据一致性------每个数据分片(Region)用一个 Raft Group 管理多副本。
TiKV 的分布式事务基于 Percolator 模型实现,支持乐观和悲观两种事务模式。数据按 key 范围切分成 Region,每个 Region 默认 96MB,太大时自动分裂。
sql
客户端 → TiDB Server(解析SQL、生成执行计划)
↓
TiKV Region 1(Raft Group:Leader + 2 Follower)
TiKV Region 2(Raft Group:Leader + 2 Follower)
...
跟 OceanBase 对比来看:
| 维度 | OceanBase | TiDB |
|---|---|---|
| 共识协议 | Multi-Paxos | Raft |
| 开源 | 部分开源 | 完全开源 |
| 背景 | 蚂蚁集团 | PingCAP |
| 事务模型 | 自研 + Percolator | Percolator |
| 存储引擎 | LSM-Tree | LSM-Tree(RocksDB) |
业内影响
TiDB 是国内开源数据库的代表作,在国际开源社区也有很高的影响力。架构设计清晰,技术文档质量也很高,是很多工程师学习分布式系统的好材料。把 TiDB 和 OceanBase 对比着看,能发现一个有意思的现象:同样是分布式 HTAP 数据库,一个用 Paxos 一个用 Raft,一个商业一个开源------背后的设计取舍是什么?这种对比本身就是阅读经典论文最大的乐趣。
总结

回头看这十几篇论文,最直观的感受就是------分布式系统没有银弹,所有设计都是在做取舍。CAP 定理告诉你取舍的边界在哪,Paxos/Raft 告诉你一致性怎么保证,Dynamo 告诉你可用性怎么做到极致,Stonebraker 告诉你不同场景需要不同的方案。
另一个感受是这些论文之间的关联性比想象中强得多。Lamport 的逻辑时钟 → Paxos 的提案编号 → Raft 的任期号,这是一条线。GFS → MapReduce → Bigtable → Spanner,这是又一条线。拜占庭将军 → PBFT → 区块链共识,这还是一条线。把这些线串起来之后,整个分布式系统的知识图谱就清晰了很多。
下一篇我们会聊 AI 领域的经典论文------从 1958 年的感知机一路到 DeepSeek,看看机器学习的七十年是怎么走过来的。