摘要:本文尝试对Mongo的复制和分布式事务的原理进行形容,在必要的中央,对实现的正确性进行论证,心愿能为MongoDB内核爱好者提供一些参考。
1.前言
- MongoDB基于wiredTiger提供的泛化SI的性能,重构了readHistory(readMajority)的能力
- 基于wiredTiger提供的AllCommittedTimestamp API,重构了前缀统一的主从复制(Prefix-Consistent-Replication)
- 引入混合逻辑时钟(HLC),每个节点(Mongos/Mongod)的逻辑时钟维持在靠近的值,基于此实现ChangeStream, 联合HLC与CLOCK-SI,实现分布式事务,HLC和泛化SI,CLOCK-SI两篇Paper能够作为了解MongoDB的设计的实践参考(这里并没有说MongoDB是Paper的实现)。
本文尝试对Mongo的复制和分布式事务的原理进行形容,在必要的中央,对实现的正确性进行论证,心愿能为MongoDB内核爱好者提供一些参考。
2.MongoDB正本集事务介绍
- MongoDB 正本集的事务
- MongoDB正本集的复制是基于raft协定,相比于Paxos,raft协定实现简略,然而raft协定只反对single-master,对应的,MongoDB的正本集是主从架构,而且只有主节点反对写入操作。MongoDB正本集的事务管理,包含冲突检测,事务提交等要害操作,都只在主节点上实现。也就是说正本集的事务在事务管理方面,跟单节点逻辑基本一致。
- MongoDB的事务,依然是实现了 ACID 四个个性, MongoDB应用 SI 作为事务的隔离级别。
3.SI的简介
- SI,即SnapshotIsolation,中文称为快照隔离,是一种mvcc的实现机制,它在1995年的A Critique of ANSI SQL Isolation Levels中被正式提出。因快照工夫点的选取上的不同,又分为Conventional Si 和 Generalized SI。
CSI(Convensional SI)
- CSI 选取以后最新的零碎快照作为事务的读取快照
- 就是在事务开始的时候,取得以后db最新的snapshot,作为事务的读取的snapshot,
- snapshot(Ti) = start(Ti)
- 能够缩小写事务抵触产生的概率,并且提供读事务读取最新数据的能力
- 个别咱们说一个数据库反对SI隔离级别,其实默认是说反对CSI。比方RocksDB反对的SI就是CSI,WiredTiger在3.0版本之前反对的SI也是CSI。
GSI(Generalized SI)
- GSI抉择历史上的数据库快照作为事务的读取快照,因而CSI能够看作GSI的一个特例。
- 在复制集的状况下,思考 CSI, 对于主节点上的事务,每次事务的开始工夫选取的零碎 最新的 快照, 然而对于其余从节点来说, 并没有 对立的 “最新的” 快照这个概念。泛化的快照实际上是基于快照观测失去的,对于以后事务来说,咱们通过选取适合的 更早工夫的快照,能够让 从节点上的事务正确且无提早的执行。
- 举例如下:
- 例如以后数据库的状态是, S={T1, T2, T3}, 当初要开始执行T4,
- 如果咱们晓得T4要批改的值,在T3上没有被批改, 那么咱们在执行T4的时候, 就能够依照 T2 commit后的snashot进行读取。
- 如何抉择更早的工夫点,须要满足上面的规定,
- 符号定义
- Ti: 事务i
- Xi: 被事务i批改过的X变量
- snapshot(Ti): 事务i的选取的快照工夫
- start(Ti): 事务i的开始工夫
- commit(Ti): 事务i的提交工夫
- abort(Ti): the time when Ti is aborted.
- end(Ti): the time when Ti is committed or aborted.
公式解释
读规定
- G1.1, 如果变量X被本事务批改了值且读取到了新的值, 那么 读操作肯定在写操作前面;
- G1.2, 如果事务i读取了事务j更新的变量的X, 那么肯定不会有事务i更新X的操作,在事务i读取了事务j更新的变量的X这个操作后面;
- G1.3, 事务j的提交工夫早于事务i的快照工夫;
- G1.4, 对于任意一个会更新变量X的事务k, 那么这个事务k肯定满足, 要么事务k的提交工夫小于事务j, 要么这个事务k的提交工夫大于事务i。
写规定
- G2, 对于任意曾经在提交历史里的两个事务,Ci, Cj, 那么肯定能够保障当 事务j的commit工夫戳在 事务i的观测时间段内时(snapshot(Ti), commit(Ti)), 那么他们更新的变量交加肯定为空。
PCSI(PREFIX-CONSISTENT SNAPSHOT ISOLATION SI)
- GSI 只是定义了一个范畴的range,都能够作为SI应用,并没有定义具体应该抉择哪个SI。
- PCSI 是为了复制集而设计的。对于一个事务Ti 要开S节点开始运行, 那么 S节点将必须蕴含这个事务所须要的所有前置事务都必须运行且提交。
- 相比拟于GSI, PCSI的读规定,额定减少了 P1.5 规定。
- SI的提交工夫戳设置,根据 A Critique of ANSI SQL Isolation Levels 中的形容, 提交工夫戳的设置应该是枯燥递增的。新设置的工夫戳,应该大于零碎中曾经存在的开始工夫戳和提交工夫戳。
- SI 读取工夫戳的设置,必须保障比以后零碎中正在运行的事务的最小的提交工夫戳还要小, 因为一旦大于以后零碎中正在运行事务的最小的提交工夫戳,那么这个读事务读取到的数据就是未定义的, 取决于读事务启动的工夫,而不是snapshot的工夫,这违反了 一致性的要求。举例如下
- 以后曾经实现的事务是T1,正在运行的事务是T2, 将要运行的读事务是T3, 如果 T3的读工夫戳大于T2事务提交工夫戳, 并且T2事务正在运行,等到T2事务执行完后。咱们察看这个 database,就会发现 他违反了GSI,
事务执行程序如下所示是:
T1 commited and commitTs(1) -> T2 start -> T2 set commitTs(2) -> T3 start -> T3 set snapshotTs(3) -> T3 commit -> pointA -> T2 commit -> pointB
那么可知, T3事务理论读取的值是 T1事务的值。但依据 pointB 点来看 GSI的读规定 1.4 的要求,会发现, 如果T3读到T1的事务的批改,那么必然要求, T3和T1之间没有空洞。但实际上 T2 是落在了 T3和T1之间的, 也就是说, 违反了 GSI 1.4的读规定。
- 所以咱们必须规定, SI 读取工夫戳的设置,必须保障比以后零碎中正在运行的事务的最小的提交工夫戳还要小。
4.MongoDB正本集工夫戳利用
MongoDB 4.0的复制也是利用工夫戳个性解决了3.x系列MongoDB从节点复制造成从节点性能降落的要害计划。
- MongoDB oplog 乱序问题
- MongoDB主备节点的数据同步并不基于WiredTiger的wal日志来做的。相同,mongodb会将每次操作的数据变更写入到一个叫做oplog的汇合里。
- oplog这个汇合,尽管名字带有log,但实际上,它是一个MongoDB的表, 对oplog的写入,并不是 append的形式批改的, 而是呈现出一种尾部乱序的形式。
- 对于oplog来说, oplog的读取程序是依照TS字段来排序的, 跟下层的提交程序无关。所以存在后开始的事务,在oplog先读取的场景。
- oplog 空洞
- 因为呈现了乱序,所以从节点在读取oplog的时候,就会在某些工夫点呈现空洞。举例如下:
- 工夫点1: oplog 程序为: Ta -> Tb, 此时零碎中还有一个事务Tc在运行
- 工夫点2: oplog 程序为: Ta -> Tc -> Tb, 当Tc运行完结后, 因为ts的程序, 看起来是将Tc插入到了Ta和Tb之间。
- 那么当 从节点 在工夫点1 reply 到 Tb的时候, 实际上是漏了 Tc的,这个就是oplog的空洞, 他产生的起因是因为,从节点如果每次读取oplog最新的数据,就有可能会失去一个不间断的数据, 例如 工夫点1上 Ta-> Tb. 这就是oplog空洞。
- 在具体复制逻辑中,咱们必须想方法来从节点读取到含有空洞的oplog数据。这也是GSI的要求, snapshot的选取不能含有空洞。
- 因为 oplog的Ts是mongo下层给的,咱们很容易晓得哪些事务有哪些ts, 咱们再将这个ts 作为事务的commitTs 放到 oplog存储的事务里, 这样咱们读取 oplog的程序事务的可见性程序相一致了,在这种状况下,咱们就能够 依据 沉闷事务列表, 就能够将oplog 分为两个局部,
- 假如沉闷commitTs列表的事务是 {T10, T11, T12}, 沉闷事务列表是 {T10, T11, T12, T13, T14}, 那么意味着, 目前有 T10, T11, T12, T13, T14 再运行,并且 T10, T11, T12 曾经设置了 commitTs, 又因为 下面探讨的 commitTs 是枯燥递增的, 那么咱们可知, T13, T14 的commitTs 肯定大于 maxCommitTs(T10, T11, T12), 而且咱们还可知, minCommitTs(T10,T11,T12) 就是全局最小的 commitTs, 而小于这些的 commitTs的事务,因为不在 沉闷事务列表里了, 示意曾经提交了, 那么咱们能够晓得, oplog ts 在 全局最小的 commitTs 之前的, 就是都提交了的, oplog 依照 commitTs 排序后,如下所示
… Tx | minCommitTs(T10,T11,T12) | …
咱们能够晓得 T9, 或者说小于 minCommitTs(T10,T11,T12) 都是无空洞,因为零碎不会再提交小于 minCommitTs(T10,T11,T12) 的事务到oplog里了, 所以从节点能够间接复原这里的数据。
- 下面说的oplog minCommitTs(T10,T11,T12) 在 mongodb里,就是非凡的timestamp, 这个后文会讲。
- 通过下面的计划,咱们能够解决空洞的问题。这个时候,从节点每次复原数据的时候,将读取的snapshot,设置为上一次复原的Ts(同样也是无空洞的Ts), 这样的话, 从节点的复原数据和读取数据也就做到了互不抵触。从而解决了 3.x系列的 从节点同步数据造成节点性能降落的问题。
点击关注,第一工夫理解华为云陈腐技术~