副本集Oplog同步原理

MongoDB副本集原理

副本集是mongodb的基础组件,是实现高可用、自动选主、读写分离以及数据一致性的基础。 比较概括的说, 副本集是将同一份数据保存在不同的节点上面, 这些节点通过一致性的协议(RAFT), 实现数据的同步, 并且选出一个主节点, 该节点对外提供读写服务, 当该主节点发生故障的时候, 自动从剩余的从节点内选出新的主节点。

本文主要针对副本集的整体架构进行分析, 先来看一下架构图:
这里写图片描述

从这里, 我们可以看到, oplog 的整个从Primary 到Second的 sync 过程, 主要是通过SyncTail类来完成的, 其中用到几个重要的类来协助完成: BackgroundSync, OpQueueBatcher, ThreadPool。
这个的流程如下流程图:
这里写图片描述

BackgroundSync 类

该类的实现在: src/mongo/db/repl/bgsync.cpp.
每一个Secondary节点都会启动一个名为rsBackgroundSync的线程, 专门负责从Primary 抓取Oplog, 并且把抓取的结果放进buffer里面。
这个类主要负责的工作:

  • 创建从secondary 到Primary的连接类: OplogReader;
  • 更新记录当前抓取的oplog的时间点lastOpTimeFetched等;
  • 创建Fetcher 类来调用Remote Command 抓取oplog;
  • 调用getMore 命令从oplog collection 获得下一批的oplog;

OpQueueBatcher 类

该类是SyncTail的一个子类, 主要是起一个名叫:ReplBatcher 的线程, 不断地把BackgroundSync得到的全部的oplog 集合, 分成一个个的batch进行处理, 每处理掉一个batch, BackgroundSync的buffer就被腾出了相应的空间, 可以继续从远端获得更多的oplog对象。
每个batch 最多是512M或者5000条oplog, 每当batcher 内有新的数据, 就会发返回给SyncTail, ReplBatcher线程继续准备下一个batch的数据。

SyncTail类

每个Secondary节点都有一个名为: rsSync的线程, 它负责得到并且更新本地的oplog。
它会检查本地的oplog状况, 决定是要全量同步oplog, 还是增量同步。
增量同步的代码在: src/mongo/db/repl/sync_tail.cpp。

SyncTail从OpQueueBatcher得到OpQueue 类型的ops, 需要把他们进行replay, 但是, 如果使用当前线程执行的话, 有可能执行的时间很长。
这里, SyncTail 通过ThreadPool的方式, 一次创建16个线程, 并行的replay这些oplog。那么, 这些的oplog是怎么分配到不同的线程里面的? 它是通过oplog的namespace。

void fillWriterVectors(OperationContext* txn,
                       const std::deque<SyncTail::OplogEntry>& ops,
                       std::vector<std::vector<BSONObj>>* writerVectors) {
    const bool supportsDocLocking =
        getGlobalServiceContext()->getGlobalStorageEngine()->supportsDocLocking();
    const uint32_t numWriters = writerVectors->size();

    Lock::GlobalRead globalReadLock(txn->lockState());

    CachingCappedChecker isCapped;

    for (auto&& op : ops) {
        StringMapTraits::HashedKey hashedNs(op.ns);
        uint32_t hash = hashedNs.hash();

        const char* opType = op.opType.rawData();
        if (supportsDocLocking && isCrudOpType(opType) && !isCapped(txn, hashedNs)) {
            BSONElement id;
            switch (opType[0]) {
                case 'u':
                    id = op.o2.Obj()["_id"];
                    break;
                case 'd':
                case 'i':
                    id = op.o.Obj()["_id"];
                    break;
            }

            const size_t idHash = BSONElement::Hasher()(id);
            MurmurHash3_x86_32(&idHash, sizeof(idHash), hash, &hash);
        }

        (*writerVectors)[hash % numWriters].push_back(op.raw);
    }
}

oplog分配到不同的线程以后, 就是replay该线程分配到的oplog, replay oplog的实现过程在SyncTail::MultiSyncApplyFunc里面, 我们后面会用单独一小节来讨论。

void applyOps(const std::vector<std::vector<BSONObj>>& writerVectors,
              OldThreadPool* writerPool,
              SyncTail::MultiSyncApplyFunc func,
              SyncTail* sync) {
    TimerHolder timer(&applyBatchStats);
    for (std::vector<std::vector<BSONObj>>::const_iterator it = writerVectors.begin();
         it != writerVectors.end();
         ++it) {
        if (!it->empty()) {
            writerPool->schedule(func, stdx::cref(*it), sync);
        }
    }
}

执行完oplog, 我们需要把oplog更新到本地的oplog 集合里面。

OpTime writeOpsToOplog(OperationContext* txn, const std::vector<BSONObj>& ops) {
    ReplicationCoordinator* replCoord = getGlobalReplicationCoordinator();

    OpTime lastOptime;
    MONGO_WRITE_CONFLICT_RETRY_LOOP_BEGIN {
        lastOptime = replCoord->getMyLastAppliedOpTime();
        invariant(!ops.empty());
        ScopedTransaction transaction(txn, MODE_IX);
        Lock::DBLock lk(txn->lockState(), "local", MODE_X);

        if (_localOplogCollection == 0) {
            OldClientContext ctx(txn, rsOplogName);

            _localDB = ctx.db();
            verify(_localDB);
            _localOplogCollection = _localDB->getCollection(rsOplogName);
            massert(13389,
                    "local.oplog.rs missing. did you drop it? if so restart server",
                    _localOplogCollection);
        }

        OldClientContext ctx(txn, rsOplogName, _localDB);
        WriteUnitOfWork wunit(txn);

        checkOplogInsert(
            _localOplogCollection->insertDocuments(txn, ops.begin(), ops.end(), false));
        lastOptime =
            fassertStatusOK(ErrorCodes::InvalidBSON, OpTime::parseFromOplogEntry(ops.back()));
        wunit.commit();
    }
    MONGO_WRITE_CONFLICT_RETRY_LOOP_END(txn, "writeOps", _localOplogCollection->ns().ns());

    return lastOptime;
}

oplog的replay过程

在同步oplog的时候, 需要创建一个SyncTail对象, 来进行增量同步。

void runSyncThread() {
      ...
      SyncTail tail(BackgroundSync::get(), multiSyncApply);
      tail.oplogApplication();
 }

SyncTail 的构造函数, 需要传入一个函数指针MultiSyncApplyFunc, 它是用来replay oplog的callback 函数。
SyncTail::SyncTail(BackgroundSyncInterface* q, MultiSyncApplyFunc func);

OpTime SyncTail::multiApply(OperationContext* txn,
                              const OpQueue& ops,
                              boost::optional<BatchBoundaries> boundaries) {
    ...
    //这里_applyFunc 就是传入的MultiSyncApplyFunc指针
    applyOps(writerVectors, &_writerPool, _applyFunc, this);
}

我们可以看到, 真个replay的入口在multiSyncApply, 接下来顺着代码看一下整个实现的过程:

void multiSyncApply(const std::vector<BSONObj>& ops, SyncTail* st) {
   // 对于每一个得到的oplog, 调用SyncTail::syncApply 
   for (std::vector<BSONObj>::const_iterator it = ops.begin(); it != ops.end(); ++it) {
         const Status s = SyncTail::syncApply(&txn, *it, convertUpdatesToUpserts);
   }
}

Status SyncTail::syncApply(OperationContext* txn,
                             const BSONObj& op,
                             bool convertUpdateToUpsert,
                             ApplyOperationInLockFn applyOperationInLock,
                             ApplyCommandInLockFn applyCommandInLock,
                             IncrementOpsAppliedStatsFn incrementOpsAppliedStats) {
     if (isCommand) {
          Status status = applyCommandInLock(txn, op);
     }
     if (isCrudOpType(opType)) {
         applyOperationInLock(txn, db, op, convertUpdateToUpsert);
     }
}

这里, oplog的replay分为两类: command和一般的增删改查。具体的实现oplog.cpp里面。
command的oplog回放是将当前支持的command以及对应的处理函数放进一个map里面, 通过每一个oplog的类型找到相应的处理函数:

std::map<std::string, ApplyOpMetadata> opsMap = {
    {"create",
     {[](OperationContext* txn, const char* ns, BSONObj& cmd)
          -> Status { return createCollection(txn, NamespaceString(ns).db().toString(), cmd); },
      {ErrorCodes::NamespaceExists}}},
    {"collMod",
     {[](OperationContext* txn, const char* ns, BSONObj& cmd) -> Status {
         BSONObjBuilder resultWeDontCareAbout;
         return collMod(txn, parseNs(ns, cmd), cmd, &resultWeDontCareAbout);
     }}},
    {"dropDatabase",
     {[](OperationContext* txn, const char* ns, BSONObj& cmd)
          -> Status { return dropDatabase(txn, NamespaceString(ns).db().toString()); },
      {ErrorCodes::NamespaceNotFound}}},
      ...
}
Status applyCommand_inlock(OperationContext* txn, const BSONObj& op) {
    while (!done) {
        auto op = opsMap.find(o.firstElementFieldName());
        if (op == opsMap.end()) {
            return Status(ErrorCodes::BadValue,
                          mongoutils::str::stream() << "Invalid key '" << o.firstElementFieldName()
                                                    << "' found in field 'o'");
        }
        ApplyOpMetadata curOpToApply = op->second;
        Status status = Status::OK();
        try {
            status = curOpToApply.applyFunc(txn, ns, o);
        } catch (...) {
            status = exceptionToStatus();
        }
    }
 }

另外一种增删改查的oplog relay, 处理了index, insert, update, create的场景, 代码不复杂, 在applyOperation_inlock里面, 可以自己查看。

// See replset initial sync code.
Status applyOperation_inlock(OperationContext* txn,
                             Database* db,
                             const BSONObj& op,
                             bool convertUpdateToUpsert) {
}

到这里, 一次oplog的更新就完成了, 循环上面的过程, 我们就可以把primary上面新的oplog更新到secondary节点上。 当primary掉线或者故障, 某个节点从secondary变为primary的时候, BackgroundSync 停止sync oplog, 并且通知Synctail 结束工作。

<think>我们正在回答用户关于MongoDB副本工作原理的问题。根据提供的引用,特别是引用[2]和引用[4],我们可以深入解析副本的工作机制。副本MongoDB实现高可用性的核心机制,它通过数据复制和自动故障转移来确保数据的持久性和服务的连续性。步骤:1.副本架构概述2.数据同步机制(初始同步和持续复制)3.选举机制4.故障转移过程5.读写关注(ReadandWriteConcerns)与副本的关系根据引用[2]和[4],我们将详细说明这些部分。注意:用户要求深入解析,因此需要详细且准确。引用[2]提到副本的基本组成:一个副本包含多个节点,其中一个是主节点(Primary),其余是从节点(Secondary)。此外,还可以有仲裁节点(Arbiter),它不存储数据,只参与选举。引用[4]提供了关于数据同步的详细文档链接,包括初始同步和持续复制(通过oplog)。我们将按照以下结构组织回答:1.副本架构2.数据同步过程(初始同步和持续复制)3.选举机制(使用Raft协议变体)4.故障转移过程5.读写关注同时,我们将生成相关问题。注意:所有数学表达式(如果有)需要使用$...$或$$...$$格式,但本问题中可能较少涉及数学表达式。如果出现,比如在描述选举超时时间等,会正确使用。由于用户要求使用中文,我们使用中文回答。开始回答:</think>### MongoDB 副本工作原理深入解析 MongoDB 副本通过**数据复制**和**自动故障转移**实现高可用性,其核心架构包含三类节点: 1. **主节点 (Primary)** 处理所有写操作,并将操作记录到 oplog(操作日志)中。 示例:客户端写入 `{name: "Alice"}` → 主节点接收并记录到 oplog。 2. **从节点 (Secondary)** 异步复制主节点的 oplog,重放操作以保持数据同步。 公式:$ \text{Secondary数据} = \text{Primary初始状态} + \sum \text{oplog操作} $ 3. **仲裁节点 (Arbiter)** 不存储数据,仅在选举中投票(避免脑裂)[^2]。 #### 关键机制详解 1. **数据同步流程** - **初始同步**:新节点加入时,全量拷贝主节点数据并应用期间产生的 oplog。 - **持续复制**: - 从节点拉取主节点 oplog(类似环形队列) - 重放操作(例如 `insert`, `update`) - 延迟公式:$ \text{复制延迟} = t_{\text{oplog生成}} - t_{\text{重放}} $ 引用文档显示,oplog 大小需合理配置以避免同步中断[^4]。 2. **选举机制 (Raft协议变体)** - **触发条件**:主节点失联(心跳超时,默认10秒) - **选举过程**: 1. 从节点发起选举请求 2. 获得大多数节点投票(含仲裁节点) 3. 新主节点需数据最新(通过 `term + index` 比较) - 公式:$ \text{选举成功条件} = \text{投票数} \geq \left\lfloor \frac{N}{2} \right\rfloor + 1 $($N$为总节点数) 3. **故障转移流程** ```mermaid graph LR A[主节点宕机] --> B[从节点检测心跳超时] B --> C[发起选举] C --> D{获得多数投票?} D -->|是| E[新主节点接管] D -->|否| F[重新选举] ``` 整个过程通常在 10-30 秒内完成[^2]。 4. **读写一致性控制** - **写关注 (Write Concern)**:指定写操作需复制到N个节点才返回成功 `db.orders.insert({item: "book"}, {w: "majority"})` - **读偏好 (Read Preference)**: - `primary`(默认读主节点) - `secondary`(读从节点,可能读到旧数据) #### 设计优势与挑战 - **优势**: - 自动故障转移保障服务连续性 - 数据冗余防止单点故障 - **挑战**: - 异步复制导致短暂数据不一致 - 网络分区可能触发无效选举(需配置奇数节点)[^4] --- ### 相关问题 1. oplog 大小如何影响副本同步效率?如何合理配置 oplog? 2. 网络分区时副本如何避免脑裂问题? 3. 副本与分片群在架构设计上有何本质区别? 4. 如何通过读写关注配置平衡一致性与性能? [^1]: MongoDB 核心架构设计解析 [^2]: 副本数据同步机制研究 [^3]: 分片群的可扩展性实现 [^4]: 副本复制原理与配置文档
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值