副本集Oplog同步原理

副本集是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 结束工作。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值