共计 4672 个字符,预计需要花费 12 分钟才能阅读完成。
本文分两部分,分布式和单机。单个 db 的存储引擎,物理和数据存储简介,事务实现等。分布式架构,分布式涉及的复制集,分片等可靠性和扩展性保障。
第一部分 单机存储引擎介绍
引擎
wiredTiger(简称 WT)支持行存储、列存储以及 LSM 等 3 种存储形式,Mongodb 使用时,只是将其作为普通的 KV 存储引擎来使用,mongodb 的每个集合对应一个 WT 的 table,table 里包含多个 Key-value pairs,以 B 树形式存储。mongodb 的集合和索引都对应一个 wiredTiger 的 table。并依赖于 wiredTiger 提供的 checkpoint + write ahead log 机制提供高数据可靠性,目前支持单机事务。
按照 Mongodb 默认的配置,WiredTiger 的写操作会先写入 Cache,并持久化到 WAL(Write ahead log journal),每 60s 或 log 文件达到 2GB 时会做一次 Checkpoint,将当前的数据持久化,产生一个新的快照。Wiredtiger 连接初始化时,首先将数据恢复至最新的快照状态,然后根据 WAL 恢复数据,以保证存储可靠性。
Wiredtiger 的 Cache 采用 Btree 的方式组织,每个 Btree 节点为一个 page,root page 是 btree 的根节点,internal page 是 btree 的中间索引节点,leaf page 是真正存储数据的叶子节点;btree 的数据以 page 为单位按需从磁盘加载或写入磁盘。
Wiredtiger 采用 Copy on write 的方式管理修改操作(insert、update、delete),保证一致性,并且不像 InnoDB 一样,需要一个 DoubleWriteBuffer 保证非 disk block 512B 写时对原有页可能发生 conrrupt。修改操作会先缓存在 cache 里,持久化时,修改操作不会在原来的 leaf page 上进行,而是写入新分配的 page,每次 checkpoint 都会产生一个新的 root page。
内存结构:B 树,索引页和数据页,新插入跳表(有序)更新 list(变更),wal,copy onn write
物理结构:
写入:写入页的跳表,不改变原值
更新:写入更新数组中
读取:若有 update 合并
做 checkpoint 时,将更新和写入到新的页中,生成新的 page_root 快照。
ACID:隔离用的未提交快照。注意这个快照只是读用的。写还是到最新页。
journal: 并发,预先分配 slots, 申请用 CAS,buffer 和 lsn 刷盘
关于快照, 缓存,数据
每个事务有自己的快照 (可能是旧的 page_root),新的事务会获取当前最新 page_root,checkpoint 对最新的最持久化和生成新 page_root 到磁盘,按需读取到内存。新建连接会先进行磁盘的数据的数据恢复,最新快照 +WAL,按需读入内存
cache 驱逐:略
数据清理:
- compact 加的是 DB 级别的互斥写锁,同一个 DB 上的读写都会被阻塞
- compact 基本不需要额外的空间,wiredtiger compact 的原理是将数据不断往前面的空洞挪动,并不需要把数据存储到临时的位置(额外的存储空间)。
运行中内存占用
存储引擎 cache,集合,索引元数据,新写入数据
MongoDB Driver 会跟 mongod 进程建立 tcp 连接,并在连接上发送数据库请求,接受应答,tcp 协议栈除了为连接维护 socket 元数据为,每个连接会有一个 read buffer 及 write buffer,用户收发网络包,buffer 的大小通过如下 sysctl 系统参数配置,分别是 buffer 的最小值、默认值以及最大值。500 个类似的连接就会占用掉 1GB 的内存,ss -m
其他并发大时排序等
主备节点差异,备节点 buffer 存储 oplog
分布式
扩展性
分片:范围,hash
迁移步骤:
集合分片开启后,默认会创建一个新的 chunk,shard key 取值 [minKey, maxKey] 内的文档(即所有的文档)都会存储到这个 chunk。当使用 Hash 分片策略时,可以预先创建多个 chunk,以减少 chunk 的迁移。
一个 Sharded Cluster 里可能有很多个 mongos,如果所有的 mongos 的 Balancer 同时去触发迁移,整个集群就乱了,为了不出乱子,同一时刻只能让一个 Balancer 去做负载均衡。
Balancer 在开始负载均衡前,会先抢锁(config.locks 集合下的一个特殊文档),抢到锁的 Balancer 继续干活,没抢到锁的则继续等待,一段时间后再尝试抢锁。
Step1: mongos 发送 moveChunk 给源 shard
mongos 接受到用户发送的迁移 chunk 命令,或者因负载均衡策略需要迁移 chunk,会构建一个 moveChunk 的命令,并发送给源 shard。Step2:源 shard 通知目标 shard 开始同步 chunk 数据
源 shard 收到 mongos 发送的 moveChunk 命令后,会向目标 shard 发送 _recvChunkStart 的命令,通知目标 shard 开始迁移数据(真正的数据迁移由目标 shard 主动发起)。接下来,源 shard 会记录该 chunk 在迁移过程中的所有增量修改操作。Step3: 目标 shard 同步 chunk 数据到本地
目标 shard 接受到 _recvChunkStart 命令后,就会启动单独的线程来读取 chunk 数据并写到本地,主要步骤包括:目标 shard 创建集合及索引(如果有必要)如果迁移的集合在目标 shard 上没有任何 chunk,则需要先在目标 shard 上创建集合,并创建跟源 shard 上集合相同的索引
目标 shard 清理脏数据(如果有必要)如果目标 shard 上已经存在该 chunk 范围内的数据,则肯定为某次迁移失败导致的脏数据,先将这些数据清理掉。目标 shard 向源 shard 发送 _migrateClone 命令读取 chunk 范围内的所有文档并写入到本地,即迁移 chunk 全量数据,迁移完后更新状态为 STEADY(可以理解为全量迁移完成的状态)。源 shard 会不断调用查询目标 shard 上的迁移状态,看是否为 STEADY 状态,如果已经是 STEADY 状态,就会停止源 shard 上的写操作(通过对集合加互斥写锁实现)。接下来发送 _recvChunkCommit 告诉目标 shard 不会再有新的写入了。目标 shard 的迁移线程不断向源 shard 发送 _transferMods 命令,读取迁移过程中的增量修改,并应用到本地,增量迁移完成后,向源确认 _recvChunkCommit 的结果。源 shard 收到 _recvChunkCommit 的结果,整个数据迁移的步骤完成。Step4:源 shard 更新 config server 元数据
数据迁移完成后,源 shard 就会向 config server 更新 chunk 对应的 shard 信息,同时也会更新 chunk 的版本信息,这样 mongos 发现本地版本更低就会主动的 reload 元数据,具体机制参考 MongoDB Sharded Cluster 路由策略。Step5:源 shard 删除 chunk 数据
chunk 迁移到目标 shard 后,源上的 chunk 就没有必要再保存了,源 shard 会将 chunk 数据删除,默认情况下源 shard 会将删除操作加入到队列,异步删除,如果 moveChunk 时,指定了 _waitForDelete 参数为 true,则同步删除完再返回。一旦源 shard 查询到目标 shard 进入到 STEADY 状态了,源 shard 就会进入临界区,测试源上的写就会排队等待。等整个迁移完了,这些等待的写操作就会继续执行,但此时 chunk 的版本号已经更新了,会告诉客户端版本过低,客户端重新从 config server 读取配置,此时拿到的路由信息里 chunk 已经在目标 shard 了,然后写会发往目标 shard。
复制集:
数据同步
Secondary 初次同步数据时,会先进行 init sync,从 Primary(或其他数据更新的 Secondary)同步全量数据,然后不断通过 tailable cursor 从 Primary 的 local.oplog.rs 集合里查询最新的 oplog 并应用到自身。
oplog: 幂等(incr 会转为 set),循环覆盖,
顺序保证:写入 oplog 前,会先加锁给 oplog 分配时间戳,并注册到未提交列表里,正式写入 oplog,在写完后,将对应的 oplog 从未提交列表里移除,在拉取 oplog 时若未提交列表为空,所有 oplog 都可读,否则只能到未提交列表最小值以前的 oplog
Secondary 拉取到一批 oplog 后,在重放这批 oplog 时,会加一个特殊的 Lock::ParallelBatchWriterMode 的锁,这个锁会阻塞所有的读请求,直到这批 oplog 重放完成
故障检测恢复
client 与复制集心跳,复制集之间心跳
复制集成员间默认每 2s 会发送一次心跳信息,如果 10s 未收到某个节点的心跳,则认为该节点已宕机;如果宕机的节点为 Primary,Secondary(前提是可被选为 Primary)会发起新的 Primary 选举。Bully 算法
每个节点都倾向于投票给优先级最高的节点(oplog 时间戳,一样谁先就谁)
优先级为 0 的节点不会主动发起 Primary 选举
当 Primary 发现有优先级更高 Secondary,并且该 Secondary 的数据落后在 10s 内,则 Primary 会主动降级,让优先级更高的 Secondary 有成为 Primary 的机会。
如果 Primary 与大多数的节点断开连接,Primary 会主动降级为 Secondary
当复制集内存活成员数量不足大多数时,整个复制集将无法选举出 Primary,复制集将无法提供写服务,处于只读状态
当 Primary 宕机时,如果有数据未同步到 Secondary,当 Primary 重新加入时,如果新的 Primary 上已经发生了写操作,则旧 Primary 需要回滚部分操作,以保证数据集与新的 Primary 一致。旧 Primary 将回滚的数据写到单独的 rollback 目录下,数据库管理员可根据需要使用 mongorestore 进行恢复。
Bully
如果 P 是最大的 ID,直接向所有人发送 Victory 消息,成功新的 Leader;否则向所有比他大的 ID 的进程发送 Election 消息
如果 P 再发送 Election 消息后没有收到 Alive 消息,则 P 向所有人发送 Victory 消息,成功新的 Leader
如果 P 收到了从比自己 ID 还要大的进程发来的 Alive 消息,P 停止发送任何消息,等待 Victory 消息(如果过了一段时间没有等到 Victory 消息,重新开始选举流程)如果 P 收到了比自己 ID 小的进程发来的 Election 消息,回复一个 Alive 消息,然后重新开始选举流程
如果 P 收到 Victory 消息,把发送者当做 Leader
部署
Secondary
Arbiter 节点只参与投票,不能被选为 Primary,并且不从 Primary 同步数据,偶数时加入
Priority0 节点的选举优先级为 0,不会被选举为 Primary
Vote0 复制集成员最多 50 个,参与 Primary 选举投票的成员最多 7 个,其他成员(Vote0)
Hidden(Vote0)可使用 Hidden 节点做一些数据备份、离线计算的任务,不会影响复制集的服务。
Delayed 节点必须是 Hidden 节点,并且其数据落后与 Primary 一段时间(错误恢复)