前言:mongodb作为一个分布式系统在实际应用中也要面临着访问延迟和一致性之间的权衡。好在mongodb对读写操作的访问延迟与一致性权衡上提供了丰富的选项。

1、write-concern:确认该操作已应用于复制集中大多数成员(准确说是可投票成员);
2、read-concern:数据已被大多数复制集成员确认并且持久化的(默认应该是majority);

3、read-preference: 对于replica set,是返回当前节点的最新数据,还是返回写入节点最多的数据,还是根据一些函数计算出的数据。

https://docs.mongodb.com/manual/reference/read-concern/

https://docs.mongodb.com/manual/reference/write-concern/

https://docs.mongodb.com/manual/core/replica-set-write-concern/

https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/

一、写关注(write concern)

        写关注描述了mongodb针对单机mongod或者副本集或分片集群的写操作的ack级别。对于分片集群,mongos实例会把写关注透传到shards。

从4.4版本开始,副本集和分片集群支持设置全局默认的write concern。没有显示指定写关注的操作将继承全局默认的设置。有关更多信息,参考 setDefaultRWConcern.

对于某些应用程序来说,写关注十分重要。它能判断哪些写操作成功写入了,哪些失败了。这样对于失败操作,驱动程序能返回错误,由应用程序决定怎么处理。如果没有写关注,应用程序发送一个写操作到socket后剩下的就完全不管了,至于后面发生了什么、是否写入数据库统统不关心;显然对于很多应用这样是不行的。带有写关注的操作会等到数据库确认成功写入后才能返回,因此写关注会带来性能上的损失。

1、写关注的指定

写关注包括如下字段:

 { w: <value>, j: <boolean>, wtimeout: <number> }

(1)w选项:确认写入操作的请求已传播到指定数量的mongod实例或具有指定标记的mongod实例。

①w: majority——写操作传播到数据承载节点的calculated majority成员;这里举个例子,对于一个3节点的副本集(Primary-Secondary-Secondary,P-S-S),它的calculated majority是2,即写操作必须传播到primary和一个secondary同ack写关注给client。

②w: number——写操作传播到指定数量的mongod实例;当取值为0时,驱动程序不会使用写关注,只返回网络和socket的错误。当取值为1时,驱动程序使用写关注,但是只针对primary节点,这个配置项是对于复制集或单mongod实例默认写关注配置。当取值为整数且大于1时,写关注将针对复制集中的n个节点,当客户端收到这些节点的反馈信息后,命令才返回给客户端继续执行。

(2)j选项:设为1表示确认写入操作的请求已经写入磁盘日志(on-disk journal),也就是下一次Journal log提交。这种情况是可以容忍服务器突然宕机,断电等意外情况的数据恢复的。

(3)wtimeout选项:指定时间限制,以防止写操作无限期阻塞。

下图是一个配置了"w:2"的写关注执行流程图,一个是primary节点,一个是secondary节点。

 一个需要写关注ack的应用程序发出一个写操作后会等待直到primary节点接受必要数量节点的相应写关注ack。对于w选项为“majority”或者w大于1来说,primary节点会一直等待直到必要数量的secondary作出ack才会返回写关注ack。对于w为1的写关注,primary节点可以在本地写操作完成后立即返回写关注确认(ack)。

确认写操作的成员越多,因为主操作失败而导致的回滚的可能性就越小。但是指定更高的写入关注会带来延迟增加的问题,因为客户端必要等待直到它收到指定级别的写关注确认(ack)。

2、验证副本集的写关注

 下面的操作是包括写关注选项的insert方法。操作指定w为"majority"此外还指定wtimeout为5秒以防止操作被无限期的阻塞。

db.products.insert(
   { item: "envelopes", qty : 100, type: "Clasp" },
   { writeConcern: { w: "majority" , wtimeout: 5000 } }
)

二、读关注(read concern)

        read concern 是针对读操作的配置。它控制读取数据的"新鲜度"和持久性。read concern 选项控制数据读取的一致性,分为 local、available、majority、linearizable 四种,它们对一致性的承诺依次由弱到强。其中 linearizable 表示线性一致性,另外 3 种级别代表了 MongoDB 在实现最终一致性时,对访问延迟和一致性的取舍。
(1)local/available: 语义基本一致,都是读操作直接读取本地最新的数据,但不保证该数据已被写入大多数复制集成员。数据可能会被回滚。默认是针对主节点读。 如果读取操作与因果一致的会话相关联,则针对副节点读。唯一的区别在于,avaliable 在分片集群场景下,为了保证性能,可能返回孤儿文档。


(2)majority:读取 majority committed 的数据,可以保证读取的数据不会被回滚,但是并不能保证读到本地最新的数据。受限于不同节点的复制进度,可能会读取到更旧的值。当写操作对应的 write concern 配置中 w 的值越大,则写操作在扩散到更多的复制集节点上之后才返回写成功,这时通过 read concern 被配置为 majority 的读操作进行读取数据,就有更大的概率读取到最新的数据。
(3)linearizable:读取 majority committed 的数据,但会等待在读之前所有的 majority committed 确认。它承诺线性一致性,要求读写顺序和操作真实发生的时间完全一致,既保证能读取到最新的数据,也保证读到数据不会被回滚。只对读取单个文档时有效,且可能导致非常慢的读,因此总是建议配合使用 maxTimeMS 使用。linearizable 只能用在主节点的读操作上,考虑到写操作也只能发生在主节点上,相当于说 MongoDB 的线性一致性被限定在单机环境下实现。实现 linearizable,读取的数据应该是被 write concern 为 majority 的写操作写入到 MongoDB 集群中的、且持久化到日志中的数据。如果数据写入到多数节点后,没有在日志中持久化,当这些节点发生重启恢复,那么之前通过配置 read concern 为 linearizable 的读操作读取到的数据就可能丢失。可以通过 writeConcernMajorityJournalDefault 选项保证指定 write concern 为 majority 的写操作在日志中是否持久化。如果写操作持久化到了日志中,但是没有复制到多数节点,在重新选主后,同样可能会发生数据丢失,违背一致性承诺。


snapshot: 与关系型数据库中的快照隔离级别语义一致。最高隔离级别,接近于 serializable。是伴随着 MongoDB 4.0 版本中新出现的多文档事务而设计的,只能用在显式开启的多文档事务中。如果事务是因果一致会话的一部分,且 write concern 为 majority,则在事务提交后,读操作可以保证已从多数提交数据的快照中读取,该快照提供与该事务开始之前的操作的因果一致性。它读取 majority committed 的数据,但可能读不到最新的已提交数据。snapshot 保证在事务中的读不出现脏读、不可重复读和幻读。 因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放

思考

        当负载过大时mongodb的主从同步存在明显延迟,导致有时候读取不到最新的数据。

        正常使用场景这种延迟几乎是感知不到的,但是如果写入负载特别大的情况下可能就会出现较为明显的延迟。关于这一点也不是mongodb的问题,因为CAP理论决定了一致性、可用性和分区容忍性只能3取2,对于大多数据分布式数据库来说都是选择A(可用性)和P(分区容忍性)放弃C(一致性),转而追求要求不那么高的最终一致性。

注:CAP理论中的C(一致性)指的是线性一致性(强一致性),与之对应的是最终一致性(弱一致性)。

        也就是说无论负载如何,延迟都是一定存在的,只是时间长短的问题。如果你的业务非常强依赖这里的一致性,那么一定要在逻辑上进行保证而不是把希望寄托于“运气”或负载高低之上。

        当程序写完数据开始读的时候,要确保从库已经复制到了刚才写入的数据,显然这里不能依赖于程序sleep。正确的做法是{w,"majority"},即写的时候要阻塞到写操作已经对大多数节点生效才算完成。但是仅有这一点是不够的,因为你并不能保证你所读取的节点正好在“大多数”之内。既然如此可不可以用{w,3}呢(这假设3副本)?如果真的这么做了。除了阻塞时间更长以外,更为致命的问题就是强依赖与所有节点了,其中任何一个节点失败(例如宕机)就会导致所有节点都失败。所以更合理的做法是{readConcern:"majority"},其语义是大多数节点有就算有。至此,我们得到了如下方案:

        {w: "majority"} + {readConcern: "majority"}

        然后你就会发现新的问题。那便是你做任何一件事情都需要集群中的大多数成员承认,也就是说一个请求要扩散到大多数节点之上且获得它们的确认。显然这违背了分散压力的初衷(甚至于压力更大了)。但是没有办法:“天下没有免费的午餐”。 但是副本集很重要的一个作用就是分散压力(读写分离);究竟是只读primary保证线性一致性 还是 适当的放弃一致性确保高性能 就需要结合具体业务场景来考虑了。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐