一致性简介
根据 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选项如下
w: 数据写入到number个节点才向用客户端确认
{w: 0} 对客户端的写入不需要发送任何确认,适用于性能要求高,但不关注正确性的场景
{w: 1} 默认的writeConcern,数据写入到Primary就向客户端发送确认
{w: “majority”} 数据写入到副本集大多数成员后向客户端发送确认,适用于对数据安全性要求比较高的场景,该选项会降低写入性能
j: 写入操作的journal持久化后才向客户端确认
默认为”{j: false},如果要求Primary写入持久化了才向客户端确认,则指定该选项为true
wtimeout: 写入超时时间,仅w的值大于1时有效。
当指定{w: }时,数据需要成功写入number个节点才算成功,如果写入过程中有节点故障,可能导致这个条件一直不能满足,从而一直不能向客户端发送确认结果,针对这种情况,客户端可设置wtimeout选项来指定超时时间,当写入过程持续超过该时间仍未结束,则认为写入失败。
三方库支持
源码地址:https://github.com/mongodb/mongo-go-driver
这里以golang的mongo-driver库为例:
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
在配置文件中指定
setParameter:
replWriterThreadCount: 32
oplog示例
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/