mongodb rollback原理分析
1. mongodb 为何会存在Rollback呢?
开始维护mongo的时候,总是很好奇为什么会存在rollback机制;理论上类raft协议的共识算法,在没有达到majority同意的情况下是不可能将日志应用到状态机中的,即节点的真实的底层数据是不会被修改的;按照这个逻辑,即使出现primary当宕机什么的,最多不过是共识算法同步的log的回滚,也没必要进行数据层面的回滚。
这个问题其实伴随了我不少时间,我一直都没有很明白的搞定这个问题,甚至于我有一段时间认为可能是因为我们线上是非majority写,所以才导致rollback; 直到我翻译了关于mongo的一篇论文之后,我才发现了问题的根本点;mongo的设计中流程上有点一些变化,导致rollback的产生,有导致了rollback上保证它数据一致性的一个必不可少的一个模块。最为主要的变化在于: 日志在达到majority同意之前会提前apply到本地的存储引擎中,当主从切换之后,回滚oplog就可能需要回滚存储引擎中的数据,因为它已经被应用到了底层数据中;即使client使用了majority写也并不能避免rollback,但是这个时候的rollback文件可以直接删除,因为majority保证了给用户回复写成功,那么这条数据必然是不会被丢弃的;
日常环境中,如果client写majority的话,理论上出现rollback都是无效的数据,直接删除即可;当然目前我公司的一些场景中是双写,所以就导致每次出现rollback的时候都需要SRE去处理这些,因为这中间可能包含了给用户回复是写成功的,但是majority写还没完成,这个时候出现primary宕机的话,就需要把那部分给用户写进去的数据再次回写回去;这个过程的确比较烦,尤其是对于sre来说,需要从业务的审计日志中去找出这条日志是否写成功了,如果写成功了那就插回去.
2. rollback流程

rollback的过程其实相对比较简单的,基本上的流程就是找到与RollbackSource的oplog共同点,然后根据oplog对数据进行修复的过程,比较复杂的还是在于oplog的修复过程,因为oplog中不仅仅包含有数据、还有collection的更改,所以不需要分别进行处理. 上图基本上是rollback的几个关键步骤,分别为:
- 获得remote rbid;这个比较重要,需要确认远程节点在我rollback过程中,自己也进行rollback操作;
- 获得oplog的公共点; 通过比较remote和local的oplog来找到本地需要回滚哪些oplog,并且进行分类;
- 恢复数据,依据就是从第二步获得的需要修复的数据;
- 截断oplog并且本地的applied optime;
- rollback已经ok,将节点的状态从
rollback-->recovering,后期就是进行oplog同步追上source节点即可;
2.1 源码分析
- 核心文件:
rs_rollback.cpp/h - 开始函数:
rollback
2.1.1 oplog
mongo副本集同步的核心是oplog,mongo的非primary节点通过拉取同步源oplog来进行数据同步,并且将oplog apply到本地的就完成数据同步,那么oplog的结构是怎么样的呢?
struct OplogEntry {
StringData ns = ""; //db.coll
StringData opType = ""; //op的类型
BSONElement version; // 版本,应该是oplog的版本信息
BSONElement o; // 被操作完之后的数据,也就是最后数据的存在状态
BSONElement o2; // 只有update的才会有效果,通常存放的是where的条件
BSONElement ts; // opTime,包含时间和顺序
}
一条真实的oplog的样子如下:
{
"ts": Timestamp(1655681357,276), // oplog的id,包含有时间+同一个秒内的递增id
"t": NumberLong(4), //任期
"h": NumberLong("6969824861434590453"), //hash字段,OpTimeWithHash,带有一个hash字段,
"v": 2,
"op": "u",
"ns": "xxx.xxx",
"o2": {
"_id": "xxxxxxx.xxxxx"
},
"o": {
"_id": "xxxxxxx.xxxxx",
"del": NumberLong(1657036800),
"size": NumberLong(86071),
"md5": BinData(0,
"xxxxxx"),
"ip": "127.0.0.1"
}
}
Oplog对应的op有这么一些:
op 的值:
i 表示 insert ,
u 表示 update,
d 表示 delete,
c 表示的是 db cmd, db 表示声明当前数据库 (其中ns 被设置成为=>数据库名称+ '.'),
n 表示 noop,,即空操作,其会定期执行以确保时效性
OplogEntry和真实的oplog有零星的区别,不过OplogEntry是从oplog这个对象中序列化出来的;mongo为了保证oplog的应用是幂等的,采用的方式是所有的操作都以最终状态为主;比如insert,从primary同步到secondary的过程中,其实o这个对象中会将_id也就是唯一id也带过来,这样如果secondary之前已经有这条数据的话,那么insert是不会成功的;通过这样的方式,即使多次执行同一条oplog也能保证最终状态是一致的;这个功能在后期恢复数据的时候也是很有用的;d
2.1.2 rollback细节
Rollback 0: 将节点状态设置成RS_ROLLBACKRollback 1: 获得RollbackSource的rbid; 如果上游自己发生了rollback, 那么本节点就需要重试,找到稳定的节点进行rollbackRollback 2-3: 获得oplog的公共位置; 主要函数为syncRollBackLocalOperations,整体过程如下:寻找共同点的方式为: 1.获得remote source的oplog的最新oplog 2.本地oplog中找找看是否存在; 通过的方式就是比较Optime的大小 3.不存在就让remote source oplog的往前移动,继续上面的过程 4.找到了就返回结果 上面的逻辑核心函数为:RollBackLocalOperations::onRemoteOperation通过在寻找公共oplog的过程中,收集到了哪些oplog是需要被rollback的,mongo需要对这些oplog进行分类,因为不同的类型处理的逻辑是不一致的;代码核心为:
struct FixUpInfo { // note this is a set -- if there are many $inc's on a single document we need to rollback, // we only need to refetch it once. std::set<DocID> docsToRefetch; //真实需要被回滚的数据部分 // Key is collection namespace. Value is name of index to drop. std::multimap<std::string, std::string> indexesToDrop; //索引要被删除的 std::set<std::string> collectionsToDrop; //collection要被删除的 std::set<std::string> collectionsToResyncData; // coll需要被恢复的 std::set<std::string> collectionsToResyncMetadata; //coll的meta数据需要被恢复的 OpTime commonPoint; RecordId commonPointOurDiskloc; int rbid; // remote server's current rollback sequence # void removeAllDocsToRefetchFor(const std::string& collection); void removeRedundantOperations(); };这部分的数据的生成的函数为
rollback_internal::updateFixUpInfoFromLocalOplogEntry;这个函数的主要逻辑是通过op来对oplog的rollback进行分类,主要是需要识别op==c这个oplog的操作;Rollback 4: 修复数据;mongo是如何去恢复数据呢?
本来我自己的猜测,可能是通过快照的方式都
RollbackSource进行读,然后进行修复;但是mongo并没有这么做,基本的逻辑是通过_id去远程查一下,要删除的删除,要更新和插入的都以远程为主;也就是所有的都以远程为主;这个地方会比较奇怪,因为按照这种方式的话,修复的数据不一定是对的,因为你现在去远程读过来的数据是现在这个时候的状态;但是mongo这么做的原因就是在于oplog的幂等性,等到后期节点开始处于RS_RECOVERING状态的时候,会通过oplog,通过oplog的过程会慢慢将数据修复到最终形态,如果oplog中存在插入数据,但是这条数据已经存在在数据库中,可能我修复的时候做的,但是没关系,只要有就行,后来的oplog会慢慢在恢复,直到和RollbackSource一致;具体的修复逻辑;
rollback 4.1.1将那些被删除的collection进行回滚;通过的方式copyCollectionFromRemote,这个函数就是连接到RollbackSource,然后把整个collection都copy;rollback 4.1.2修复collection 的meta信息;采用的方式和第一步一样,从远程获得信息,然后进行修复;rollback 4.6: 删除那些需要被删除的表和索引;rollback 4.7: 开始修复数据- 根据
FixUpInfo.docsToRefetch获得需要被修复数据 - 根据这些数据的
_id从远程获得对应的数据;当然这些oplog的_id可能会重复,但是没关系,mongo会通过_id进行聚合,最后就变成一条; - 遍历上面的id,首先通过这个id获得本地节点的数据,然后将这个数据写入到rollback的文件;也就是每次rollback以后留下的文件;这个文件存放的是当时需要被回滚的数据的当时数据;
- 根据远程获得数据来进行修复
- 如果远程没有这个数据,那么就直接本地删除;使用内部的接口
Helpers来进行删除;这个操作过程是没有产生oplog; 如果是capped有着不一样的处理方式; - 如果远程有这个数据的话,就进行upsert的方式来进行更新;
- 如果远程没有这个数据,那么就直接本地删除;使用内部的接口
- 根据
Rollback 5 - 6: 截断本地的oplog并且更新本地的last optimes from oplog; 当此为止,rollback就结束了;
3. 总结
Rollback的流程相对比较简单,花了2天时间专门看了代码;这次主要是要帮同事review相关代码所以顺便看了这部分代码,我之前对这块也很好奇;rollback的过程结束之后需要同步oplog,在同步完成之前,理论上当前节点是不能被访问的,因为当前的节点属于不一致的状态。
mongo这种rollback的本质的点在于oplog的幂等和恢复以remote为主,通过这种方式可以保证最终状态是一致的;好了rollback分析到此为主;下一篇文章可能是最近查网络延迟的经验;