news 2026/10/3 13:36:38

DDIA 第 10 章精读:一致性与共识——线性一致性、逻辑时钟与共识算法的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DDIA 第 10 章精读:一致性与共识——线性一致性、逻辑时钟与共识算法的完整指南
  • 文档
  • 教程

【免费下载链接】ddia

《Designing Data-Intensive Application》DDIA 第一版 / 第二版 中文翻译

项目地址:https://gitcode.com/gh_mirrors/dd/ddia
点击查看免费下载

本篇技术指南以《Designing Data-Intensive Applications》(DDIA)第二版第 10 章"一致性与共识"为骨架,结合本仓库的 英文原文 与 中文译本 深度展开。你将系统掌握三大核心能力:理解"强一致性"的精确定义——线性一致性(linearizability);掌握分布式 ID 生成器与逻辑时钟(Lamport 时钟、混合逻辑时钟)的设计取舍;并理解共识(consensus)算法如何让分布式系统在线性一致性与容错之间取得平衡。无论你是后端工程师、数据库开发者还是分布式系统架构师,本章的理论框架都是设计可靠数据系统的必修课。

从容错说起:复制带来的不一致难题

分布式系统里可能出错的事情很多(第 9 章 已详述),要让服务在这些故障发生时仍能正确运行,就必须设法容忍故障。复制(replication)是实现容错最有力的工具之一。但把同一份数据复制到多个副本,也带来了不一致的风险:读请求可能由尚未追上进度的副本处理、返回陈旧结果;如果多个副本都能接受写入,还必须解决并发写入产生的值冲突。

处理这类问题有两种彼此竞争的思路:

  • 最终一致性(eventual consistency):把系统采用复制这一事实暴露给应用,由应用开发者处理随之而来的不一致与冲突。采用多主复制(multi-leader replication)和无主复制(leaderless replication)的系统常常使用这种方式。
  • 强一致性(strong consistency):应用不应操心复制的内部细节,系统应当表现得仿佛只有一个节点。代价是更强的一致性会损害性能,而且有些在最终一致系统中尚可容忍的故障,会令强一致系统停摆。

哪种方式更好取决于具体应用:如果应用允许用户离线修改数据,最终一致性不可避免;如果各副本位于通信快速而可靠的数据中心,强一致性的成本通常可以接受,因而往往更合适。

本章深入讨论强一致性,重点考察三个方面:

  1. 线性一致性:"强一致性"这个说法相当含糊,需要给出更精确的目标。
  2. ID 和时间戳的生成:看似与一致性无关,实际上关系密切。
  3. 共识算法:分布式系统如何既实现线性一致性,又保持容错能力。

在此过程中会看到,分布式系统中什么可以做到、什么无法做到,受到一些根本限制。本章内容素以难以正确实现而著称——一个系统在没有故障时运行良好并不难,难的是它可能在某种设计者未曾考虑的不利故障组合下彻底崩溃。

线性一致性:让分布式系统表现得像单机

要让复制数据库尽可能简单易用,最好让它表现得仿佛根本没有复制。这就是线性一致性(linearizability)1——也称为原子一致性(atomic consistency)、强一致性、即时一致性(immediate consistency)或外部一致性(external consistency)2——背后的思想:让系统看起来仿佛只有一份数据,所有操作都原子地作用于这份数据。

在线性一致的系统中,只要一个客户端成功完成写入,此后所有客户端读取时都必须能看到刚写入的值。维持"只有一份数据"的假象,就必须保证读到的是最近写入的最新值,而不是来自陈旧缓存或副本的旧值。换句话说,线性一致性是一种新鲜度保证(recency guarantee)。

以 图 10-1 的体育网站为例:Aaliyah 和 Bryce 坐在同一房间用手机关注比赛,终场比分刚公布,Aaliyah 刷新页面看到了获胜方并告诉 Bryce;Bryce 随后刷新,请求却被路由到落后的副本,页面仍显示比赛进行中。若两人同时刷新,得到不同结果倒不意外;但 Bryce 明确知道自己是听到 Aaliyah 报出比分之后才发起查询的,因此有理由期待结果至少不比 Aaliyah 看到的更旧。返回陈旧数据,就违反了线性一致性。

什么使系统具有线性一致性

在分布式系统理论中,被读写对象称为寄存器(register);实际系统里它可以是键值存储中的一个键、关系数据库中的一行,或文档数据库中的一个文档。寄存器上有两类基本操作:

  • read(x) ⇒v:客户端请求读取寄存器x,数据库返回值v。
  • write(x,v) ⇒r:客户端请求把寄存器x设为v,数据库返回响应r(ok或error)。

考虑一个寄存器x初始值为 0,客户端 C 写 1,同时客户端 A、B 不断轮询读值的场景:

  • A 在写入开始前完成的读取,必然返回旧值 0;
  • A 在写入完成后开始的读取,在线性一致数据库中必然返回新值 1;
  • 与写操作时间上重叠的读取,可能返回 0 或 1——这些操作与写入是并发的。

但这还不够。如果并发读可以任意返回新旧值,读者可能看到值在新旧之间来回跳变,这不符合"只有一份数据"的预期。因此还需一条额外约束:在线性一致的系统中,可以设想写操作起止之间存在某个时刻,x的值在那一点原子地从 0 变为 1。一旦某个客户端读到新值 1,此后所有读取也必须返回 1,即使写操作本身尚未结束——这就是"线性化点"的思想:每个操作在某个时刻原子生效。

更复杂的情形(图 10-4)还加入了第三种操作:

  • cas(x,vold,vnew) ⇒r:原子比较并设置(compare-and-set)操作,若寄存器x当前值等于vold 则原子地改为vnew,否则保持不变并返回错误。

把每个操作视为在某个时刻原子生效,再将各操作标记按序连接,得到的必须是寄存器的一条合法读写序列——每次读取都返回最近一次写入所设置的值。线性一致性要求这些标记连线只能沿时间向前移动(从左向右),绝不能倒退。

几个容易误解的细节:请求发出顺序可以不同于数据库处理顺序(并发请求处理顺序任意,只要合法);一个客户端可能在写客户端收到ok确认之前就读到新值(响应在网络中延迟所致);模型不作事务隔离假设,其他客户端随时可能改值。实践中可以用工具记录所有请求与响应的时序,再检查它们能否排成一条合法的顺序序列来检验线性一致性——只是这种检验的计算成本很高。

线性一致性与可串行化:必须区分的两种保证

线性一致性很容易与可串行化混淆,但二者是完全不同的保证:

特性可串行化(Serializability)线性一致性(Linearizability)
作用对象事务的隔离属性,可读写多个对象(行、文档、记录)寄存器(单个对象)的读写保证
核心保证事务行为等同于按某种串行顺序执行,顺序可与实际运行顺序不同新鲜度保证:若一个操作在另一个操作开始前完成,后者必须观察到至少同样新的状态
防止的问题防止事务交错导致的异常不防止涉及多对象的写入偏斜(write skew)等问题
对陈旧读的态度允许陈旧读不允许

数据库可以同时提供二者,这种组合称为严格可串行化(strict serializability)或强一拷贝可串行化(strong-1SR)。单节点数据库通常既是可串行化又是线性一致的;采用可串行化快照隔离(SSI)这类乐观方法的分布式数据库则更复杂——例如 CockroachDB 提供可串行化以及部分读取新鲜度保证,但不提供严格可串行化,因为那需要事务间昂贵的协调。此外,一致性模型与隔离级别在很大程度上可以彼此独立选择。

依赖线性一致性的场景

锁与主节点选举:采用单主复制(single-leader replication)的系统必须确保只有一个主节点(防止 split brain)。一种选主方式是租约(lease):每个启动的节点尝试获取租约,成功者成为主节点;无论机制如何实现,它必须满足线性一致性——不能让两个节点同时获取租约。Apache ZooKeeper 和 etcd 等协调服务常用来实现分布式租约与选主,它们用共识算法以容错方式实现线性化操作。严格来说 ZooKeeper 只保证写操作线性一致,读可能陈旧;etcd 自 3.x 起默认提供线性化读。Oracle Real Application Clusters(RAC)则按磁盘页加锁,多节点共享同一磁盘存储,由于这些线性化锁处于事务执行关键路径上,RAC 部署通常配备专用集群互联网络。

约束与唯一性保证:用户名、邮箱必须唯一标识一个用户,文件存储服务中不能存在同名同路径文件。若要在写入时强制执行这类约束(两个用户并发注册同一用户名,一个成功一个报错),就需要线性一致性。这实际上类似锁:用户注册相当于在所选用户名上获取"锁",操作也类似原子 CAS——前提是用户名尚未被占用,就把它设为认领用户的 ID。银行账户余额不为负、库存不超卖、机票或剧院座位不重复预订,也都要求所有节点对单一最新值(余额、库存、座位占用状态)达成一致。宽松约束(如超卖后可改签并补偿)可以不依赖线性一致性,但关系数据库中典型的硬唯一性约束需要它;外键等约束则可以不借助线性一致性实现。

跨通道时序依赖:这是最容易忽视的场景。设想一个视频上传网站:Web 服务器把视频写入文件存储服务,写完后通过消息队列通知后台转码器处理。图 10-5 展示了这一数据流。若文件存储服务不是线性一致的,就可能出现竞态:消息队列的投递比存储服务内部复制更快,转码器取回的是旧版本视频甚至什么都没有——原始视频与转码结果永久不一致。问题的根源在于 Web 服务器与转码器之间存在两个通信通道(文件存储与消息队列),没有新鲜度保证就可能出现竞态。移动应用的推送通知场景同样如此:通知很快到达,但随后拉取数据的请求可能打到落后副本,看不到通知所描述的数据。线性一致性不是避免竞态的唯一方法(若你控制额外通道,可用"读自己写入"等替代方案),但它是最容易理解的一种。

实现线性一致的系统:各复制方法的可行性对比

线性一致性本质上意味着"表现得像只有一份数据",最朴素的想法就是真的只用一份数据——但那无法容忍故障:持有该副本的节点一旦宕机,数据就丢失或不可访问。回到第 6 章 的复制方法,逐一评估:

  • 单主复制(可能线性一致):只要所有读写都走主节点,通常就是线性一致的。但前提是你确切知道谁是主节点——节点可能误以为自己是主节点(分布式锁与租约 中讨论过),"妄想型主节点"继续服务请求就会违反线性一致性。异步复制下,故障切换甚至可能丢失已提交写入,同时违反持久性与线性一致性。按分片各设主节点的做法不影响线性一致性(它是单对象保证),跨分片事务则是另一回事。
  • 共识算法(很可能线性一致):一些共识算法本质上是带自动选主与自动故障切换的单主复制,被精心设计为防止 split brain,因而能安全实现线性化存储。例如 ZooKeeper 使用 Zab,etcd 使用 Raft。但使用共识并不自动保证所有操作线性一致:若允许节点不确认自己仍是主节点就提供读服务,新主刚选出时读到的结果可能陈旧。
  • 多主复制(不线性一致):多个节点并发处理写入并异步复制,会产生需要解决的冲突写入,因此一般不满足线性一致性。
  • 无主复制(大概不线性一致):Dynamo 风格的无主复制常被声称通过仲裁读写(w+r>n)获得"强一致性",但严格来说并不成立。

仲裁为何不足以保证线性一致:图 10-6 展示了竞态场景。初始x= 0,写客户端向全部三个副本写 1(n= 3,w= 3);客户端 A 从一个双节点仲裁(r= 2)读到新值 1;客户端 B 从另一个双节点仲裁读到旧值 0。仲裁条件w+r>n虽然满足,但执行并不线性一致:B 的请求在 A 完成后才开始,却读到更旧的值。

要让 Dynamo 风格仲裁线性一致,代价是降低性能:读者必须在返回结果前同步执行读修复;写者必须在写前读取仲裁节点的最新状态,确保新写入的时间戳更大。即便如此,Riak 因性能代价不执行同步读修复;Cassandra 在仲裁读上等待读修复完成,却因使用墙上时钟做时间戳而失去线性一致性。更重要的是,只有线性化的读写操作能这样实现,线性化的 CAS 不行——它需要共识算法。总结:最稳妥的假设是,Dynamo 风格无主系统即使使用仲裁读写,也不提供线性一致性。

线性一致性的代价:CAP 定理与网络延迟

考虑两个区域之间的网络中断(网络分区):多主数据库中每个区域可继续独立运作,写入排队待网络恢复后交换;单主数据库中,跟随者区域的客户端无法联系主节点,既不能写,也不能线性化读(仍可做可能陈旧的普通读),应用在无法触达主节点的区域整体不可用。这个问题不限于单主/多主,任何线性化数据库都有此问题:

  • 若应用要求线性一致性,分区期间部分副本无法处理请求——要么等待网络修复,要么返回错误,即CP(分区时保持一致性)。
  • 若不要求线性一致性,可让每个副本独立处理请求(如多主),分区期间仍可用(AP,分区时保持可用)。

这一洞察即著名的CAP 定理,2000 年由 Eric Brewer 命名,虽然 1970 年代分布式数据库设计者就已知道这一权衡。CAP 最初只是作为启发式规则提出,旨在开启数据库权衡的讨论,客观地说它推动了 NoSQL 运动——这值得肯定。

但 CAP 也常被误导性地表述为"一致性、可用性、分区容错性:三选二"。这种表述有误导性:网络分区是一种故障,你无从选择它是否发生。更准确的说法是:网络正常时系统可同时提供一致性与可用性;网络故障时必须在二者间选择。此外,CP/AP 分类还有若干缺陷:CAP 的"一致性"被形式化为线性一致性(对弱一致性模型无话可说),其对"可用性"的形式化也与通常含义不符,还有些系统两者都不提供、既非 CP 也非 AP。从正式定义看,CAP 定理范围很窄:只考虑一种一致性模型(线性一致性)和一种故障(网络分区——据 Google 数据,分区只占不到 8% 的事故),不涉及网络延迟、宕机节点或其他权衡。CAP 虽有历史影响力,但对设计系统的实际价值有限,最好避免使用。

作为推广,PACELC 原则指出:网络分区时(P)需要在可用性(A)与一致性(C)间选择;否则(E)时,可在低延迟(L)与一致性(C)间选择。实践中很少有系统真正线性一致:多核 CPU 的 RAM 都不线性一致(缓存与存储缓冲异步回写主存),因为放弃线性一致性是为了性能而非容错。Attiya 与 Welch 证明:若要线性一致性,读写响应时间至少与网络延迟的不确定性成正比;在高延迟变动的网络中,线性化读写响应时间必然很高,不存在更快的线性化算法。弱一致性模型则可以快得多——这个权衡对延迟敏感系统至关重要。

ID 生成器与逻辑时钟

很多应用需要为数据库记录分配唯一 ID 作为主键。单节点数据库常用自增整数:只占 64 位(若确定记录数不会超过 40 亿也可用 32 位,但这很冒险),且 ID 顺序即创建顺序——例如聊天应用可据自增 ID 排序消息,Aaliyah 的提问 ID 为 1,Bryce 的回答 ID 更大(图 10-8)。

这个单节点 ID 生成器本身就是一个线性化系统:每次取 ID 都是一次原子递增并返回旧值的操作(fetch-and-add),线性一致性保证先完成的消息获得更小 ID。内存中的实现很容易(用 CPU 原子递增指令即可),难点在于持久化(节点崩溃重启不能重置计数器导致重复 ID)以及三个现实问题:单点故障、跨地域取 ID 需绕地球半圈的网络往返、高写入吞吐下成为瓶颈。

分布式 ID 生成方案对比

方案优点缺点
分片 ID 分配多个节点并行分配,ID 仍紧凑丢失排序属性:ID 16 与 17 无法判断谁先发出(不同节点可能进度不同)
预分配 ID 块节点从块内独立发号,块将耗尽时再向中心申请排序同样不保证:后分配的消息可能拿到更小的 ID
随机 UUID(v4)本地生成无需通信,碰撞概率极低占用 128 位;顺序随机,无法比较新旧
墙上时钟 + 唯一性填充高位为时间戳可粗略排序;实现如 Version 7 UUID、Twitter Snowflake、ULID、Hazelcast Flake、MongoDB ObjectID依赖 NTP 时钟同步;时钟跳变或偏斜时排序可能与真实事件顺序不一致,难以线性一致

这些方案都能生成足够唯一的 ID,但排序保证远弱于单节点自增。基于墙上时钟的 ID 生成器还受制于时钟偏斜:稍快的时钟先写的事件可能拿到更晚的时间戳。利用原子钟或 GPS 接收机做高精度同步可缓解,但能否不依赖特殊硬件就生成唯一且有序的 ID?这就要说到逻辑时钟。

逻辑时钟:Lamport 时间戳与混合逻辑时钟

物理时钟(墙上时钟、单调时钟)测量流逝的秒数;逻辑时钟(logical clock)则是统计已发生事件的算法。逻辑时钟的时间戳不告诉你"现在几点",但可以比较两个时间戳的先后。典型要求:时间戳紧凑(几个字节)且唯一;任意两个时间戳可比较(全序);顺序与因果一致——若操作 A 发生在 B 之前,则 A 的时间戳小于 B 的。

Lamport 时间戳(1978 年 Leslie Lamport 提出):每个节点有唯一标识(实践中可用随机 UUID),并维护一个计数器;时间戳即二元组(counter,node ID)。每次生成时间戳,节点递增本地计数并使用新值;每次看到来自其他节点的时间戳,若其计数大于本地计数,就把本地计数提升到该值。比较时先比较计数;计数相同则按节点 ID 字典序比较。

例如 图 10-9 中 Aaliyah 和 Caleb 各自从 0 递增到 1 发消息;Bryce 收到后把计数提升到 1,回复时再递增到 2。时间戳顺序为 (1, "Aaliyah") < (1, "Caleb") < (2, "Bryce")。Lamport 时钟的局限:与物理时间无直接关系,无法按日期检索事件;互不通信的节点计数可能差距悬殊。

混合逻辑时钟(HLC)结合物理时钟的读数能力与 Lamport 时钟的排序保证:像物理时钟一样计数秒或微秒,看到更大的其他节点时间戳时把自己的本地值前移,每次生成时间戳再递增——保证单调前进,即使底层物理时钟(如 NTP 调整)回跳。HLC 时间戳几乎可以当作常规墙上时钟时间使用,又附加了与 happened-before 关系一致的排序,不依赖特殊硬件,只需大致同步的时钟。CockroachDB 即使用 HLC。

与向量时钟的取舍:Lamport/HLC 适合生成快照隔离的事务 ID(保证快照与因果一致)。但当多个时间戳并发生成时,算法会任意排序它们,一般无法从两个时间戳判断是否并发。若要能判断记录是否并发创建,需要向量时钟——代价是时间戳大得多,可能为每个节点存一个整数。

线性化 ID 生成器:从逻辑时钟到共识的缺口

图 10-10 展示了非线性化 ID 生成器引发的问题:用户 A 先在笔记本上把公开账号改为私密,再用手机上传私密照片。账号权限与照片存储在两个数据库(或同一数据库的不同分片),各自用 Lamport/HLC 分配时间戳。照片库没读过账号库,本地计数落后,照片上传被分配了比账号设置更新更小的时间戳。查看者用 MVCC 快照读时,快照时间戳大于照片上传却小于账号更新,于是系统判定账号当时仍公开,展示了不该看到的私密照片。

最简单的修复是用线性化 ID 生成器,确保照片上传获得更大 ID。实现方式:单节点原子递增计数器 + 持久化(防重启重复)+ 单主复制(容错)。TiDB/TiKV 称之为时间戳预言机(timestamp oracle),灵感来自 Google Percolator。优化:不必每次请求都做磁盘写入与复制,可批量分配——持久化复制一条描述一批 ID 的记录后,节点按序发放;节点崩溃或切换到跟随者时会跳过部分 ID,但不会重复或乱序。该生成器不能轻易分片(多个分片发号无法保证全序线性化),也难以跨地域分布;好在其职责极简,单节点可支撑高吞吐。

替代方案是 Google Spanner 的做法(第 9 章"同步时钟用于全局快照"):物理时钟返回的不是单一时间戳而是一个表示不确定性的区间,然后等待该不确定区间过去再返回。若区间估计正确(真实物理时间总在区间内),即使跨地域请求也能在无通信的情况下正确排序,前提是硬件与软件对时钟同步和不确定性区间计算提供支持。

为什么逻辑时钟不足以实现锁与唯一性约束:用逻辑时钟给争抢同一锁/用户名的请求排序、取最小时间戳者为胜看似可行,但难题在于:节点如何知道自己的时间戳就是最小?它必须听到每一个可能生成时间戳的节点的回应——若有节点故障或网络不通,系统就会停滞,因为我们无法确定那个节点是否持最小时间戳。这不符合容错要求。要实现容错的锁、租约等构造,需要比逻辑时钟或 ID 生成器更强的东西——共识。

共识:分布式系统的基石

本章已经看到许多"单节点容易、容错后极难"的问题:单主复制如何安全故障切换而避免 split brain;线性化 ID 生成器崩溃后怎么办;CAS 操作(决定谁获得锁/租约、保证文件或用户名唯一性)如何做到容错。所有这些问题都是同一个根本问题的实例:共识(consensus)——分布式计算中最重要、最基础、也最臭名昭著难以做对的问题。

最著名的共识算法包括 Viewstamped Replication、Paxos、Raft 和 Zab,彼此相似但不相同。它们工作在非拜占庭系统模型下:网络消息可任意延迟或丢弃,节点可崩溃、重启、断连,但假定节点会正确遵守协议、不恶意行为。另有能容忍部分拜占庭节点的 BFT 算法(常见假设是少于三分之一的节点为拜占庭故障),用于区块链——但超出本书范围。

FLP 不可能性:共识真的无解吗?

以 Fischer、Lynch、Paterson 命名的FLP 结果证明:在节点可能崩溃的系统中,不存在总能达成共识的算法。但 FLP 只说明不能保证总是终止,且其证明基于异步系统模型中的确定性算法(不能使用时钟或超时)。只要允许使用超时来怀疑节点崩溃(哪怕有时猜错),共识就变得可解;甚至仅允许算法使用随机数也能绕过不可能性。因此,虽然 FLP 在理论上极其重要,分布式系统在实践中通常可以达成共识。

共识的多种面孔:等价问题家族

共识可以表述为多种形式,它们彼此等价——有了其中一个的解法,就能转换成其他任何一个的解法:

  • 单值共识:多个节点对一个值达成一致,类似原子 CAS,可实现锁、租约、唯一性约束。共识算法必须满足四条性质:

    • 统一同意(Uniform agreement):没有两个节点做出不同决定;
    • 完整性(Integrity):节点一旦决定某值,不能改判另一个值;
    • 有效性(Validity):节点决定的值v必须曾被某个节点提出过(排除"总是决定 null"这类平凡解);
    • 终止性(Termination):不崩溃的节点最终都做出决定——这是活性(liveness)属性,前三者是安全性(safety)属性。

    不关心容错时,前三条性质很容易满足(硬编码一个"独裁者"节点做决定即可),但独裁者失败系统就停摆。共识算法要求至少多数节点正常运转才能保证终止;好在安全属性(同意、完整、有效)即使在多数节点故障或严重网络问题时也始终成立——大规模故障只会让系统无法处理请求,不会造成不一致的决定。

  • CAS 即共识:有了容错线性化的 CAS,容易解决共识——对象初始化为 null,节点用 CAS(期望 null,新值为自己的提议)竞争,最终对象的值即决定值;反之,有了共识也能实现 CAS(用共识协议决定 CAS 的新值,落选者返回错误)。但线性化读写寄存器不足以解决共识——这正是从 FLP 与仲裁实现寄存器的事实推出的结论。

  • 共享日志 / 全序广播即共识:日志存储有序条目序列,所有读者看到相同顺序。共享日志(shared log,形式化为全序广播、原子广播)的性质:最终追加(请求者最终读到自己的值)、可靠投递(不丢条目)、仅追加(条目不可变)、同意(读同一条目 e 之前必须读到完全相同的前缀序列)、有效性。有了共享日志即得共识:每个提议者请求把值追加到日志,日志中第一个出现的值即决定值;反之,为每个未来日志槽位运行一次共识实例,被选中的值依次追加成条目。单主复制不满足活性要求——主节点崩溃就停止投递,挑战在于安全而自动地故障切换。

  • fetch-and-add 与共识数:线性化 ID 生成器(fetch-and-add)几乎就是共识但差一步。所有节点执行 fetch-and-add 后,读到 0 的节点是赢家——但其他节点不知道赢家是谁;若赢家在广播结果前崩溃,共识无法终止。例外是确定最多两个节点提议时(两节点互发提议再各自 fetch-and-add 即可解决),因此 fetch-and-add 的共识数(consensus number)为 2;而 CAS 与共享日志对任意数量节点可解,共识数为 ∞。

  • 原子提交即共识:分布式事务的原子提交(如两阶段提交)与共识表面相似但有重要区别:共识可以决定任意被提出的值,而原子提交只要任一参与者投票中止就必须中止。原子提交的性质:统一同意、完整性、有效性(若决定提交则所有节点必须此前都投了提交票;任一节点投了中止则必须中止)、非平凡性(全部节点都投票提交且无通信超时则必须提交)、终止性。有了共识可解原子提交(每节点把投票提交/中止提议给共识算法,得知决定后相应提交或中止);有了容错原子提交也可解共识——两者等价。

实践中的共识:从共享日志到自动化选主

理论等价性很有价值,但实践中最有用的形式是什么?答案是共享日志(全序广播):Raft、Viewstamped Replication、Zab 直接提供共享日志;Paxos 提供单值共识,实践中多数系统使用其扩展 Multi-Paxos 也提供共享日志。

共享日志的用途:每个日志条目代表一次数据库写入,所有副本用确定性逻辑按相同顺序处理相同写入,最终状态一致——即状态机复制(state machine replication),也是事件溯源(第 3 章)背后的原理;共享日志还可用于流处理(第 12 章)。每个日志条目代表一个确定性存储过程事务、各节点按相同顺序执行,即可实现可串行化事务(实际串行执行)。强一致模型的分片数据库通常每分片维护独立日志,这提升可扩展性,但限制跨分片保证(一致快照、外键引用);跨分片可串行化事务需要额外协调。共享日志还能衍生出其他共识形式:决定日志中第一个出现的值即单值共识;把座位号写入条目即可为每个座位做一次决定;把计数增量写入条目、当前计数值即历史条目之和——日志上的简单计数器可用于生成 fencing token(ZooKeeper 中叫zxid)。

从单主复制到共识:传统单主数据库把主节点故障切换留给人工 DBA 操作,这带来大量停机时间,也不满足共识的终止性。矛盾在于:选主需要共识,解共识又需要主节点——如何打破循环?答案是共识算法并不要求任何时刻只有一个主节点,而是定义纪元号(epoch number,Paxos 称ballot number,Viewstamped Replication 称view number,Raft 称term number),保证每个纪元内主节点唯一。节点超时未听到主节点消息时可发起更高纪元号的新选举;两个纪元的冲突以更高纪元的主节点为准。主节点追加下一条日志前,必须先通过收集法定人数节点的投票确认不存在更高纪元的主节点。于是有两轮投票:选主一次,为主节点追加日志的提议投票一次——两次投票的仲裁必须相交,从而保证赢得提议投票时没有更高纪元的主节点被选出。这与两阶段提交表面相似但本质不同:共识算法中任何节点可发起选举、只需仲裁响应;2PC 只有协调者可请求投票、提交前需要所有参与者投"是"。

共识的微妙之处:所有 Raft/Multi-Paxos/Zab/VR 共享这一基本结构:仲裁投票选出主节点,主节点追加的每条日志再经一次仲裁投票,每条新条目在向客户端确认前同步复制到仲裁——确保主节点故障时不丢条目。细节差异在于:旧主故障后,Raft 只允许日志至少与多数跟随者一样新的节点当选;Paxos 允许任意节点当选,但要求其先补全日志再追加新条目。若允许陈旧节点当选,它可能覆盖旧主已写入的条目,违反仅追加属性——严格保证共识属性要求新主在服务写入/线性化读前补齐所有已确认条目。有些系统会为更快恢复而放宽这一要求,例如 Kafka 的unclean leader election允许任何副本当选;异步复制的数据库也无法保证故障切换时任一跟随者是最新的。放宽后性能与可用性可能改善,但共识理论不再适用,故障时极易造成大量数据丢失或损坏。此外,线性化读也需要像写一样经过仲裁投票确认"自认主节点"确实仍是最新的(etcd 的线性化读即如此);多数共识算法假定固定的节点集合,实践中需要重配置(reconfiguration)功能支持增删节点,例如跨地域扩展或迁移。

共识的优缺点:共识本质上是"做得对的单主复制"——自动故障切换、不丢已提交数据、杜绝 split brain,这是巨大突破。但代价不菲:始终需要严格多数(容忍 1 个故障至少 3 节点,容忍 2 个至少 5 节点);每次操作都要与仲裁通信,加节点只会让算法更慢(吞吐不增反降);分区时只有多数侧能推进。共识依赖超时检测故障,延迟波动大(尤其跨地域)时超时调优困难:太大则故障恢复慢,太小则频繁无谓选举、系统把时间花在选主上。Raft 还被证明存在不愉快边界情况:当整个网络正常、仅某一条链路持续不可靠时,领导权可能在两个节点间反复横跳,系统实际无法推进。想要高可用又不接受共识成本,唯一现实替代是弱一致性模型(无主或多主复制)——它们通常不提供线性一致性,但对不需要它的应用正合适。

协调服务:共识的忠实用户

协调服务(ZooKeeper、etcd、Consul)是共识算法最突出的用户。它们外表像键值存储,但并非为通用数据存储设计,而是用于协调另一个分布式系统的节点:Kubernetes 依赖 etcd,Spark 与 Flink 的高可用模式依赖 ZooKeeper。协调服务的数据量小到可全部放入内存(仍写磁盘保证持久性),由容错共识算法复制。这类服务以 Google 的 Chubby 锁服务为模型,把共识算法与几项对构建分布式系统特别有用的特性结合:

  • 锁与租约:利用共识实现的容错原子 CAS——多个节点并发争抢同一租约时只有一个成功。
  • fencing 支持:给每个日志条目单调递增 ID(ZooKeeper 的zxid、cversion,etcd 的 revision 号),用于生成 fencing token 防止进程暂停或大延迟时客户端相互干扰。
  • 故障检测:客户端维持长会话、周期心跳;心跳超时则服务端认定客户端死亡并释放租约(ZooKeeper 称临时节点,ephemeral nodes)。连接暂时中断或服务器故障时租约保持有效。
  • 变更通知:客户端可订阅键变化通知,从而发现其他客户端加入集群或失败(会话超时、临时节点消失),免去频繁轮询。

故障检测与变更通知本身不需要共识,但配合依赖共识的原子操作与 fencing 支持,构成了分布式协调的完整工具箱。协调服务也常用来存储配置(超时、线程池大小等键值对):进程启动时加载最新配置并订阅变更通知,配置变化后立即生效或重启加载。配置管理本身不需要共识,但既然已在运行协调服务,顺便利用其通知能力很方便。

工作分配:协调服务适合"从多个进程实例中选主/主节点、失败后接管",也适合为分片资源(数据库、消息流、文件存储、分布式 actor 系统)决定分片归属、在节点加入/退出时再平衡。正确组合原子操作、临时节点与通知,应用就能自动从故障中恢复——虽不简单(Apache Curator 等库提供了 ZooKeeper 客户端之上的高级配方),但远比自己实现共识算法可靠。专用协调服务的另一优势:无论被协调系统有多少节点,协调服务自身通常只需固定三五个节点——给上千个分片跑共识算法极其低效,把共识"外包"给少量节点要划算得多。注意协调服务适合变化缓慢的数据("IP 10.1.1.23 的节点是分片 7 的主节点",分钟或小时级变化);每秒变化数千次的数据应使用常规数据库,或用 Apache BookKeeper 复制服务内部快速变化的状态。

服务发现:ZooKeeper、etcd、Consul 也常用于服务发现——云端虚拟机来来去去,服务启动时在网络端点注册表注册自身,供其他服务查找。既然已经用协调服务做租约、锁或选主,顺便用它做服务发现很自然。但对服务发现而言共识往往大材小用:这个用例通常不需要线性一致性,更需要高可用与低延迟。因此更常见的做法是缓存服务发现信息、容忍轻微陈旧——DNS 式服务发现就用多层缓存换取性能与可用性。为此 ZooKeeper 支持observer(观察者):接收日志并维护数据副本但不参与投票的副本——observer 读不线性化(可能陈旧),但网络中断时仍可用,并通过缓存提升系统可支撑的读吞吐。

总结

本章深入考察了容错系统中的强一致性:是什么,以及如何实现。

  • 线性一致性是强一致性的流行形式化:复制数据表现得仿佛只有一份拷贝,所有操作原子地作用于它。需要读到最新数据、或需要解决竞态(如多节点并发创建同名文件)时,线性一致性非常有用。它容易理解(让数据库表现得像单线程程序中的变量),但代价是慢——尤其在网络延迟大的环境中。许多复制算法表面上可能像提供了强一致性,实际并不保证线性一致性。
  • ID 生成器:单节点自增计数器是线性一致的,但不容错;许多分布式 ID 生成方案不保证 ID 顺序与事件真实发生顺序一致。Lamport 时钟、混合逻辑时钟等逻辑时钟提供与因果一致的排序,但不提供线性一致性。
  • 共识意味着以所有节点同意、且不可改判的方式做出决定。大量问题都可归结为共识且彼此等价:线性化 CAS、锁与租约、唯一性约束、共享日志(全序广播)、原子事务提交、线性化 fetch-and-add(此例只对两个节点可解)。这些在单节点上都很简单,或可以交给单一决策节点(单主数据库的实质);但主节点故障或不可达时系统停滞,直到人工故障切换。Raft、Paxos 等共识算法本质上是内置自动选主与故障切换的单主复制——精心设计确保故障切换不丢已提交写入、不进入多节点同时接受写入的 split brain 状态。这要求每次写入和每次线性化读都被仲裁(通常为多数)确认,跨地域尤其昂贵,但这是强一致性与容错兼得的必要条件。
  • 协调服务(ZooKeeper、etcd)基于共识算法构建,提供锁、租约、故障检测与变更通知,用于管理分布式应用状态。若你想做某件可归结为共识的事且要求容错,使用协调服务是明智之选——它不保证你一定做对,但很可能帮到你。
  • 共识并不总是对的工具:有些系统不需要强一致性,弱一致性加高可用与更好性能更合适(可用无主或多主复制),本章的逻辑时钟在那类场景中很有帮助。

共识算法复杂而微妙,但有 1980 年代以来发展出的丰富理论支撑,使构建能容忍第 9 章 所述各种故障、同时保证数据不被破坏的系统成为可能——这是了不起的成就。本章参考文献列出了该领域的重要工作,可作为深入学习的起点。


仓库资源索引:本篇基于 英文原文(含完整参考文献列表)与 中文译文 撰写,繁体版本见 content/tw/ch10.md;全部插图位于 static/fig/(本文引用的 图 10-1、图 10-6、图 10-9 等时序示意图);本仓库为 Hugo 站点,构建配置见 hugo.yaml(启用了 footnote、table 等 Markdown 扩展),可通过 Makefile 中的make dev、make build、make epub等目标本地构建与导出。


  1. Maurice P. Herlihy and Jeannette M. Wing, "Linearizability: A Correctness Condition for Concurrent Objects," TOPLAS 12(3), 1990.

    ↩
  2. David K. Gifford, "Information Storage in a Decentralized Computer System," Xerox PARC, CSL-81-8, 1981.

    ↩
  • 文档
  • 教程

【免费下载链接】ddia

《Designing Data-Intensive Application》DDIA 第一版 / 第二版 中文翻译

项目地址:https://gitcode.com/gh_mirrors/dd/ddia
点击查看免费下载
上一篇:Flutter-Notebook深度链接:App Links与Universal Links配置
下一篇:2025企业AI成本革命:T-pro-it-2.0-GGUF如何让本地化部署成本直降60%

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 13:28:48

【LSSVM回归预测】基于matlab蝙蝠算法优化最小二乘支持向量机BA-LSSVM回归预测【含Matlab源码 109期】

⛄一、运行结果 ✅博主简介:热爱科研的Matlab仿真开发者,修心和技术同步精进,Matlab项目合作可私信。 🍎个人主页:海神之光 🏆代码获取方式: 海神之光Matlab王者学习之路—代码获取方式 ⛳️座右铭:行百里者,半于九十。 更多Matlab仿真内容点击👇 Matlab图像处理…

作者头像 李华
网站建设 2026/10/3 13:27:10

CogImageFileTool深度解析:VisionPro图像数据流枢纽

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:27:04

AURIX TC4x看门狗WTU全解析:原理、配置与调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 13:27:02

MIGO屏幕增强:基于BADI MB_MIGO_BADI的自定义字段实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华