这一章在做什么
复制让数据有了好几份。第 6 章把由此带来的不一致交给应用去处理,这一章走另一条路:让系统看起来只有一份数据。先给「强一致」一个精确的定义,叫线性一致性,看哪几种复制方式做得到、要付出多少延迟;再看分布式的发号器和逻辑时钟为什么只能保证因果;最后落到共识:选主、锁、唯一约束、共享日志、原子提交,其实是同一个问题。代价是每一步都要多数节点点头。
读之前:先预测
这一章是「分布到多台机器」这一块的收口,前面三章留下的问题都在这里回收:第 6 章的复制延迟、多主和无主复制的冲突、法定人数;第 8 章的比较并设置(CAS)和两阶段提交里那个单点的协调者;第 9 章的租约、防护令牌、不可靠的时钟和系统模型。往后看,第 12 章的流处理会用到这里的共享日志,第 13 章会讨论怎样在不要线性一致的情况下仍然做对。
数据有好几份,机器会坏,网络会慢。怎样让这样一组机器对外表现得像只有一份数据、一个说了算的人?要付出什么?什么时候不值得付?
- 线性一致性和可串行化都像是「排成一个顺序」,它们各自管的是什么?§1.1
- 无主复制里 w + r > n 的法定人数读写,为什么还不算线性一致?§1.3
- Lamport 时间戳和因果一致,为什么用它给两个库的写入编号还是会出错?§2.2
- CAS、共享日志、原子提交为什么都能叫「共识」?哪一个只差一点?§3.1
这一章的骨架
这一章分三段:§1 定义并评估线性一致性,§2 从 ID 生成器绕到逻辑时钟,§3 讲共识。容易乱的是 §2:它看上去跑题了,其实是在比较「保证因果」和「保证线性一致」差在哪,又说明两者都不够用来做容错的锁,把你推进 §3。§3.1 一口气列了五种问题,读的时候每一种都问一句:这里要决定的是「谁赢」还是「什么顺序」。
边读边填:每行一个问题,读到答案就写一两句。只存在你这台设备的浏览器里,别人看不到。
| 要找的 | 带着这个问题读,找到了就写下来 |
|---|---|
| 两个「排成顺序」 | 线性一致和可串行化各管什么?两样都要,叫什么?§1.1 |
| 四种复制方式 | 单主、共识、多主、无主,各自能不能做到线性一致?卡在哪?§1.3 |
| 真正的代价 | 很多系统放弃线性一致,主要是为了什么?§1.4 |
| 逻辑时钟的边界 | Lamport 时间戳保证什么,不保证什么?§2.1§2.2 |
| 发号器做锁 | 有了线性一致的发号器,为什么还做不了容错的锁?§2.2 |
| 只差一点 | fetch-and-add 为什么只能给两个节点解决共识?§3.1 |
| 先有主还是先有共识 | 选主要靠共识,共识又要有主。这个死结怎么解开?§3.2 |
读完了?对照我找到的
- 两个「排成顺序」:可串行化管多对象的事务,排出的顺序可以和真实时间不符;线性一致管单个对象,要求新近。两样都要叫严格可串行化。
- 四种复制方式:单主要读写都走主、并且确知谁是主;共识大多可以,读也要确认主的身份;多主不行;无主基本不行,法定人数也不够。
- 真正的代价:多数是为了性能(延迟),不是为了容错;而且这份延迟一直在付,不只在网络出故障时。
- 逻辑时钟的边界:给出和因果一致的全序;管不了自己没见过的时间戳,所以不线性一致,也分不出并发。
- 发号器做锁:要确认自己的号最小,得听到所有节点的回音,有一个联系不上就只能干等。
- 只差一点:拿到 0 的知道自己赢了,其余的只知道自己输了、不知道谁赢;赢家一挂,大家悬着。
- 先有主还是先有共识:只保证每个任期里主唯一;追加日志前再拉一次多数票,两次多数必然重叠,过期的主会被发现。
分节地图
全章约 1.7 万词,建议分五次读。第一、二次是线性一致性,例子多、读得快;第三次的逻辑时钟篇幅不长但概念密;第四次的 §3.1 是全章最抽象的一节,五种问题一个接一个,最好一口气读完;第五次回到工程,节奏轻一些。
- 第一次坐下§0–§1.2 · 约 3600 词 · 什么是线性一致,什么时候非它不可
- Lots of things can go wrong…约 500 词开篇最终一致和强一致两种思路;这一章只走强一致这条路,分三块讲。
- Linearizability约 400 词线性一致性目标是让多副本的库用起来像单机。一个看比分的例子引出「新近」。
- What Makes a System Linearizable?约 1600 词什么样的系统算线性一致用请求的时间条画读写,规则一条条加上去;方框区分它和可串行化。
- Relying on Linearizability约 1100 词依赖线性一致的场合锁与选主、唯一约束、跨通道的时序依赖:三种非它不可的情形。
- 第二次坐下§1.3–§1.4 · 约 2300 词 · 哪些系统做得到,代价是什么
- Implementing Linearizable Systems约 800 词实现线性一致的系统第 6 章的几种复制方式逐个过一遍。留意法定人数的那个反例。
- The Cost of Linearizability约 1400 词线性一致的代价网络断开时只能二选一;CAP 为什么不好用;平时就在付的代价是什么。
- 第三次坐下§2–§2.2 · 约 3200 词 · 从发号器绕到逻辑时钟
- ID Generators and Logical Clocks约 1000 词ID 生成器与逻辑时钟单机自增 ID 好在哪;几种分布式发号办法各自丢了什么。
- Logical Clocks约 1100 词逻辑时钟Lamport 时钟、混合逻辑时钟、向量时钟,各自能比出什么、比不出什么。
- Linearizable ID Generators约 1100 词线性一致的 ID 生成器只保因果会出什么事;怎样做到线性一致;为什么还不够实现锁。
- 第四次坐下§3–§3.1 · 约 3300 词 · 共识和它的几副面孔
- Consensus约 600 词共识前面那些难题其实是同一个问题;方框解释 FLP 结论为什么不妨碍实践。
- The Many Faces of Consensus约 2700 词共识的多种面孔单值共识、CAS、共享日志、fetch-and-add、原子提交,看它们怎样互相转化。
- 第五次坐下§3.2–§4 · 约 4300 词 · 真实的共识系统和协调服务
- Consensus in Practice约 2000 词实践中的共识任期号和两轮投票;新主怎样接住旧日志;共识的好处和代价。
- Coordination Services约 1300 词协调服务ZooKeeper、etcd 这类系统提供什么,适合和不适合拿来做什么。
- Summary约 1000 词小结全章回顾。先自己回忆一遍再读它。
先用中文过一遍
线性一致性:假装只有一份数据
复制以后,同一个值在几台机器上各有一份,一次更新到达各处的时间不同。线性一致性(linearizability)要求系统对外表现得像只有一份数据:每个操作都在「请求发出」和「收到响应」之间的某一刻一下子生效。客户端看不到那一刻在哪,只知道它落在自己那根时间条里。
落到读上是两条规矩。和写在时间上重叠的读,读到旧值、新值都行,因为谁也说不清服务器先处理了哪个。但只要有一个读者读到了新值,此后才开始的读,不管来自哪个客户端,都不许再读到旧值;值不能在新旧之间来回跳。所以它是一种关于「新近」的保证。它只管单个对象,不把几次读写打包成事务,这是它和可串行化的分界,§1.1 有个方框专门讲。
逻辑时钟:数事件,不看钟表
几台机器各自发号时,号码的大小和事件的先后对不上:时钟有快有慢,分段发的号各走各的。逻辑时钟(logical clock)不管现在几点,只数事件。Lamport 的做法就两条:自己每产生一个时间戳,计数先加一;每看到别人带来的时间戳,如果比自己的计数大,就把计数抬到那个值。时间戳写成(计数, 节点 ID),先比计数,计数相同再比节点 ID,任意两个都能比出大小。
它保证的方向是:X 先于 Y 发生(Y 所在的进程沿着消息链能追到 X),X 的时间戳就一定更小。反过来不成立。两件互不知情的事也会被排出一个先后,只看时间戳分不出「先发生」和「并发」。想分辨并发,要用向量时钟:每个节点一格计数,比较时逐格比,代价是时间戳随节点数变长。
共识:任期号,加两次多数票
单主复制里,主节点说了算:谁拿到锁、日志下一条写什么、事务提交还是回滚,都由它决定。难的是主节点挂了怎么办:换主需要所有节点对「谁是主」意见一致,这本身又是一个要达成一致的问题。
常用的共识算法这样绕开:不保证任何时刻只有一个主,只保证每个任期(epoch,Raft 叫 term)里最多一个主,任期号只增不减。某个节点一段时间听不到主的声音,就用一个更大的任期号发起选举,拿到多数票就当选。当选以后,主每要往日志里追加一条,还要再拉一次多数票;投票的节点只要见过更大的任期号,就投反对。两次「多数」必然至少有一个节点重叠,所以一个停顿后醒来、还以为自己是主的旧主,一定会在第二轮投票时撞上那个已经见过新任期的节点。
这一章点名的系统
| 英文 | 中文 | 一句话 |
|---|---|---|
| linearizability | 线性一致性 | 多副本表现得像只有一份数据,每个操作在某一刻原子地生效(§1)。 |
| register | 寄存器 | 理论里对「一个可读写的对象」的叫法,实际可以是一个键、一行、一个文档(§1.1)。 |
| strict serializability | 严格可串行化 | 可串行化加线性一致:事务排得出顺序,并且这个顺序符合真实时间(§1.1)。 |
| network partition | 网络分区 | 各部分内部通,彼此之间断开(§1.4)。 |
| CAP theorem | CAP 定理 | 网络分区时,在线性一致和可用之间只能选一个(§1.4)。 |
| logical clock | 逻辑时钟 | 数事件的算法,不报时,只能比先后(§2.1)。 |
| Lamport timestamp | Lamport 时间戳 | (计数, 节点 ID),给出和因果一致的全序(§2.1)。 |
| hybrid logical clock | 混合逻辑时钟 | 按物理时间走,又像 Lamport 时钟那样跟着别人往前拨(§2.1)。 |
| vector clock | 向量时钟 | 每个节点一格计数,能判断两个事件是否并发(§2.1)。 |
| consensus | 共识 | 让多个节点就一个值达成一致,决定了就不再改(§3)。 |
| FLP result | FLP 不可能性结论 | 节点可能崩溃时,没有确定性算法能保证共识总会结束(§3)。 |
| shared log / total order broadcast | 共享日志 / 全序广播 | 多个节点都能请求追加、所有节点按同一顺序读到的日志(§3.1)。 |
| consensus number | 共识数 | 一种原子操作最多能给几个节点解决共识(§3.1)。 |
| epoch number | 任期号 | 每次选主递增的编号,同一任期里最多一个主(§3.2)。 |
| coordination service | 协调服务 | ZooKeeper、etcd 一类,用共识存少量慢变的协调数据(§3.3)。 |
动手:模拟器
先猜:A 先做三件本地事件,再给 B 发一条消息;B 之前什么都没做,收到后马上给 C 发一条。B 这条消息带的计数是几?C 在收到它之前自己做了一件事,得到 (1,C)。(1,C) 比 A 发出的 (4,A) 小,C 这件事是不是发生在 A 发消息之前?猜好了再动手。
逻辑时钟
三个进程 A、B、C 做本地事件、互相发消息,消息要你手动送达。每个事件带一个 Lamport 时间戳(计数, 进程)。挑两个事件比一比:Lamport 怎么排,真实的因果关系又是什么。
- 重置。进程选 A,点三次「本地事件」;发给 B,点「发消息」;把这条消息送达。进程选 B,发给 C,点「发消息」,看它带的计数。进程选 C,点一次「本地事件」,再比较 C1 和 A4。
- 接着把 B 发给 C 的消息送达,让 C 再做一件本地事件,比较 A1 和 C2。然后取消「收到时把计数抬到对方的值」,重置后重复第一题的步骤,看「和因果相反的对」那个数字。
- 勾上「显示向量时钟」,点几次「随机走 4 步」,找一对计数不同、却是并发的事件。看它们的向量:为什么逐格比较比不出大小?
词与短语
词条按在书里出现的顺序排,按节分组;读一节之前先把这一节的过一遍,读的时候就不用停。斜体小字是原句里连续的几个词,在电子书里搜它就能找到那一句。标着「每个字都认识」的,是字面意思会把你带偏的。行话(quorum、linearizable 这类)不在这里,在上面的术语表里。
§0 Lots of things can go wrong…
§1 Linearizability
§1.1 What Makes a System Linearizable?
§1.2 Relying on Linearizability
§1.3 Implementing Linearizable Systems
§1.4 The Cost of Linearizability
§2 ID Generators and Logical Clocks
§2.1 Logical Clocks
§2.2 Linearizable ID Generators
§3 Consensus
§3.1 The Many Faces of Consensus
§3.2 Consensus in Practice
§3.3 Coordination Services
§4 Summary
下面是读完再看的:你的预测对不对、几道换成你来选的判断题、值得细读的句子和理解检查。先看了,读书时就只是在找答案。
读完:回到你的预测
书里的答案是第四项(§1.3)。写入是分别发往三个副本的,到达的时间有先有后。甲读的两个副本里有一个已经是 1,按时间戳取新的,读到 1;乙读的另外两个恰好都还没收到,读到 0。整个过程没有任何故障,法定人数的条件也满足,但「甲读完以后才开始的乙读到了更旧的值」,违反了线性一致。书里说要补救,读者在返回前得同步做读修复,写者在写之前也得先从法定人数个副本读到最新的时间戳;即使这样,CAS 也做不出来。想一想:如果每个读者都在返回之前把读到的最新值写回到两个副本上,乙还会读到 0 吗?
书里的答案是第三项(§2.1)。Lamport 时间戳和因果一致:如果 q 先于 p,q 的时间戳一定更小;现在 q 更大,所以 q 不可能先于 p。但反过来不成立,时间戳小不代表先发生,两件互不知情的事照样被排出先后。要分清「先于」和「并发」,得用向量时钟。上面的模拟器里,「计数不同却并发」的那几对就是例子。想一想:如果两个时间戳是 (4, A) 和 (4, C),能得出什么?
判断题:换成你来选
场景都是本站编的。先选,再看解释;解释里写了对应的节号,拿不准就回原书看那一节。
场景:一个分布式账务库宣称提供「可串行化」。运营在 10:00:05 收到了一笔调账事务提交成功的响应;10:00:07 另一个只读事务查这个账户的余额,看到的是调账之前的数。
可串行化只要求事务的效果等同于某一种串行顺序,这个顺序可以和真实时间不一致,把只读事务排在调账之前也合法,所以读到旧值不算违规。要求「先完成的操作一定被后开始的操作看到」的是线性一致的新近性保证,两者都要叫严格可串行化。写偏斜涉及两个事务基于旧前提各写一笔;脏读是读到未提交的数据,这里读到的是已提交的旧值。
场景:一款协作文档应用部署在东京和法兰克福两个机房,两地之间的线路每月会断几次,每次几分钟。断线时两地的用户都要能继续编辑,事后合并即可。
需求是断线时两边都能写,这正是不要线性一致的场合:多主复制让每个机房各自处理写入,恢复后再交换。单主和共识集群在断线时,联系不上主或者处在少数派一侧的机房就写不了(法兰克福那两个节点会停下)。无主加法定人数既不能保证线性一致,在断线、凑不够 w 个副本时也会拒绝写入。
场景:商品信息和价格存在两个分片上,各自用混合逻辑时钟给写入打时间戳。运营甲先把一件商品「下架」(分片 1),运营乙在聊天群里看到后,把它的价格改成内部测试价 0.01 元(分片 2)。结果有顾客用快照读看到这件商品仍在架上、价格 0.01 元,下了单。
分片 2 从没见过分片 1 的时间戳,它的时钟可能稍慢,于是后发生的改价拿到了更小的时间戳,快照恰好落在两次写之间。因果信息走的是聊天群,系统看不到,逻辑时钟管不着。线性一致的发号器保证「先完成的请求号更小」,不管两边有没有通信,书里 TiDB 的时间戳预言机就是这么做的。向量时钟能告诉你两次写并发,却不能把它们排对;调高 NTP 只能缩小、不能消除误差;快照时间戳选得再新,下一次也可能落在两次写之间。
场景:团队自己写了一个主备切换脚本:备库 3 秒收不到主库的心跳,就把自己提升为主。复制是异步的,没有用任何共识算法或协调服务。
只靠超时判断主死了,又没有任期号或防护令牌,停顿后醒来的旧主不知道自己已被替换,两个主同时接受写入,就是脑裂;异步复制下,旧主上已确认但还没复制过去的写也会丢。书里的说法是:做自动切换却没用经过验证的共识算法,多半不安全。超时长短是调参问题;心跳的带宽可以忽略;心跳超时只要在备库本机用单调时钟计时,就不依赖两台机器的时钟同步(第 9 章 §3.1)。
场景:一个对象存储系统有 300 台存储节点、2000 个分片,节点会加入、退出、宕机,需要决定每个分片由哪台节点负责。同事提议在全部 300 台节点上直接跑 Raft,让它们一起投票决定分配。
共识的每次操作都要和多数节点通信,节点越多越慢;书里说在几千个节点上跑共识效率极低,更好的做法是把共识「外包」给少数几台跑协调服务的机器。分片分配变化慢、数据量小,正适合协调服务,临时节点和变更通知正好用来发现节点的加入和消失。把多数降成三分之一,两个不重叠的少数派会各自做出决定;各自按超时接管则会出现两个节点同时认领一个分片。
和本站其他书对照
值得停下来的句子
Linearizable systems tend to be higher latency—and this is true all the time, not only during a network fault.— §1.4。本站译:线性一致的系统往往延迟更高,而且一直如此,不只是在网络出故障的时候。
CAP 让人以为线性一致的代价只在网络分区时才付,平时白拿。这句话把账算对了:每次读写都要确认自己看到的是最新的,就要等网络往返,而延迟的上限又说不准。多核 CPU 放弃线性一致、很多分布式数据库不提供它,理由都是性能。读到 §3.2 共识的每一步都要多数派点头时,可以回头再看这句。
Consensus is essentially “single-leader replication done right,” with automatic failover on leader failure— §3.2。本站译:共识本质上就是「做对了的单主复制」,主挂了能自动切换。
§3.1 把共识讲得很抽象,五种问题互相转化;这句话把它拉回读者熟悉的东西。第 6 章的单主复制最难的就是故障切换:脑裂、丢失已确认的写。共识算法没有发明新的复制方式,而是把这一步做到在第 9 章那些故障下也不出错。
检查一下理解
1.(§1.1)和写在时间上重叠的读,读到旧值或新值都可以。线性一致还额外要求了什么?
一旦有任何一个读返回了新值,此后开始的所有读,不管来自哪个客户端,都必须返回新值(直到它再被覆盖)。值不能在新旧之间来回跳,就像存在某一刻它原子地变了。
2.(§1.2)为什么硬性的唯一约束需要线性一致,外键约束却不需要?
两个人同时注册同一个用户名,必须由一个大家都认的最新值来判谁先占到,这和拿锁、做一次 CAS 是一回事。书里说外键、属性约束这类不需要线性一致也能实现。
3.(§1.3)用时钟时间戳做 LWW 的无主数据库,为什么几乎肯定不是线性一致的?
时钟有偏差,时间戳的大小和事件真实的先后可能对不上,后写的值可能因为时间戳更小被丢掉。所以即使用了法定人数,书里也建议默认它不提供线性一致。
4.(§1.4)书里为什么说用 CAP 解释多核 CPU 内存不线性一致是说不通的?
一台机器内部的通信被当作可靠的,一个核和其余部分断开后也不指望它还能正常工作,根本谈不上「分区时选可用」。放弃线性一致是为了性能:缓存比主存快得多。
5.(§2.1)Lamport 时间戳为什么要带上节点 ID?
两个节点可能产生相同的计数。带上节点 ID,计数相同时按节点 ID 比,每个时间戳都唯一,任意两个都能比出大小,构成一个全序。
6.(§2.2)有了线性一致的时间戳,「时间戳最小的赢」为什么仍然做不成容错的锁?
一个节点要确认自己的时间戳最小,必须听到所有可能参与竞争的节点的回音。只要有一个节点挂了或联系不上,就没法确定它的时间戳不更小,系统只能停着等。
7.(§3.1)用 fetch-and-add 做共识,拿到 0 的节点赢。问题出在哪?为什么两个节点时又可以?
其他节点只知道自己输了,不知道谁赢了;赢家如果在通知大家之前挂了,其余节点无法决定,也不能改选别人,因为赢家可能回来。只有两个节点时,可以先交换各自的提议,拿到 1 的那个就知道赢的是对方。
8.(§3.2)共识算法里,选主和追加日志的两次投票,为什么要求法定人数重叠?
重叠保证追加日志的投票里至少有一个节点参加过最近一次成功的选举。如果出现了任期更大的主,旧主拉票时一定会从这个节点那里发现,追加就不会成功。
用一句话说这一章(写给一个没读过的朋友):
精彩回顾
一句话:共识让一组会坏的机器像一台不会坏的单主机器那样做决定,代价是每一步都要多数节点点头。
和写重叠的读新旧皆可;但只要有人读到了新值,之后开始的读都只能是新值。线性一致是新近性保证,只管单个对象。
§1.1w + r > n,又没有任何故障,和写重叠的两次读仍可能一新一旧。要补救得同步读修复,CAS 仍然做不到。
§1.3放弃线性一致多半是为了快,不是为了容错。它的响应时间至少和网络延迟的不确定性成正比,不只在断网时慢。
§1.4Lamport 时间戳只保证「先发生的更小」。两件互不知情的事也会被随手排个先后,要分出并发得用向量时钟。
§2.1两个库各用逻辑时钟,彼此没见过对方的时间戳,后发生的写可能拿到更小的号。线性一致的发号器才能堵住。
§2.2fetch-and-add 里拿到 0 的赢,其余的只知道自己输了。赢家没来得及通知就挂,大家既不能决定,也不能改选。
§3.1共识不保证任何时刻只有一个主,只保证同一任期里最多一个。追加日志前再拉一次多数票,过期的主就会被发现。
§3.2几千个分片不必都跑共识,把慢变的分配信息交给三到五台机器上的协调服务。服务发现通常连线性一致都不需要。
§3.3