这一章在做什么
复制就是在几台联网的机器上各放一份相同的数据。数据不变时这很容易,难全在改动上:一次写先落到一台机器,别的副本迟早跟上,没跟上的这段时间里读的人会看到什么,机器坏了又会丢掉什么。这一章讲三种做法:所有写只进一个主库(单主),几个主库都能写(多主),读写都同时发给好几个副本(无主)。取舍落在一条线上:越不依赖单个节点,越扛得住故障和断网,代价是要你来解决冲突,一致性也更弱。
读之前:先预测
第 4 章讲一台机器上的存储引擎,这一章开始把数据放到好几台机器上,每个副本里跑的仍是那些引擎,主库甚至可以直接把预写日志发给从库。后面几章都从这里接着走:第 7 章去掉「每台机器都装得下全部数据」这个假设,讲分片;第 9 章细说这一章反复碰到的不可靠的网络和时钟;第 10 章的共识,正是自动选出新主库要解决的问题;第 12 章的变更数据捕获,用的就是这里的逻辑日志。
写总要先落到某一台机器上,别的副本总要慢一步。这一步的空档里,读的人会看到什么、机器坏了会丢掉什么?三种复制方式把「谁能收写」放在不同的地方,各自付出的代价是什么?
- 异步复制的主库坏了,换一个从库当主库,已经告诉用户「成功」的写为什么可能就此消失?§1.1§1.3
- 复制延迟会造成哪三种怪象?每一种要靠哪一种保证挡住?§1.5
- 两个主库同时改了同一条记录,有哪几种收场的办法,各自丢掉了什么?§2.3
- 无主复制里 w + r > n 为什么通常能读到最新值,又在哪些情况下不灵?§3.1
这一章的骨架
这一章按「谁能收写」分成三大块:单主(§1)、多主(§2)、无主(§3),后两块都比第一块短,因为很多问题前面已经铺好了。容易乱的是两处:§1 里同步与异步、故障切换、日志格式、复制延迟一路排下来,前四节都是在为 §1.5 做铺垫;§2.3 的冲突解决和 §3.4 的并发检测是同一个问题的两半,隔着一大块无主复制,读到 §3.4 时要回头把它们接上。
边读边填:每行一个问题,读到答案就写一两句。只存在你这台设备的浏览器里,别人看不到。
| 要找的 | 带着这个问题读,找到了就写下来 |
|---|---|
| 半同步 | 「半同步」到底同步了几个从库?为什么不把所有从库都设成同步?§1.1 |
| 故障切换 | 自动故障切换分哪三步?至少找出三处会出事的地方。§1.3 |
| 日志格式 | 三种复制日志各卡在哪里?哪一种允许主从跑不同版本的数据库?§1.4 |
| 读己之写 | 书里给了哪几种实现办法?用户换着设备用时又多出哪两个麻烦?§1.5 |
| 最后写入者胜 | 这个名字为什么会误导人?什么情况下用它没问题?§2.3 |
| 法定人数的边界 | w + r > n 也可能读到旧值,书里列了哪几种情形?§3.1 |
| 版本向量 | 只有一个副本时一个版本号就够,为什么多个副本时不够?§3.4 |
读完了?对照我找到的
- 半同步:一个从库同步、其余异步,它慢了就换一个异步的顶上;全部同步的话,任何一个节点出事,写就全停。
- 故障切换:判定主库死亡、选新主库、让大家改认新主库;会出事的有:异步时丢写、旧主库回来还当自己是主、脑裂、超时设得不对。
- 日志格式:语句会遇到不确定的函数和执行顺序;预写日志太底层,和存储格式绑死;逻辑日志与引擎解耦,能跨版本。
- 读己之写:可能改过的从主库读;按最近一次写的时刻决定读主库;记住自己写的位置,只读追上了的副本。多设备时位置要集中保存,各设备还可能被路由到不同区域。
- 最后写入者胜:并发写没有先后,所谓「最后」其实是随机挑一个,其余悄悄丢掉;只插入、键不重复、从不更新时没问题。
- 法定人数的边界:新值的节点坏了、从旧副本恢复;再平衡途中;读和写并发;写在部分节点成功但总数不足 w;时钟不准;并发写。
- 版本向量:多个副本同时各自收写,一个计数器分不清谁见过谁,要每个副本一个版本号。
分节地图
全章约 1.8 万词(不算图注),建议分五次读。第二次的 §1.5 和第三次的 §2.3 是全章的重心;第四、五次讲无主复制,§3.4 的版本号例子要拿笔跟着算一遍。
- 第一次坐下§0–§1.3 · 约 3700 词 · 单主复制怎么搭起来,节点坏了怎么办
- Replication means keeping a copy…约 600 词开篇复制的三个理由,三种做法的总览。方框讲复制和备份为什么不能互相替代。
- Single-Leader Replication约 400 词单主复制主库收所有写,从库按同样顺序重放复制日志。哪些系统是这么做的。
- Synchronous Versus Asynchronous Replication约 600 词同步复制与异步复制主库等不等从库确认:各自换来什么、丢掉什么,半同步怎么折中。
- Setting Up New Followers约 900 词添加新的从库不停机加从库靠快照加日志位置。方框讲把数据库放在对象存储上的利弊。
- Handling Node Outages约 1300 词处理节点宕机从库挂了怎么追上;主库挂了的故障切换分三步,留意每一步能出的事。
- 第二次坐下§1.4–§1.6 · 约 3300 词 · 复制日志的格式,复制延迟的三种怪象
- Implementation of Replication Logs约 1000 词复制日志的实现语句、预写日志、逻辑日志三种格式,看它们和存储引擎绑得多紧。
- Problems with Replication Lag约 2000 词复制延迟带来的问题读扩展只能配异步复制,于是有了三种怪象和对应的三种保证。最该细读的一节。
- Solutions for Replication Lag约 300 词复制延迟的解决办法在应用里补,还是换一个强一致的数据库?NoSQL 和 NewSQL 的来回。
- 第三次坐下§2–§2.3 · 约 5100 词 · 多主复制和写冲突
- Multi-Leader Replication约 300 词多主复制让不止一个节点收写。同步的多主等于单主,所以只讨论异步的。
- Geographically Distributed Operation约 1200 词跨地域运行每个区域一个主库,从四个方面和单主对比;再看几种复制拓扑各自的毛病。
- Sync Engines and Local-First Software约 1100 词同步引擎与本地优先软件离线可用和实时协作的应用,本质上都是多主复制。同步引擎的好处和适用范围。
- Dealing with Conflicting Writes约 2500 词处理写冲突避免冲突、最后写入者胜、手动解决、自动合并。注意每种办法丢掉的是什么。
- 第四次坐下§3–§3.2 · 约 3000 词 · 无主复制和法定人数
- Leaderless Replication约 300 词无主复制不要主库,任何副本都能直接收写。Dynamo 风格这个叫法的来历。
- Writing to the Database When a Node Is Down约 2000 词有节点宕机时怎么写读写都发给多个副本:漏掉的写怎么补,w + r > n 保证什么、不保证什么。
- Single-Leader Versus Leaderless Replication Performance约 700 词单主与无主的性能对比无主为什么更扛慢节点和灰色故障,它自己又有哪些性能问题。
- 第五次坐下§3.3–§4 · 约 2800 词 · 跨区域、检测并发写,加小结
- Multi-Region Operation约 200 词跨区域运行无主数据库跨区域时怎么转发写、怎么选一致性级别。
- Detecting Concurrent Writes约 1900 词检测并发写什么叫「并发」;用版本号分出覆盖和并发,推广到多副本就是版本向量。
- Summary约 600 词小结全章回顾。先自己回忆一遍再读它。
先用中文过一遍
单主复制:写只进主库,从库照着日志重放
选一个副本当主库,客户端的写都发给它。主库每改一次数据,就把改动记进复制日志发给各个从库,从库按同样的顺序重放一遍;读可以落到任何一个副本上。好比一个班只有一个人记账,其他人照着这份流水抄。
关键的选择是同步还是异步。主库等某个从库确认收到了,才告诉客户端「成功」,这是同步:这个从库一定和主库一样新,可它一卡住,所有写都得停下等它。不等就是异步:写永远不会被从库拖慢,代价是主库如果在改动发出去之前坏掉,客户端已经收到「成功」的写就没了。实际系统常常折中,一个从库同步,其余异步。
主库坏了要故障切换(failover):先判断它真的死了(通常靠超时),再选一个数据最新的从库接任,最后让客户端和其他从库都改认新主库。每一步都可能出事:超时设长了,恢复得慢;设短了,负载一高就误判。旧主库回来以后如果还以为自己是主库,两个主库同时收写,就是脑裂(split brain)。
复制延迟:刚写完读不到,刷新又倒回去
大多数线上服务读多写少,于是有一种很诱人的做法:多加几个从库分摊读请求,叫读扩展。从库一多,就只能用异步复制,否则任何一个从库出问题,写就全停了。异步就有延迟:平时不到一秒,系统负载高或者网络出问题时,可能是几秒甚至几分钟。只要停止写入、等一会儿,从库终究会追上,这就是最终一致性(eventual consistency);「终究」是多久,没有上限。
延迟大了,用户会碰到三种怪象。自己刚改的东西刷新后不见了,要的保证叫读己之写(read-after-write);刷新一次,看到的比上一次还旧,像时间倒流,要的保证叫单调读(monotonic reads);有先后关系的两次写,读的人看到的顺序反了,比如先看到回复、后看到提问,要的保证叫一致前缀读(consistent prefix reads)。前两种在下面的模拟器里能直接看到。办法本身不复杂,难在应用代码里处处都得想着它。
多主:每个区域都能写,代价是冲突
单主的毛病是所有写都得经过那一个主库。数据库部署在好几个区域(几个地理上分开的数据中心)时,离主库远的用户每次写都要跨洋走一趟;连不上主库,就根本写不了。多主让每个区域都有自己的主库,写在本地完成,再异步同步给其他区域。手机上离线可用的应用、几个人同时编辑一份文档,其实也是多主:每台设备、每个浏览器标签页都是一个能收写的副本,只是网络差到了极点。
代价是冲突。两个主库同时改了同一条记录,各自都说「成功」,同步时才发现对不上。收场的办法大致有四种:从源头上避免(同一条记录只让一个主库写);比时间戳,留大的,丢掉另一个(最后写入者胜,LWW);把几个值都留下,交给应用或用户去挑;按数据类型自动合并,文本、列表、计数器都有专门的算法(CRDT 和 OT)。还有一类冲突更隐蔽:两次写各自没问题,合在一起才违反约束,比如演出的同一个座位被两个区域各卖出去一次。
无主:同时写好几个,同时读好几个
无主复制干脆不要主库:客户端(或替它跑腿的协调节点)把写同时发给 n 个副本,等到 w 个确认就算成功;读也同时问 n 个,等 r 个回复,取版本号最大的那个值。只要 w + r > n,写过的那几个副本和读到的那几个副本至少有一个重叠,读就能拿到最新值。有副本宕机也不用故障切换,只要活着的还凑得够 w 和 r。
宕机期间漏掉的写怎么补?读的时候发现某个副本的值旧了,顺手把新值写回去(读修复);别的副本先替它收着,等它回来再转交(提示移交);后台进程定期比对副本之间的差异(反熵)。书里接着列了一串「凑够了法定人数也可能读到旧值」的情形,这一段别跳过。
这一章点名的系统
| 英文 | 中文 | 一句话 |
|---|---|---|
| leader / follower | 主库 / 从库 | 主库收所有写,从库按主库的顺序重放;也叫 primary / replica 等(§1)。 |
| replication log | 复制日志 | 主库发给从库的改动流,从库靠它追平(§1)。 |
| synchronous / asynchronous replication | 同步 / 异步复制 | 主库等不等从库确认再报成功;一个同步、其余异步叫半同步(§1.1)。 |
| failover | 故障切换 | 主库坏了,把一个从库提升为新主库,并让所有人改认它(§1.3)。 |
| split brain | 脑裂 | 两个节点都以为自己是主库,同时收写(§1.3)。 |
| logical (row-based) log | 逻辑日志(按行) | 按行记录增删改,和存储引擎的内部格式解耦(§1.4)。 |
| replication lag | 复制延迟 | 一次写在主库生效到在从库上可见之间的时间差(§1.5)。 |
| eventual consistency | 最终一致性 | 停止写入、等足够久,各副本终究会一致;多久没有上限(§1.5)。 |
| read-after-write consistency | 读己之写一致性 | 用户总能读到自己提交过的改动;对别人的改动不作承诺(§1.5)。 |
| monotonic reads | 单调读 | 同一个用户连续读,不会先读到新数据、再读到更旧的(§1.5)。 |
| consistent prefix reads | 一致前缀读 | 有先后的几次写,任何人读到的顺序都和写的顺序一样(§1.5)。 |
| last write wins (LWW) | 最后写入者胜 | 冲突时留时间戳最大的值,其余丢掉(§2.3)。 |
| CRDT | 无冲突复制数据类型 | 能把并发修改自动合并、让各副本收敛到同一状态的数据结构(§2.3)。 |
| quorum | 法定人数 | 读写各要多少个副本回应才算数;w + r > n 时读写集合重叠(§3.1)。 |
| version vector | 版本向量 | 每个副本一个版本号,合起来判断两次写是先后还是并发(§3.4)。 |
动手:模拟器
先猜:从库 B 落后 4 秒,读请求在 B 和 A 之间轮流(先 B 后 A)。你上传一个文件,接着连刷新 4 次,有几次看不到它?哪一次会比上一次看到的还少?勾上「读己之写」以后,同事上传的文件还会不会忽隐忽现?猜好了再动手。
复制延迟:读到旧数据
一个共享文件夹:一个主库,两个从库。上传写进主库,刷新从从库读。每个操作算 1 秒。先点「我上传一个文件」,再连点几次「刷新文件夹」。
- 默认设置下,上传一个文件,连刷新 4 次。哪几次看不到自己的文件?哪一次「时间倒流」?把 B 的延迟调到 1 秒,重置后再做一遍。
- 重置,勾上「读己之写」,重做第一步:「改读主库」记了几次?再点「同事上传一个文件」,连刷新 3 次,同事的文件为什么还会忽隐忽现?
- 重置,只勾「单调读」(这里按用户固定读从库 B),上传后连刷新几次:还会倒流吗?自己的文件多久才出现?再把「读己之写」也勾上,看看这次读付出的代价在哪一格。
词与短语
词条按在书里出现的顺序排,按节分组;读一节之前先把这一节的过一遍,读的时候就不用停。斜体小字是原句里连续的几个词,在电子书里搜它就能找到那一句。标着「每个字都认识」的,是字面意思会把你带偏的。行话(quorum、failover 这类)不在这里,在上面的术语表里。
§0 Replication means keeping a copy…
§1 Single-Leader Replication
§1.1 Synchronous Versus Asynchronous Replication
§1.2 Setting Up New Followers
§1.3 Handling Node Outages
§1.4 Implementation of Replication Logs
§1.5 Problems with Replication Lag
§1.6 Solutions for Replication Lag
§2.1 Geographically Distributed Operation
§2.2 Sync Engines and Local-First Software
§2.3 Dealing with Conflicting Writes
§3 Leaderless Replication
§3.1 Writing to the Database When a Node Is Down
§3.2 Single-Leader Versus Leaderless Replication Performance
§3.4 Detecting Concurrent Writes
§4 Summary
下面是读完再看的:你的预测对不对、几道换成你来选的判断题、值得细读的句子和理解检查。先看了,读书时就只是在找答案。
读完:回到你的预测
答案是第三项(§1.5)。主库的「成功」只说明主库写好了,异步的从库什么时候跟上没有保证。读落到 A 时看得到,落到 B 时 B 还没收到这次写,文件就没了,下一次轮回 A 又出现。这里同时破坏了两个保证:刚上传就读不到,违反读己之写;看到过又消失,违反单调读。第二项是把两个从库想成一样慢了,只要读在副本之间换来换去,就会忽隐忽现。第四项说的是应用自己在客户端缓存,书里讨论的是数据库返回什么。想一想:如果把读请求固定发给 B,还会倒流吗?刚上传的文件要多久才出现?
答案是第四项(§2.3)。最后写入者胜给每次写打一个时间戳,同步时只留大的,另一个直接丢掉,不报错,不回滚,两个管理员都以为自己保存成功了。两次修改是并发的,谁「后」本来就没有定义,时间戳的先后基本是随机的;如果某个区域的时钟走得快,真正后保存的那次反而可能输(§2.3 讲时钟,第 9 章细说)。第一项是「保留兄弟值」的做法,不是 LWW;第五项是什么都不做的后果,LWW 恰恰能让各副本收敛到同一个值。想一想:如果这个字段换成「库存数量」,两边各扣了 3 件,LWW 会算出什么?换成自动合并的计数器呢?
判断题:换成你来选
场景都是本站编的。先选,再看解释;解释里写了对应的节号,拿不准就回原书看那一节。
场景:客服后台的读请求随机分到三个异步从库。一位客服盯着一张工单,刷新时状态一会儿是「已处理」,一会儿又变回「处理中」。工单是别的同事处理的,不是这位客服自己改的。
先看到新状态、再看到旧状态,是「时间倒流」,要的是单调读;书里的办法是让同一个用户总从同一个副本读,比如按用户 ID 的哈希选。读己之写只管自己写过的东西,这张工单不是这位客服改的。一致前缀读说的是有因果关系的几次写顺序颠倒,这里只有一条记录的新旧。脑裂会写坏数据,不会让同一条记录在新旧之间来回跳。
场景:一个给野外勘探队用的标注应用,几个人在同一张地图上画线、加标记。山里经常一连几小时没有信号,回到营地联网后要把各人的改动合在一起,谁的标注都不能丢。
离线也要能写,就必须让每台设备都能收写,这是多主,具体形态就是 §2.2 讲的同步引擎;谁的标注都不能丢,冲突就得自动合并而不是丢弃,CRDT 正是为此设计的。单主和 w = n 的无主都要求联网才能写。LWW 能收敛,但会悄悄丢掉并发的改动。
场景:一个订单库用单主复制,团队想让故障恢复更快,把「多久没响应就判定主库死亡」从 30 秒改成 3 秒。每逢大促,主库负载很高,响应时间偶尔会冲到 4、5 秒。
超时只看「多久没回应」,分不清主库是死了还是忙。负载尖峰把响应拖过 3 秒,系统就会认定主库死亡并切换;书里特别提醒,系统本来就在高负载下挣扎时,一次多余的切换只会让情况更糟。脑裂是可能的后果之一,但不是必然。
场景:一个 Dynamo 风格的数据库,n = 3,为了快把 w 和 r 都设成了 1。现在用户偶尔会读到几秒前的旧值。
w + r = 2,不大于 n = 3,写过的副本和读到的副本可能完全不重叠。改成 w = r = 2 后 w + r > n,读写集合至少重叠一个副本,代价是每次要多等一个副本,能容忍的宕机节点也少了。加副本而 w、r 不变,重叠只会更难;关掉读修复让旧值留得更久;时间戳只决定读到几个值时选哪个,没读到新值就选不出来。
场景:一个 24 小时都有人下单的服务,数据库要从一个大版本升到下一个,新版本改了磁盘上的存储格式。团队希望先升级从库,再切换主库,全程不停机。
先升级从库、再切换,要求主库是旧版本、从库是新版本时复制还能跑。逻辑日志和存储引擎的内部格式解耦,容易保持向后兼容。预写日志记的是哪个磁盘块的哪几个字节变了,存储格式一变,新旧版本就对不上。基于语句的复制有不确定函数、执行顺序的老问题,书里说很多数据库更愿意用别的方式。关掉复制就谈不上不停机切换了。
和本站其他书对照
值得停下来的句子
one of those writes is randomly chosen to be the winner and the other writes are silently discarded— §2.3。本站译:这几次写里随机挑一个当胜者,其余的悄悄丢掉。
这是作者对「最后写入者胜」的重新定义。名字让人以为系统在尊重时间顺序,可对并发的写来说根本不存在谁先谁后,时间戳只是把随机性藏了起来。值得停下来,是因为 Cassandra、ScyllaDB 这类数据库用的就是这种办法(§3.4),用它之前要先问自己:我的应用丢得起这些写吗?
the resilience of a leaderless system comes from the fact that it doesn’t distinguish between the normal case and the failure case.— §3.2。本站译:无主系统的韧性,来自它不区分正常情况和故障情况。
单主系统平时走一条路,出了故障走另一条路(故障切换),而切换前要先判断「够不够坏」,这个判断本身就会出错。无主系统每次都同时问好几个副本、用最先到的回复,慢一个、坏一个都在日常流程里消化掉了。这句话解释了为什么无主复制特别扛「灰色故障」,也说明它的韧性和它的弱一致性来自同一个设计。
检查一下理解
1.(§0 方框)已经有三个副本了,为什么还要备份?
副本会尽快反映每一次写,包括误删。删除同样会被复制到所有副本上,复制救不回来;备份存的是过去某个时刻的快照,才能回到误删之前。
2.(§1.1)为什么不能把所有从库都设成同步?
同步意味着主库要等每个从库确认才算写成功。从库越多,任一个宕机或网络出问题的可能越大,而任何一个卡住,所有写都得停。
3.(§1.2)给正在运行的主库加一个新从库,为什么不能直接拷数据文件?
拷贝期间数据一直在变,拷出来的各部分来自不同时刻,拼不成一致的状态。要先做一个一致的快照,记下它在复制日志里的位置,新从库装好快照后从这个位置往后追。
4.(§1.3)一个从库掉线很久,主库为什么左右为难?
主库要么一直留着这个从库还没确认的日志,冒磁盘写满的风险;要么删掉,那个从库回来时就没法靠日志追,只能从备份重建。
5.(§1.4)基于语句的复制在哪几种情况下会让副本不一致?
语句里有取当前时间、随机数这类不确定的函数;语句依赖自增列或已有数据,而各副本的执行顺序不同;触发器、存储过程这类有副作用的语句,副作用不确定。
6.(§1.5)一致前缀读为什么主要是分片数据库的问题?
如果所有写都按同一个顺序应用,读到的总是某个一致的前缀。分片之后各分片独立处理写,没有全局顺序,有因果关系的两次写落在不同分片上,就可能一个先被读到、一个后被读到。
7.(§3.1)已经有读修复了,为什么还需要提示移交和反熵?
读修复只在读的时候发生,很少被读的值可能一直是旧的。提示移交让别的副本先替宕机的副本收着写,等它回来再转交;反熵在后台比对副本差异,补上剩下的漏洞,只是不保证顺序,可能很慢。
8.(§3.4)服务器只看版本号,怎么知道一次写是覆盖还是并发?
客户端写的时候带上它上次读到的版本号,表示「我是在这个版本的基础上改的」。版本号不超过它的值都已经被合并进新值,可以覆盖;比它大的是客户端没见过的,属于并发写,要保留成兄弟值。
用一句话说这一章(写给一个没读过的朋友):
精彩回顾
一句话:复制的难处全在改动:单主让读的人看到延迟,多主和无主让写的人碰上冲突,没有哪一种能两头都躲开。
误删一行,删除会被原样复制到每个副本。想回到过去,得靠存在别处的旧快照。
§0异步复制下主库突然坏掉,新主库可能还没收到最后几次写;旧主库回来后,这些写通常直接扔掉。
§1.3判定主库死亡的超时设长了,恢复慢;设短了,负载一高就误切换,切换本身又让局面更糟。
§1.3两次读落到落后程度不同的从库,第二次比第一次还旧。让同一个用户固定读一个从库就能挡住。
§1.5最后写入者胜,其实是在并发写里随机挑一个留下,其余悄悄丢掉;时钟一偏,真正后写的反而输。
§2.3无主系统本来就同时问好几个副本,慢一个就用别人的回复,从来不用判断要不要故障切换。
§3.2写的时候带上读到的版本号,服务器就知道这次写覆盖了谁、和谁并发,用不着看值本身。
§3.4