# mongodb rollback原理分析

### 1. mongodb 为何会存在`Rollback`呢？

开始维护mongo的时候，总是很好奇为什么会存在rollback机制；理论上类raft协议的共识算法，在没有达到majority同意的情况下是不可能将日志应用到状态机中的，即节点的真实的底层数据是不会被修改的；按照这个逻辑，即使出现primary当宕机什么的，最多不过是共识算法同步的log的回滚，也没必要进行数据层面的回滚。

这个问题其实伴随了我不少时间，我一直都没有很明白的搞定这个问题，甚至于我有一段时间认为可能是因为我们线上是非majority写，所以才导致`rollback`; 直到我翻译了[关于mongo的一篇论文](https://nomoshen.com/fault-tolerant-replication-with-pull-based-consensus-in-mongodb)之后，我才发现了问题的根本点；mongo的设计中流程上有点一些变化，导致rollback的产生，有导致了rollback上保证它数据一致性的一个必不可少的一个模块。最为主要的变化在于: **日志在达到majority同意之前会提前apply到本地的存储引擎中**，当主从切换之后，回滚oplog就可能需要回滚存储引擎中的数据，因为它已经被应用到了底层数据中；即使`client`使用了`majority`写也并不能避免rollback，但是这个时候的rollback文件可以直接删除，因为majority保证了给用户回复写成功，那么这条数据必然是不会被丢弃的；

日常环境中，如果client写majority的话，理论上出现rollback都是无效的数据，直接删除即可；当然目前我公司的一些场景中是双写，所以就导致每次出现rollback的时候都需要SRE去处理这些，因为这中间可能包含了给用户回复是写成功的，但是majority写还没完成，这个时候出现primary宕机的话，就需要把那部分给用户写进去的数据再次回写回去；这个过程的确比较烦，尤其是对于sre来说，需要从业务的审计日志中去找出这条日志是否写成功了，如果写成功了那就插回去.

### 2. rollback流程

![image-20220717093112368](https://cdn.nomoshen.com/typora_img/image-20220717093112368.png)

`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的结构是怎么样的呢？

```c++
struct OplogEntry {
    StringData ns = ""; //db.coll
    StringData opType = ""; //op的类型

    BSONElement version; // 版本,应该是oplog的版本信息
    BSONElement o; // 被操作完之后的数据，也就是最后数据的存在状态
    BSONElement o2; // 只有update的才会有效果，通常存放的是where的条件
    BSONElement ts; // opTime，包含时间和顺序
}
```

一条真实的oplog的样子如下:

```json
{
    "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`有这么一些:

```json
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进行分类，因为不同的类型处理的逻辑是不一致的；代码核心为：

    ```C++
    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分析到此为主；下一篇文章可能是最近查网络延迟的经验；
