Skip to main content

Command Palette

Search for a command to run...

mongodb rollback原理分析

Published
2 min read

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流程

image-20220717093112368

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_ROLLBACK

  • Rollback 1: 获得RollbackSource的rbid; 如果上游自己发生了rollback, 那么本节点就需要重试,找到稳定的节点进行rollback

  • Rollback 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一致;

    • 具体的修复逻辑;

      1. rollback 4.1.1 将那些被删除的collection进行回滚;通过的方式copyCollectionFromRemote,这个函数就是连接到RollbackSource,然后把整个collection都copy;
      2. rollback 4.1.2修复collection 的meta信息;采用的方式和第一步一样,从远程获得信息,然后进行修复;
      3. rollback 4.6: 删除那些需要被删除的表和索引;
      4. rollback 4.7: 开始修复数据
        1. 根据FixUpInfo.docsToRefetch获得需要被修复数据
        2. 根据这些数据的_id从远程获得对应的数据;当然这些oplog的_id可能会重复,但是没关系,mongo会通过_id进行聚合,最后就变成一条;
        3. 遍历上面的id,首先通过这个id获得本地节点的数据,然后将这个数据写入到rollback的文件;也就是每次rollback以后留下的文件;这个文件存放的是当时需要被回滚的数据的当时数据;
        4. 根据远程获得数据来进行修复
          • 如果远程没有这个数据,那么就直接本地删除;使用内部的接口Helpers来进行删除;这个操作过程是没有产生oplog; 如果是capped有着不一样的处理方式;
          • 如果远程有这个数据的话,就进行upsert的方式来进行更新;
  • Rollback 5 - 6: 截断本地的oplog并且更新本地的last optimes from oplog; 当此为止,rollback就结束了;

3. 总结

Rollback的流程相对比较简单,花了2天时间专门看了代码;这次主要是要帮同事review相关代码所以顺便看了这部分代码,我之前对这块也很好奇;rollback的过程结束之后需要同步oplog,在同步完成之前,理论上当前节点是不能被访问的,因为当前的节点属于不一致的状态。

mongo这种rollback的本质的点在于oplog的幂等和恢复以remote为主,通过这种方式可以保证最终状态是一致的;好了rollback分析到此为主;下一篇文章可能是最近查网络延迟的经验;

More from this blog

Ai时代的工具链

本周是black Friday,我订阅了几个AI服务,还是蛮贵的...不过这样基本上构成我目前整体的知识阅读的过程,随着Ai的不断发展,工具链的替换可能是很重要的一个过程的。我主要订购了以下几个工具: Memo: 这个工具的主要作用是将视频/audio转srt,并且带有ai翻译的工具;当然我觉得它做的非常好的是,它把整个链路做的非常好的,并且可以用本地的资源做audio->text;而且它自带了很多的ai功能,比如对字幕进行进一步的AI的处理,提问,summarize和思维导图等等;目前我主要...

Nov 30, 20251 min read

做了一个噩梦

今天凌晨4点多起来看了一眼丈母娘的发烧是否ok...就导致我有点睡不着的,刷了一会推特之后又开始睡觉了,于是就开始做了一个很可怕的梦。 噩梦 那天,我不知道是在哪里..我带着女儿和我弟出去玩的,貌似是一个风景山区。于是我就带着女儿和弟弟出去玩的;我们走啊走, 沿着一条路一直走..突然看到一个小道有一家饭店的,这个饭店是比较特殊,有很多海鲜的;我看上了一只大龙虾,我问多少钱的,他说大概就70rmb就可以的。。。我觉得很划算的,我心想:我买下来,到时候把老婆叫过来一起吃的,并且告诉她这个才70rmb...

Nov 24, 20251 min read

子女教育-2

下面我分享一个推特上的一个关于子女教育的推 哈哈哈哈,李诞这个视频我看过 我给你分享几个我和我女儿之间的小故事 第一个故事 我经常给小朋友说:你们现在上学的成绩不重要,你们现在数学考试都是语文脑筋急转弯,语文考试都是历史背诵,一点用都没有,你出了社会就知道,社会根本没有选择题,社会要有选择题就好了,最难的是你遇到困难,你连门都找不到。我第一次这样讲的时候是女儿小学4年级,那时候我女儿听的一愣一愣的,她不明白,但是觉得我的理论和学校的不一样,很狂妄,但是她很喜欢,哈哈哈哈。 她什么时候真正明...

Nov 13, 20251 min read

被诈骗-马来西亚

最近我在国内,我老婆在马来;最近在计划搬家的,找的那个房子不包含一些必要的家具,于是我老婆就必须要买点家具的,主要是沙发和餐桌..我们本来计划是说去ikea去买,但是我老婆觉得ikea的家具不便宜,并且款式一般的,最终问了中介找了一个二手平台找找看不错的家具。 我老婆挑了两个家具的,我看了一下价格也不算便宜的,但是我老婆喜欢的,于是我就说你觉得ok那就购买吧。我还顺便问了一下,这个家具能不能线下看一下货的,但是我老婆说这货在很远的地方的,大概是300公里的一个城市的。那我就说这个包邮吗,我老婆说...

Nov 13, 20251 min read

当下和最近想做的事情

1. Current 当下 最近依然还在中国,已经回来快一个月了. 最近一直在忙着带丈母娘看病和住院的。索性一切都还在可控范围内的,丈母娘由于糖尿病控制的很差导致本身的冠心病也复发. 这次去浙江省人民医院去做了造影检查和支架植入的手术的,不过这一切都比我预估的要顺利,我就怕她由于长时间没吃药和高血糖的持续的时间太长了,会带来严重的问题,不过好在没有发生最坏的事情的。 因为做了手术,所以这段时间我和我老婆的姐姐每人轮换的陪床,不过陪床真的好累的,因为睡得很不好的,特别的累。不过好在都结束了,而且丈...

Nov 9, 20251 min read

Keep Move - 永不止步

39 posts