这次直播分享是基于 MongoDB 之前发布的一篇论文,叫做 Tunable Consistency in MongoDB ,我们尝试用简单易懂的语言来表达其中的一些技术要点。欢迎大家去阅读一下英文的原文,相信大家可以学到更多有用的知识。
可调一致性产生的背景
通常讲到一个数据库系统的时候,在单机系统都很容易达到强一致的状态。但是在分布式的系统中会产生各种各样的情况。所以为了应对这些不同的情况,需要能够通过不同的配置来实现更合理的应对方式。
- 第一种情况弱一致:数据会先到达一个节点,再到达其他节点,这样会导致每一个节点的数据新旧程度不一样。
- 第二个情况部分失效:可能会有部分节点失效在一定的时间。
- 第三种情况网络区分:可能会有不同的网络中断情况,从而产生网络的分区。
在分布式系统中,经常会听说 CAP 理论, C 代表的是一致性, A 代表的是可用性, P 代表的是分区容忍性。CAP 理论描述的就是这三个要素在极端情况下只能够保证两个,不能够全部确保。
对于 CAP 理论的一个扩展,叫做 PACELC。其中 PAC 即 CAP 其实是一样的(E 除此之外)。CAP 三者在没有问题的一个前提下, L 和 C 也只能保证其中之一。也就是在一个分布式系统正常运行的情况下,延迟性和一致性也只能 2 选 1 保障其中的一个。
选择保证一致性可能就会有数据的延迟;而选择数据没有延迟,那这条数据就可能不是一致的。所以 PACELC 这个理论其实是在告诉我们要根据实际情况从 L 和 C 当中选择合适的一个来做保障,不可能两者兼顾。
对待这个问题,首先要考虑的应用需要的是真正对数据强一致要求更高?或是对性能的要求更高?
根据不同的场景要有所取舍,这个取舍就会造成对一致性的要求不一样。MongoDB 通过可调一致性来达到这个目的,让应用通过可调一致性选择自己合适的一致性保障,而不是一成不变的去用最高等级的一致性。
就像用关系数据库一样,用到不同的隔离等级、效果和性能是不一样的。关系数据库不会要求你总是用 read uncommitted 或者是 read committed ,是可以根据实际的情况来做调整的。MongoDB 通过可调一致性的方式就能够在这个分布式的环境下达到同样的目的。
MongoDB 支持哪些一致性选项?
Write concern 也叫做写关注,它包含哪些选项呢?
其中包含:W : 0、W :1 和 W :majority、同时还可以设置一个 J :true 或者 J :false。
- W :0,是我们很早期用过的一个配置,现在完全不推荐使用。
- W :1,性能比较好,当写操作到达一个节点(主节点)上,服务器就会反馈成功。
- W :majority,数据持久化比较好,这里所说的持久化可以认为是一条数据在写入之后,可以保证它不丢失,就是持有化的。
- J 代表的是 journal 也就是单次写入是不是要落到磁盘上
Write concern 其实是在持久化和性能上做选择。
注意:持久化在单机系统上和在分布式的系统统上有比较大的差异。
在单机系统上:只要落到硬盘上,就可以认为这条数据是持久化的。但是在分布式系统上落在单台机器的硬盘上并不能保证它的持久化,会有一些很极端的情况,导致必须把这些数据回滚的情况。
在分布式的系统上:如果要保证一条数据持久化,必须要保证这条数据在大多数的节点上落盘,才能够认为它是持久化。这也是为什么 write concern 有两个选项,一个是1另外一个是 majority ,因为其他的选项并没有意义。
比如:W 写到所有的节点上,那当然可以保证持久化,但是没有这个必要。因为写在所有的节点上跟写到大多数的节点上产生的持久化的效果是一样的。而写到所有的节点上又会花更多的时间。所以如果你设置了写到所有的节点上,那等于是浪费了时间,同时又没有得到更高的持久化能力。
J 代表的是怎样算作在一个节点上写入成功。是要落到硬盘上就成功?还是算落在内存当中就算成功?这分别对应的是 J 等于 true 和 J 等于 false 。
综合上面这些情况,W 和 J 有意义的组合其实只有以下几种。
- 当 W 等于 1 的时候:可以选择是不是要落到硬盘上,也就是 J 等于 true 或者是 false。
- 当 W 等于 majority 的时候:J 一定是等于 true, J 不可以等于 false。因为 majority 代表的是必须要大多数节点都接受这条数据,如果都没有落到硬盘上,就代表着可能被回滚,那就是没有被大多数节点去接受,这样从本质上是竟有意义的。
W 等于 1 和 W 的 majority 三个选项的组合,越往上代表的是效率越好,而越往下代表的是持久化的程度越高。所以根据你的业务的需求,应该做出适当的选择,到底是要效率好还是要持久化的更强好。
接下来简单看一下 W 等于 0 、 1 、majority 它们分别代表的是怎么样一个工作流程。
W 等于 0 :代表 driver 把这条数据扔给服务器,之后就不管了,根本不知道这条数据是否已经写入成功,会强制的认为它写入成功,完全不会得到服务端的确认。所以在现在的版本当中,已经不推荐去使用这个参数了。
W 等于 1 :代表去写到主节点上之后,需要主节点进行反馈,才会认为这条数据写成功了。如果主节点也没有反馈,或者反馈失败,就要做相应的错误处理,或者是抛异常,或者是记日志,或者是重试,总之需要采取一些措施。
W 等于 majority:代表必须把数据写到超过半数以上的节点上主节点才会反馈,代表这条数据写成功。
W 等于 1,且 J 等于 true :注意 J 的默认值是 false,即不落到硬盘上。所以当什么都没有配置的时候,数据其实是到达了 MongoDB 的内存当中,就会认为写操作是成功,这其实是一个不严谨的做法。
那么数据到达的内存当中时,如果机器宕机死掉了,是不是这条数据就丢了?
但是数据其实是会在, journal 是每隔 60 毫秒会自动的往磁盘上去刷一次。所以 J 即使等于 false,只要超过 60 毫秒(最多 60 毫秒),数据也会被写到磁盘上,也能够保证它的持久化。
所以一般来说,即使没有给 J 等于 true ,最多也就会丢失 60 毫秒以内的数据。如果加了 J 等于 true ,就代表着每一条数据写 MongoDB 的时候都必须要落到硬盘上。这样的话数据安全性是得到了保障,它不会丢,但是效率肯定是有损失。因为你每次在写操作的时候都会刷盘,对硬盘来讲压力是会变大的。攒齐 60 毫秒的操作,一次性刷盘,跟你每次操作都去刷盘,肯定是有性能上的差异。
除了 Write concern 之外,其实还会有 Read concern 。
读关注代表的是什么意义?
1. Local :代表读最新的,不管其他的节点怎么样,只管当前节点上最新的数据,把它读出来。
2. Majority :代表只能去读已经在大多数节点上落盘的数据。分布式系统里边的持久化代表的是必须在大多数节点上落盘。所以只去读大多数节点上落盘的那些数据,就代表的是始终读到的是持久化的数据,这个数据后面不可能被回滚,不可能丢失,它一定是存在已经持久化的。但是要想到读的是已经持久化的数据,那它就一定不是最新的数据,因为最新的数据它肯定还没有来得及在大多数数节点上落盘。
3. Snapshot:快照隔离等级代表读已经持久化并且会有一个快照,保证在整个的事务的过程当中,读到的东西是不会变的。
4. Linearizable:是最高的标准,也是最完美的一种隔离等级,但是效率比较差,越是趋于完美,效率就会越差。代表在读的时候能够让 MongoDB 的所有的节点从整体上看起来表现为一个单一的节点,虽然是一个分布式的数据库,不能让外界看到这个它内部有不一样的状态,这种最高的隔离等级叫做线性一致性。
Read concern 是允许使用者在新旧程度和一致性上做选择。最新的它一定不是持久化的,持久化的一定不是最新的。那到底要哪一个呢?这两者是一个矛盾。
Read concern与隔离等级
- Read concern :local 、majorities 、Snapshot 和 Linearizable
- 隔离等级:read uncommitted 、read committed 和 repeatable read 还有 Snapshot 还有 serializable
如果一定要做一个比较,那 read concern local 相当于 read uncommitted 或者叫脏读、majority 相当于是 read committed 、repeatable read 在 MongoDB 里边并不存在,但是它的能力可以由 Snapshot 来确保。所以说只提供了 Snapshot 这个隔离等级。
假设集群不会发生故障,节点也不会发生故障。都是读到的已经提交的数据。事务都不会回滚,读到的始终是 read committed ,但是不要忘记事务回滚的可能性是比节点故障的可能性要高的很多。节点故障有时运气好,可能几年都不会遇到一次。所以 read concern local 和 read concern majority 是没有那么大的区别,因为基本上是不会遇到节点故障这个问题的。
什么场景选择哪些可调一致性?
第一个场景:学生贷款申请网站。写入量其实很少,但是很重要,因为贷款一天能申请的人数不多。所以写入很重要,因为数据一旦丢失,可能就要从头填写申请表格,会带来很大的麻烦。所以要求持久化并不会造成性能的瓶颈。选择 W 等于 majority ,read concern 可以选择 majority 或者 local 。
第二个场景:匹配游戏对战玩家。如果我们是一个线上游戏平台,要去匹配游戏的对战玩家,因为游戏玩家的数量很多,这个匹配会大量的随时发生,所以量性能是非常重要的。如果匹配数据丢失,并不是很重要。因为开始重新匹配就可以,所以匹配丢失数据不重要,数据的新旧程度很重要,因为我们跟这个人匹配,就需要和这个人去对战。所以基于以上这些需求,可以要求 read concern 等于 local 也就是读最新的数据。而我们对 write concern 的要求只需要 1 就可以,因为性能对我们来说才是最重要。
第三个场景:点评网站。一个由用户来点评的网站,对用户来讲其实成本很高。平时自己去点了个外卖,有多少时间会去写点评呢?写点评对用户来讲是一个额外的负担,所以用户一般不愿意去写点评。就在用户不愿意写点评的情况下,如果写了肯定不希望写的东西丢失,对用户来讲绝对是不会再去重写一遍的。所以说这个数据的不可丢失特别重要,也就是持久化特别重要。
而读的时候所有用户希望看到的是最新的点评,即使是这条点评还没有持久化,也能够看到。这是对用户来讲最重要的,而不是在乎这条点评后续还存不存在这个问题。所以在这种场景下,应该选择读的时候最新,也就是读 local。而写的时候应该是去写到大多数的节点上。
从这三个场景可以看出,根据实际的应用情况,会去选择不同的 Write concern 和 Read concern。 大家在实际的使用过程当中也应该去这样分析,看一下到底什么样的配置对你来说是最合适的。
如何实现一致性?
Read concern majority 要求读持久化的这些数据。这个持久化的状态到底是怎么样被追踪到的?难道每读一次都要去跟所有的节点沟通一次,数据最新的是哪条?这样效率太低?
其实并不用这么麻烦,因为在 MongoDB 里边有考虑两个一致性,一个叫最终一致性,另一个叫因果一致性。
- 最终一致性:指的是在从节点上的状态最终都会跟主节点是一样,并且从节点上出现过的状态一定在主节点上出现过。
- 因果一致性:指的是在事件发生的因果关系一定是会受到保障,并不会错乱,是先发生的事情一定是先发生,不管在哪一个节点上,它的顺序是不会有颠倒。
所以在某一个时刻,主节点进入了 S1 状态。那这个状态会被复制到从节点上,因为最终一致性,从节点也必须到达这个状态。从节点上会向主节点反馈,它已经得到了这个状态。一旦有一个从节点反馈,三个节点当中就有两个节点确认了这个状态。主节点也就知道 S1 这个状态已经持久化了(因为已经有一个从节点接受了这个状态)。又因为因果一致性的存在,所以如果 S1 这个状态已经持久化了,那么所有 S1 以前的状态肯定也已经持久化。
当一条数据写进 MongoDB 的时候,如果这条数据已经到达大多数节点,那就可以说,这条数据之前写入的所有数据都已经到达大多数节点,都已经持久化。所以需要去判断的就是一个时间点,在这个时间点以前的所有的数据都是持久化的。而在这个时间点以后的数据可能还没有持有化。这个时间点,就把它叫做 majority commit point 大多数提交时间点。
在进行 read concern majority 的读取的时候,只需要知道去读这个 majority commit point 之前的数据就可以保证读到的数据一定是持久化数据。所以并不需要跟所有的节点去做沟通去比对数据,只需要知道去读这个时间点之前的产生的数据,它就一定是持久化的。
关于其他数据库的可调一致性
那这里的一些选项是根据过去的一些产品的状态了解到的情况。这个可能并不一定是最新的一个情况,可能已经做了一些改进。
- GoldenGate 和 DataGuard 这两个都是 Oracle 提供的一个数据同步的工具。因为关系数据库一般来讲都是单机的。如果要讨论集群的情况,讨论一致性的情况,那它一定是一个集群。集群的情况就会有数据复制这样一个概念的存在。在 Oracle 当中,数据的复制是通过 golden gate 和 data guard 来完成的。
我们用这两个产品的时候,认为它的读只能够选择 local ,而它的写只能够选择 W 等于 1 和 W 等于 all 。这两个产品不同的配置,它就没有能够给你提供一个你可以自由选择的这样一个能力。
- MySQL 的组复制,也叫 Group replication。它的读一定是读大多数节点上已经提交的数据,所以你就没有办法去读最新的。可以选择写到一个或者写大多数节点,所以它能够提供给你一定的可调一致性的这个能力。但是并不完善。
- PostgreSQL 在集群状态下读的时候只能读最新的。在写的时候可以写1或者是写一个节点加上一个节点,没有其他的配置。也就是说当你有多个复制节点的时候,你也最多能写两个节点。所以它也能够提供很少量的可调一致性的定制方案,但是也比较少。
- Cassandra 提供的能力可以说是最完善的,但是可调一致性的能力有一些欠缺。我们认为在读 majority 的时候,它提供的可调一致性的能力是比较有限的,因为它的性能会比较差。它不是通过去像我们用 MongoDB 一样,用 majority commit point 去判断的,其实是需要跟每一个节点去沟通,来确定哪些数据是在大多数节点上截掉要提交的,所以它的性能相对来说会比较差一些。
一致性多语句事务
接下来是更深入一些的话题,这些可能对我们的使用上影响并不是那么严重。但是如果能够了解其中的细节原理的话,对分布式的环境会有更大的帮助。
事物和分布式系统它们是一个什么样的关系?
当提到事物的时候,往往都是在强调强一致。如果在这样的环境当中去做一个分布式的事务,那这个事物就只能基于一份陈旧的数据来做(持久化的数据都不是最新的数据),这里会产生很大问题是可能性已经有新版本的数据了,只是因为这些新版本的数据还没有来得及在大多数节点上落盘。所以那些数据又没有办法用,只能基于一份老的数据来做修改。等到想要提交这个事务的时候,就有可能会发现别人已经先提交了,你这个事务就必须作废。也就是在 MongoDB 里看到事物的回滚。所以这样做在分布式的环境当中是有缺陷的,并不是说它一定会失败,但是它会大大提升提高你的事务失败的风险。
如果不以最持久化的数据为基础来进行事务会发生什么?也就是在进行事务的时候,基于最新的数据来完成这个事务。这样就有可能会遇到新数据被回滚,因为新数据还没有来得及落盘,它在极端情况下是有可能回滚,虽然这个概率不大,但是确实有可能。所以基于一份还没有持久化的数据来进行事务,这其实是有违事物本身的一个定义,也不合适。
什么是推测性执行模型?
为了很好的解决这个问题,MongoDB 在实现事务的时候,做了一个权衡叫做推测性执行模型。让事物不使用陈旧数据,会去选择最新的数据,让事物基于最新数据来运行。
假设新数据最终都能够持久化,也就是会到达大多数的节点,除非这个节点发生故障,要不然这些新的数据一定都能够持久化。并做一个事情来保证不会发生问题。在进行事务的时候选择了最新的数据,在进行提交之前,会等到最新的数据持久化完成之后才会提交当前的事务。也就是能够基于最新的数据进行事务,并且也能够保证最新的数据要持久化之后才会提交这个事务,这样在逻辑上就不会有问题,并且在性能上大概率也不会产生影响。虽然是基于最新的数据,但是在事务当中进行的写操作必须是持久化的写操作。所以你在事务当中进行一定是 majority 的写。
因果一致性保证当你在这个事物当中进行的写都已经是到达大多数节点,在这个事物读的这些数据肯定也已经到达大多数节点,所以也就代表着对性能不会有影响,同事又能够提供很好的一致性的保障,这样就可以大大的减少事务的冲突的可能性。这个也是在 MongoDB 的事务公测之后,根据用户的反馈做出的一些修改。然后最后变成了这样一个推测执行模型。
数据回滚
Raft 一致性算法
MongoDB 的复制协议是基于 Raft 一致性算法去完成的。Raft 一致性算法有一个基本的要求是操作日志必须持久化。在数据持久化之后,也就是到达大多数节点之后,操作日志才可以提交。如果放在 MongoDB 上,就会认为 oplog 必须要在大多数节点上都持久化之后才能够写进 oplog 里边。换一个说法就是 Raft 的要求是操作日志不会回滚。但是实际在 MongoDB 里,为了去实现这个推测性执行模型,提供最新数据的读取能力,必须提前提交操作日志,这样才能够被复制到其他的节点上,所有的节点才能够读到最新的数据。既然提前提交了这个操作日志,就代表着数据有可能发生回滚,换种说法就是推测性执行模型,导致了可能会发生数据的回滚。
RTT (Recover to Timestamp)回滚
在 4.0 开始,提供了 RTT (Recover to Timestamp)的能力,可以让 MongoDB 直接回到某一个时间点。在回滚的时候,当旧的主节点的状态跟新的主节点的状态不一样的时候,就会通过 RTT 的能力回滚到过去的一个时间点。
注意:RTT 可能会把这个时间点回滚到一个比较早的状态。比如旧的主节点和新的主节点最后的一个公共状态是 12:00 这个时刻,那 RTT 可能会把你回滚到 11:59。只需要把后面差异的这一部分通过去叠加 oplog 就可以追到公共时间点,这样回滚就可以完成了。
这也是我们更期望使用的一种能力,因为它很快,它可以代价很小地完成。
Fetch Based Rollback
在 4.0 之前,还有一种回滚的方式叫做 Fetch Based Rollback。也就是在发生回滚的时候,首先找到最新的公共状态,最后一个公共状态还是假设是在 12:00,这是最后一个公共状态,就会去把最后的公共状态之后所有修改过的文档,从新的主节点重新抓取一遍。
注意:重新抓取的肯定不是这个这些文档在 12:00 这个时刻的状态,可能有些文档又被改过了,所以抓到的是文档的最新版本,而不是公共状态 12:00 这个时刻的版本。
为了解决这个问题,利用的是 MongoDB oplog 的幂等性,把这一段时间 ,12:00 到最新状态之间的 oplog 再重放一遍。这样旧的主节点就能够到达一个跟新的组节点一致的状态,后续才可以继续开始复制。
这里其实是利用了幂等性,这种方式它实现比较复杂,并且是在老版本当中使用的。后续的版本当中基本上不太会去再用到这种这个 rollback 的方式。
所以如果大家在 4.0 和 4.0 之间不同的版本当中看到的这个 rollback, 可能要求会有些不一样。比如说能够回滚多少数据?能够回滚多长时间的数据?这些要求都会有些不一样。那这些不一样的点从哪里来的?其实就是因为我们在用不同的方式来完成这个 rollback 这个动作。
QA环节
Q. 是否可以设置 W 等于任意数字?
是可以的。可以设置 W 等于 1 等于 2 或者是任何一个数字。但是有意义的设置只有两个,第一个是 W 等于1,第二个是 W 等于 majority 。
假设我有 5 个节点,当设置 W 等于 1 或者 W 等于2,其实它达到的持久化的保障是一样的。在概率上来讲可能 2 会比 1 好那么一点点,但是并不能保障这个数据一定是能够持久化的。所以 2 和等于 1(5 节点的情况)其实达到的效果是一样的。但是等于 2 你要花更长的时间,而它又不能够提供更高的保障,是没有什么意义。所以,有意义的选项只是 W 等于 1 和 W 等于 majority。
Q. 回滚是自动完成还是手动?
这个是自动完成的。一个旧的主节点回滚是怎么发生的?当一个旧的主节点接受了数据的写入,但是还没有来得及把这些数据分发出去,就 down 掉了。那新的主节点被选取出来的之后是没有这些数据的,但是也需要恢复服务,然后会继续先接受写操作。那这就造成了新主节点和旧主节点的状态不一致,旧主节点上有一些数据是新主节点上没有的。
那当旧的主节点恢复服务之后,就没有办法直接加入新的这个节点去复制数据,因为自己上面有别人没有的数据,所以就要进行回滚的动作,这个动作是自动完成的,自动会去找到公共的这个祖先状态在哪里,然后它把这个节点之后的那些数据回滚掉,然后它变成了一个一致的状态之后才可以开始继续从新的主节点去复制数据。这个动作是自动完成的,不需要手动的去做。
Q. 可以查询最近的公共状态的时间吗?
因为 majority commit point 其实是会一直在往前推进的。当你看到的这一刻,其实就已经在继续往前推进了,所以它是一个不断的在变化的这样一个东西。这个要看你对几个节点的使用情况。当前的 majority commit point 可以在 rs.status() 中的 optimes.last CommittedOp Time 找到。
Q. 做集群配置时,是不是主节点的 CPU 主频越高越好?
默认情况下读和写都是用的主节点,所以它的资源消耗肯定会更高一些。但是其实是可以配置你要从哪个节点读的,所以并不是主节点的 CPU 就一定要更高,也有可能如果把大部分的读都打到从节点上了之后,可能从节点上的资源要求会更高。所以并不是说主节点的频率越高越好。
并且还要记住一个问题,主节点这个角色落在谁的身上,这是一个会变化的,不是固定的状态。不是这个节点是主节点,将来就一定一直都是主节点。主节点在分布式的环境当中,是会出现故障的,出现故障之后是有节点来替代现在的位置,所以不可能永远都是主节点。
如果把单台机器的配置做得比其他节点的配置好,那就要考虑一个风险性,如果将来这台机器一旦 down 掉了其他节点接替它的时候,它有没有能力能够接替他的工作?因为它们的资源比它少。有可能出现最极端的情况,就是其他节点一接替它因为压力太大就被压死了,那就会出现一个级联的宕机,高可用实际上就丢失了,这就是一个很大的风险。所以我们在选择主机配置的时候,应该是每一个节点的配置应该是一样的,因为它们都有一样的概率会成为新的主节点。
关于作者:张耀星
MongoDB 中文社区常委会委员
MongoDB 中文社区论坛联席主席
MongoDB 北亚区首席技术咨询服务顾问
在 MongoDB 的开发、应用和咨询服务方面,拥有多年的丰富实践经验。
直播彩蛋
因为工作日错过了上一场直播?还有其他问题没得到解答?别担心!8 月 20 日(星期六)由 MongoDB 中文社区常委会委员、北京分会主席、某互联网公司运维负责人李丹老师带来的线上直播活动即将来袭!
现在点击这里填写报名表单入群,就可以提前预知直播内容,并且有社区核心成员将为大家线上答疑,帮助开发者们快速理清困惑。
感谢大家一直以来对社区的关注与支持!社区在大家共同的努力下不断的发展与壮大,为了给大家营造更便捷的交流环境,QQ 技术交流群将同步在“微信技术交流群”中。扫描下方二维码添加小芒果微信发送“mongo”即可进入技术交流群。
评论前必须登录!
注册