副本集是mongodb的基础组件,是实现高可用、自动选主、读写分离以及数据一致性的基础。 比较概括的说, 副本集是将同一份数据保存在不同的节点上面, 这些节点通过一致性的协议(RAFT), 实现数据的同步, 并且选出一个主节点, 该节点对外提供读写服务, 当该主节点发生故障的时候, 自动从剩余的从节点内选出新的主节点。
从这里, 我们可以看到, oplog 的整个从Primary 到Second的 sync 过程, 主要是通过SyncTail类来完成的, 其中用到几个重要的类来协助完成: BackgroundSync, OpQueueBatcher, ThreadPool。
这个的流程如下流程图:
该类的实现在: src/mongo/db/repl/bgsync.cpp.
每一个Secondary节点都会启动一个名为rsBackgroundSync的线程, 专门负责从Primary 抓取Oplog, 并且把抓取的结果放进buffer里面。
这个类主要负责的工作:
该类是SyncTail的一个子类, 主要是起一个名叫:ReplBatcher 的线程, 不断地把BackgroundSync得到的全部的oplog 集合, 分成一个个的batch进行处理, 每处理掉一个batch, BackgroundSync的buffer就被腾出了相应的空间, 可以继续从远端获得更多的oplog对象。
每个batch 最多是512M或者5000条oplog, 每当batcher 内有新的数据, 就会发返回给SyncTail, ReplBatcher线程继续准备下一个batch的数据。
每个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 & ops,
std::vector<std::vector >* 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 >& writerVectors,
OldThreadPool* writerPool,
SyncTail::MultiSyncApplyFunc func,
SyncTail* sync) {
TimerHolder timer(&applyBatchStats);
for (std::vector<std::vector >::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 & 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的时候, 需要创建一个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 boundaries) {
...
//这里_applyFunc 就是传入的MultiSyncApplyFunc指针
applyOps(writerVectors, &_writerPool, _applyFunc, this);
}
我们可以看到, 真个replay的入口在multiSyncApply, 接下来顺着代码看一下整个实现的过程:
void multiSyncApply(const std::vector & ops, SyncTail* st) {
// 对于每一个得到的oplog, 调用SyncTail::syncApply
for (std::vector ::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 结束工作。