Mongo数据一致性浅析

2023-08-19 09:30:18 浏览数 (2)

一致性简介

根据 CAP 理论的一致性(Consistency)问题,即在读写发生在不同节点的情况下,怎么保证每次读取都能获取到最新写入的数据。这个一致性即是我们今天要讨论的MongoDB 可调一致性模型中的一致性,区别于单机数据库系统中经常提到的 ACID 理论中的一致性。

可调性具体指的是什么呢? 这里就不得不提分布式系统中的另外一个理论,PACELC。PACELC 在 CAP 提出 10 年之后,即 2012 年,在一篇 Paper 中被正式提出,其核心观点是,根据 CAP,在一个存在网络分区( P )的分布式系统中,我们面临在可用性( A )和一致性( C )之间的选择,但除此之外( E ),即使暂时没有网络分区的存在,在实际系统中,我们也要面临在访问延迟( L )和一致性( C )之间的抉择。所以,PACELC 理论是结合现实情况,对 CAP 理论的一种扩展。

以复制为基础构建的分布式系统中,一致性模型通常可按照以数据为中心(Data-centric)以客户端为中心(Client-centric)来划分,

mongo一致性

MongoDB 的 Causal Consistency Session 即提供了上述几个承诺:RYW,MR,MW,WFR。

但是,这里是 MongoDB 和标准不太一样的地方,MongoDB 的因果一致性提供的是 Client-centric 一致性模型下的承诺,而非 Data-centric。这么做主要还是从系统开销角度考虑,实现 Data-centric 下的因果一致性所需要的全局一致性视图代价过高,在真实的场景中,Client-centric 一致性模型往往足够了,关于这一点的详细论述可参考 MongoDB 官方在 SIGMOD’19 上 Paper 的 2.3 节。

Causal Consistency 在 MongoDB 中是相对比较独立一块实现,只有当客户端读写的时候开启 Causal Consistency Session 才提供相应承诺没有开启 Causal Consistency Session 时,MongoDB 通过 writeConcern 和 readConcern 接口提供了可调一致性,具体来说,包括线性一致性和最终一致性

最终一致性在标准中的定义是非常宽松的,是最弱的一致性模型,但是在这个一致性级别下 MongoDB 也通过 writeConcern 和 readConcern 接口的配合使用,提供了丰富的对性能和正确性的选择,从而贴近真实的业务场景。

ReadConcern

readConcern 的初衷在于解决『脏读』的问题,比如用户从 MongoDB 的 primary 上读取了某一条数据,但这条数据并没有同步到大多数节点,然后 primary 就故障了,重新恢复后 这个primary 节点会将未同步到大多数节点的数据回滚掉,导致用户读到了『脏数据』。 当指定 readConcern 级别为 majority 时,能保证用户读到的数据『已经写入到大多数节点』,而这样的数据肯定不会发生回滚,避免了脏读的问题。 需要注意的是, readConcern 能保证读到的数据『不会发生回滚』,但并不能保证读到的数据是最新的,这个官网上也有说明。

WriteConcern

MongoDB支持的WriteConcern选项如下

  1. w: 数据写入到number个节点才向用客户端确认
  • {w: 0} 对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
  • {w: 1} 默认的writeConcern,数据写入到Primary就向客户端发送确认
  • {w: “majority”} 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能
  1. j: 写入操作的journal持久化后才向客户端确认
  • 默认为”{j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true
  1. wtimeout: 写入超时时间,仅w的值大于1时有效。
  • 当指定{w: }时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败。

三方库支持

源码地址:https://github.com/mongodb/mongo-go-driver

这里以golang的mongo-driver库为例:

代码语言:javascript复制
type Collection struct {
   client *Client
   db *Database
   name string
   readConcern *readconcern.ReadConcern
   writeConcern *writeconcern.WriteConcern
   readPreference *readpref.ReadPref
   readSelector description.ServerSelector
   writeSelector description.ServerSelector
   registry *bsoncodec.Registry
}
// ReadPref determines which servers are considered suitable for read operations.
type ReadPref struct {
  maxStaleness time.Duration
  maxStalenessSet bool
  mode Mode
  tagSets []tag.Set
  hedgeEnabled *bool
}
// Mode indicates the user's preference on reads.
type Mode uint8
// Mode constants
const (
  _ Mode = iota
  // PrimaryMode indicates that only a primary is
  // considered for reading. This is the default
  // mode.
  PrimaryMode
  // PrimaryPreferredMode indicates that if a primary
  // is available, use it; otherwise, eligible
  // secondaries will be considered.
  PrimaryPreferredMode
  // SecondaryMode indicates that only secondaries
  // should be considered.
  SecondaryMode
  // SecondaryPreferredMode indicates that only secondaries
  // should be considered when one is available. If none
  // are available, then a primary will be considered.
  SecondaryPreferredMode
  // NearestMode indicates that all primaries and secondaries
  // will be considered.
  NearestMode
)

读写分离

mongodb进行了读写分离,写入往主库,读取从从库,这样减轻了主库的压力。但由于从库同步数据的延时性,某数据在主库写入后马上从从库读,会读取到旧数据并且会将旧数据塞入了缓存。mongodb写的操作有相关设置,writeConcern可设置为Replica Acknowledged级别,即数据被写入到至少两个从库节点返回。这样虽然一定程度上解决从库延时,但写的性能大大降低。

同步流程

Primary上的写入会记录oplog,存储到一个固定大小的capped collection里,Secondary主动从Primary上拉取oplog并重放应用到自身,以保持数据与Primary节点上一致。

initial sync

新节点加入(或者主动向Secondary发送resync)时,Secondary会先进行一次initial sync,即全量同步,遍历Primary上的所有DB的所有集合,将数据拷贝到自身节点,然后读取『全量同步开始到结束时间段内』的oplog并重放。全量同步不是本文讨论的重点,将不作过多的介绍。

tailing oplog

全量同步结束后,Secondary就开始从结束时间点建立tailable cursor,不断的从同步源拉取oplog并重放应用到自身,这个过程并不是由一个线程来完成的,mongodb为了提升同步效率,将拉取oplog以及重放oplog分到了不同的线程来执行。

  • producer thread,这个线程不断的从同步源上拉取oplog,并加入到一个BlockQueue的队列里保存着,BlockQueue最大存储240MB的oplog数据,当超过这个阈值时,就必须等到oplog被replBatcher消费掉才能继续拉取。
  • replBatcher thread,这个线程负责逐个从producer thread的队列里取出oplog,并放到自己维护的队列里,这个队列最多允许5000个元素,并且元素总大小不超过512MB,当队列满了时,就需要等待oplogApplication消费掉。
  • oplogApplication会取出replBatch thread当前队列的所有元素,并将元素根据docId(如果存储引擎不支持文档锁,则根据集合名称)分散到不同的replWriter线程,replWriter线程将所有的oplog应用到自身;等待所有oplog都应用完毕,oplogApplication线程将所有的oplog顺序写入到local.oplog.rs集合。

producer的buffer和apply线程的统计信息都可以通过db.serverStatus().metrics.repl来查询到 默认情况下,Secondary采用16个replWriter线程来重放oplog,可通过启动时设置replWriterThreadCount参数来定制线程数,当提升线程数到32时,同步的情况大大改观,主备写入的qps基本持平,主备上数据同步的延时控制在1s以内。

修改replWriterThreadCount参数的方法,具体应该调整到多少跟Primary上的写入负载如写入qps、平均文档大小等相关,并没有统一的值。

通过mongod命令行来指定 mongod --setParameter replWriterThreadCount=32

在配置文件中指定

代码语言:javascript复制
setParameter:
    replWriterThreadCount: 32

oplog示例

代码语言:javascript复制
cmgo-9t1zzciv_0:PRIMARY> use local
switched to db local
cmgo-9t1zzciv_0:PRIMARY> db.oplog.rs.find({}).limit(1).pretty()
{
        "ts" : Timestamp(1672717376, 3),
        "t" : NumberLong(2),
        "h" : NumberLong(0),
        "v" : 2,
        "op" : "u",
        "ns" : "component.deploy",
        "ui" : UUID("a164a9ab-7b3a-43e5-890d-64e1142b3455"),
        "o2" : {
                "_id" : NumberLong(8814)
        },
        "wall" : ISODate("2023-01-03T03:42:56.762Z"),
        "lsid" : {
                "id" : UUID("f2ce9e4a-b863-4830-9824-d5e773846512"),
                "uid" : BinData(0," WgOVnKmrMF6/mSQ4vglRC SRli4twPTLNSeVRPyVy8=")
        },
        "txnNumber" : NumberLong(1325),
        "stmtId" : 0,
        "prevOpTime" : {
                "ts" : Timestamp(0, 0),
                "t" : NumberLong(-1)
        },
        "o" : {
                "$v" : 1,
                "$set" : {}
      }
}

http://mysql.taobao.org/monthly/2021/04/02/

0 人点赞