mongodb系统分析

背景

raft算法

在Raft中,每个结点会处于follower、candidate、leader三种状态中的一种:
follower

所有结点都以follower的状态开始。如果没收到leader消息则会变成candidate状态

candidate

会向其他结点“拉选票”,如果得到大部分的票则成为leader。这个过程就叫做Leader选举(Leader Election)

leader

所有对系统的修改都会先经过leader。每个修改都会写一条日志(log entry)。leader收到修改请求后的过程如下,这个过程叫做日志复制(Log Replication):

  1. 复制日志到所有follower结点(replicate entry)。
  2. 大部分结点响应时才提交日志。
  3. 通知所有follower结点日志已提交。
  4. 所有follower也提交日志。
  5. 现在整个系统处于一致的状态。

leader选举


votedFor是server保存的投票对象,一个server在一个term内只能投一次票。如果此时已经投过票了,即votedFor就不为空,那么此时就可以直接拒绝当前的投票(当然还要检查votedFor是不是就是请求的candidate)。

如果没有投过票:则对比candidate的log和当前server的log哪个更新,比较方式为谁的lastLog的term越大谁越新,如果term相同,谁的lastLog的index越大谁越新。

candidate统计投票信息,如果过半同意了则认为自己当选了leader,转变成leader状态,如果没有过半,则等待是否有新的leader产生,如果有的话,则转变成follower状态,如果没有然后超时的话,则开启下一次的选举。

  1. Raft对选举流程加了一些限制,只有最新的Server才能被选举为Leader,因此,这种机制保证了Leader拥有所有在上一个任期内达成一致的日志,Leader不需要从全局达成一致的日志中去学习那些它自身没有的日志。因此当一个Server成为Leader后,它可以立即对外服务,比如向其他Server发起一致性提案。

    Raft对比不同Server中最后一条log记录的Index和Term来决定是否拥有最新的决议。如果两条log的Term不相同,则Term较大者胜出。如果两条log的Term相同,则Index较大者胜出。

    扫描二维码关注公众号,回复: 1645729 查看本文章
  2. Paxos允许任何Server被选举为Leader. 在Paxos中当一个Server成为Leader后,它需要先向其它Server学习自己所没有的已达成一致的日志,然后才能对外提供服务,paxos协议的这种灵活性带来了额外的复杂度。

log复制

client请求到达leader

leader首先将该请求转化成entry,然后添加到自己的log中,得到该entry的index信息,entry中就包含了当前leader的term信息和在log中的index信息。

leader复制上述entry到所有follower

一个leader在刚开始的时候会初始化:
nextIndex=leader的log的最大index+1
matchIndex=0

然后开始准备AppendEntries RPC请求的参数:
prevLogIndex=nextIndex-1
prevLogTerm=从log中得到上述prevLogIndex对应的term

然后开始准备entries数组信息,从leader的log的prevLogIndex+1开始到lastLog,此时是空的,
然后把leader的commitIndex作为参数传给leaderCommit:
leaderCommit=commitIndex
至此,所有参数准备完毕,发送RPC请求到所有的follower,follower再接收到这样的请求之后,处理如下:

  1. 重置HeartbeatTimeout
  2. 检查传过来的请求term和当前follower的term
  3. Reply false if term < currentTerm
    检查prevLogIndex和prevLogTerm和当前follower的对应index的log是否一致。
  4. Reply false if log doesn’t contain an entry at prevLogIndex whose term matches prevLogTerm
    这里可能就是不一致的,因为初始prevLogIndex和prevLogTerm是leader上log的lastLog,不一致的话返回false,同时将该follower上log的lastIndex传送给leader。
  5. leader接收到上述false之后,会记录该follower的上述lastIndex:
    macthIndex=上述lastIndex
    nextIndex=上述lastIndex+1
    然后leader会从新按照上述规则,发送新的prevLogIndex、prevLogTerm、和entries数组
  6. follower检查prevLogIndex和prevLogTerm和对应index的log是否一致(目前一致了),然后follower就开始将entries中的数据全部覆盖到本地对应的index上,如果没有则算是添加如果有则算是更新,也就是说和leader的保持一致。
  7. 最后follower将最后复制的index发给leader,同时返回ok,leader会像上述一样来更新follower的macthIndex。

leader统计过半复制的entries
leader一旦发现有些entries已经被过半的follower复制了,则就将该entry提交,将commitIndex提升至该entry的index。(这里是按照entry的index先后顺序提交的),具体的实现可以通过follower发送过来macthIndex来判定是否过半了。
一旦可以提交了,leader就将该entry应用到状态机中,然后给客户端回复OK
然后在下一次heartBeat心跳中,将commitIndex就传给了所有的follower,对应的follower就可以将commitIndex以及之前的entry应用到各自的状态机中了。

安全性

选举约束

被选举出来的leader必须要包含所有已经比提交的entries,只要当前server的log比半数server的log都新就可以被选举为leader,这里的新就是上述说的:
谁的lastLog的term越大谁越新,如果term相同,谁的lastLog的index越大谁越新

当前term的leader不能“直接”提交之前term的entries

当前term的leader不能“直接”提交之前term的entries,必须要等到当前term有entry过半了,才顺便一起将之前term的entries进行提交。

系统初始化

通过宏定义MONGO_INITIALIZER_WITH_PREREQUISITES注册系统初始化函数

MONGO_INITIALIZER_WITH_PREREQUISITES(CreateReplicationManager,
                                     ("SetGlobalEnvironment", "SSLManager"))
(InitializerContext* context) {
    ... ...
}
#define MONGO_INITIALIZER_WITH_PREREQUISITES(NAME, PREREQUISITES) \
    MONGO_INITIALIZER_GENERAL(NAME, PREREQUISITES, MONGO_NO_DEPENDENTS)
#define _MONGO_INITIALIZER_FUNCTION_NAME(NAME) _mongoInitializerFunction_##NAME
#define MONGO_INITIALIZER_GENERAL(NAME, PREREQUISITES, DEPENDENTS)                        \
    ::mongo::Status _MONGO_INITIALIZER_FUNCTION_NAME(NAME)(::mongo::InitializerContext*); \
    namespace {                                                                           \
    ::mongo::GlobalInitializerRegisterer _mongoInitializerRegisterer_##NAME(              \
        #NAME,                                                                            \
        _MONGO_INITIALIZER_FUNCTION_NAME(NAME),                                           \
        MONGO_MAKE_STRING_VECTOR PREREQUISITES,                                           \
        MONGO_MAKE_STRING_VECTOR DEPENDENTS);                                             \
    }                                                                                     \
    ::mongo::Status _MONGO_INITIALIZER_FUNCTION_NAME(NAME)
GlobalInitializerRegisterer::GlobalInitializerRegisterer(
    const std::string& name,  const InitializerFunction& fn,
    const std::vector<std::string>& prerequisites,
    const std::vector<std::string>& dependents) {
    Status status = getGlobalInitializer().getInitializerDependencyGraph().addInitializer(
        name, fn, prerequisites, dependents);
    if (Status::OK() != status) {
        ::abort();
    }
}

调用注册的初始化函数

static int mongoDbMain(int argc, char* argv[], char** envp) {
     ... ...
     Status status = mongo::runGlobalInitializers(argc, argv, envp);
     ... ...
}
Status runGlobalInitializers(const InitializerContext::ArgumentVector& args,
                             const InitializerContext::EnvironmentMap& env) {
    return getGlobalInitializer().execute(args, env);
}
Status runGlobalInitializers(int argc, const char* const* argv, const char* const* envp) {
    ... ...
    return runGlobalInitializers(args, env);
}
Status Initializer::execute(const InitializerContext::ArgumentVector& args,
                            const InitializerContext::EnvironmentMap& env) const {
    std::vector<std::string> sortedNodes;
    Status status = _graph.topSort(&sortedNodes);   //InitializerDependencyGraph _graph;
    InitializerContext context(args, env);
    for (size_t i = 0; i < sortedNodes.size(); ++i) {
        InitializerFunction fn = _graph.getInitializerFunction(sortedNodes[i]);
        ... ...
        status = fn(&context);
        ... ...
    }
    ... ...
}

Sharding

cammands

db.runCommand({ addshard:”replset_xxx/HostAndPort”, name:”shard_xxx”, maxsize: 20480})

执行addshard命令时,会调动ForwardingCatalogManager::addShard(),最终调用会调用makeCatalogManager()函数创建的CatalogManagerReplicaSet对象继承的CatalogManagerCommon::addShard()方法,addShard()方法将分片相关信息插入到configserver。

const std::string ShardType::ConfigNS = "config.shards";
Status result = insertConfigDocument(txn, ShardType::ConfigNS, shardType.toBSON());

primaryShard的设置

1) 对于addshard前已经存在的database,addshard添加HostAndPort到一个shard后,会将该节点上所有已有的database对应的primaryShard都设置为该shard。如果有已经启用了sharding的database存在,则addshard命令会失败。
2) 对于addshard后新创建的database,则需要先调用如下命令为该database启动sharding。

db.runCommand({"enablesharding":"database"})

enablesharding命令会调用selectShardForNewDatabase()函数为该database选择一个当前totalSize最小的shard作为primaryShard,然后将相关信息写入到configserver的config.databases中。

const std::string DatabaseType::ConfigNS = "config.databases";

db.runCommand({“shardcollection”:”database.collection”, “key”: {“xxx” : “hashed”}})

shardcollection会为启动分片的collection按key范围进行切分,然后通过ChunkManager::createFirstChunks()函数将切分后的相关分片信息插入到configserver的config.chunk中。

const std::string ChunkType::ConfigNS = "config.chunks";
... ...
long long intervalSize = (std::numeric_limits<long long>::max() / numChunks) * 2;
long long current = 0;
if (numChunks % 2 == 0) {
     allSplits.push_back(BSON(proposedKey.firstElementFieldName() << current));
     current += intervalSize;
} else {
     current += intervalSize / 2;
}
for (int i = 0; i < (numChunks - 1) / 2; i++) {
    allSplits.push_back(BSON(proposedKey.firstElementFieldName() << current));
    allSplits.push_back(BSON(proposedKey.firstElementFieldName() << -current));
    current += intervalSize;
}
sort(allSplits.begin(), allSplits.end)
Status CatalogManagerReplicaSet::shardCollection(... ...){
    ... ...
    Status createFirstChunksStatus =
        manager->createFirstChunks(txn, dbPrimaryShardId, &initPoints, &initShardIds);
    ... ...
}
Status ChunkManager::createFirstChunks(... ...){
    ... ...
    for (unsigned i = 0; i <= splitPoints.size(); i++) {
        BSONObj min = i == 0 ? _keyPattern.getKeyPattern().globalMin() : splitPoints[i - 1];
        BSONObj max =
           i < splitPoints.size() ? splitPoints[i] : _keyPattern.getKeyPattern().globalMax();
        ChunkType chunk;
        ... ...
        Status status = grid.catalogManager(txn)
                            ->insertConfigDocument(txn, ChunkType::ConfigNS, chunk.toBSON());
    }
    ... ...
}

sh.enableBalancing(coll)

mongos> use config
mongos> db.collections.find()

mongos> sh.setBalancerState(true)

mongos> db.settings.find()
{ "_id" : "chunksize", "value" : NumberLong(2) }
{ "_id" : "balancer", "stopped" : false }

configserver中setting初始化是在runMongosServer() -> balancer.go() -> Balancer::run()中的如下处理:

auto balSettingsResult = grid.catalogManager(txn.get())->getGlobalSettings(
                txn.get(), SettingsType::BalancerDocKey);

db.runCommand({“listDatabases” : 1})

mongos> use admin
mongos> db.runCommand({"listDatabases" : 1})
{
        "databases" : [
                {
                        "name" : "test",
                        "sizeOnDisk" : 69632,
                        "empty" : false,
                        "shards" : {
                                "shard_27010" : 45056,
                                "shard_27011" : 12288,
                                "shard_27012" : 12288
                        }
                },
                {
                        "name" : "config",
                        "sizeOnDisk" : 868352,
                        "empty" : false
                }
        ],
        "totalSize" : 69632,
        "totalSizeMb" : 0,
        "ok" : 1
}

//mongs上执行listDatabases命令时会给所有shard对应副本集的primary节点发送listDatabases命令。

Othres

mongos> use config
mongos> db.getCollectionNames()
[
        "actionlog",
        "changelog",
        "chunks",
        "collections",
        "databases",
        "lockpings",
        "locks",
        "mongos",
        "settings",
        "shards",
        "tags",
        "version"
]

mongos> use admin
mongos> db.runCommand( { moveChunk : "test.111" , find : {"idx" : NumberLong("-3074457345618258601")}, to: "shard_27012"} )
{ "millis" : 199, "ok" : 1 }
mongos> db.runCommand( { moveChunk : "test.111" , "bounds" : [ { "idx" : NumberLong("-6148914691236517204") } , 
{ "idx" : NumberLong("-3074457345618258602") } ], to: "shard_27012"} )
mongos> db.getLogComponents()
mongos> db.setLogLevel(1)
mongos> db.setLogLevel(5, "command")

chunk split

触发时机

1、mongos写入数据时,当达到一定条件时(参考ChunkManager::getCurrentDesiredChunkSize()函数),会触发 chunk 的自动分裂。

ClusterWriter::write() -> splitIfNeeded() -> Chunk::splitIfShould() -> Chunk::split() ->
Chunk::determineSplitPoints() -> ChunkManager::getCurrentDesiredChunkSize()

2、sh.splitFind()、sh.splitAt()手动发送split命令给mongos,mongos调用SplitCollectionCmd::run()实现chunk split。

chunk split处理流程

1、mongos给chunk对应shard的primary节点发送splitVector命令,获取splitPoints。

Created with Raphaël 2.1.0 chunk.cpp chunk.cpp chunk_manager.cpp chunk_manager.cpp d_split.cpp d_split.cpp split() determineSplitPoints() getCurrentDesiredChunkSize() pickSplitVector() splitVector command SplitVector::run() const long long avgRecSize = dataSize / recCount; long long keyCount = maxChunkSize / (2 * avgRecSize); 遍历该chunk的所有key,当遍历的currCount > keyCount时, 将本次最后一个遍历的key加入splitKeys中。 response splitKeys multiSplit()

2、mongos获取splitPoints后,在multiSplit()函数中给对应shard的primary节点发送splitChunk命令。

Created with Raphaël 2.1.0 chunk.cpp chunk.cpp d_split.cpp d_split.cpp catalog_manager_ replica_set.cpp catalog_manager_ replica_set.cpp sharding_state.cpp sharding_state.cpp chunk_manager.cpp chunk_manager.cpp multiSplit() splitChunk command SplitChunkCommand::run() applyChunkOpsDeprecated() 更新remote configserver的配置 splitChunk() 更新本地metadata response shouldMigrate //used by Chunk::splitIfShould() reload() force reload of config

rebalance

Created with Raphaël 2.1.0 server.cpp server.cpp balance.cpp balance.cpp balancer_policy.cpp balancer_policy.cpp main() mongoSMain() _main() runMongosServer() Balancer::go() Balancer::run() Balancer::_doBalanceRound(..., &candidateChunks) BalancerPolicy::balance() Balancer::_moveChunks() Balancer::_ping()

副本集

heartbeat机制

Created with Raphaël 2.1.0 NetworkInt erfaceASIO NetworkInt erfaceASIO Replication CoordinatorImpl Replication CoordinatorImpl ReplicationExecutor ReplicationExecutor TopologyCoor dinatorImpl TopologyCoor dinatorImpl _startLoadLocalConfig scheduleWork _readyQueue.splice (_finishLoadLocalConfig) ReplicationExecutor::run() _finishLoadLocalConfig() _setCurrentRSConfig_inlock() _startHeartbeats_inlock() _doMemberHeartbeat() asyncSendMessage write sendMessageCallback read recvHeaderCallback AsyncOp* op->_onFinish() _finishRemoteCommand _readyQueue.splice (_handleHeartbeatResponse) ReplicationExecutor::run() _handleHeartbeatResponse() TopologyCoordinatorImpl:: processHeartbeatResponse action = TopologyCoordinatorImpl:: _updatePrimaryFromHBDataV1 _scheduleHeartbeatToTarget() _handleHeartbeatResponseAction()
  1. mongod进程启动时,会调用_initAndListen函数初始化服务,_initAndListen函数中调用startReplication函数初始化副本集,startReplication函数中又会调用_startLoadLocalConfig函数加载本地配置信息(local.system.replset中保存了本地的配置信息),同时,_startLoadLocalConfig函数会将ReplicationCoordinatorImpl::_finishLoadLocalConfig的回调函数设置item,然后会将item加入到ReplicationExecutor::_readyQueue队列中。
  2. startReplication函数中会调用ReplicationExecutor对象的startup函数,startup函数会启动一个线程执行ReplicationExecutor::run函数,run函数中会从_readyQueue队列中获取item,执行item中的callback函数。
  3. ReplicationExecutor::run()函数中获取队列item时,执行item中的_finishLoadLocalConfig回调函数,_finishLoadLocalConfig 中又会调用_setCurrentRSConfig_inlock函数,_setCurrentRSConfig_inlock中调用_startHeartbeats_inlock函数启动心跳流程。
  4. _startHeartbeats_inlock函数会调用_scheduleHeartbeatToTarget函数,_scheduleHeartbeatToTarget函数会将要执行的时间点和ReplicationCoordinatorImpl::_doMemberHeartbeat回调函数保存在item中,然后将item加入ReplicationExecutor::_sleepersQueue队列中。
  5. ReplicationExecutor::run函数中会调用getWork函数获取_readyQueue队列中的item,然后执行item中的回调函数,其中,getWork函数会将_sleepersQueue队列中满足执行条件的item加入_readyQueue队列中。
  6. ReplicationCoordinatorImpl::_doMemberHeartbeat函数在满足执行条件后,会在ReplicationExecutor::run函数中被取出相应的item执行,_doMemberHeartbeat函数执行时会将心跳请求通过ReplicationExecutor::scheduleRemoteCommand发送到target副本,在收到响应后,会调用ReplicationExecutor::_finishRemoteCommand函数,而_finishRemoteCommand函数又会将ReplicationCoordinatorImpl::_handleHeartbeatResponse回调函数保存到item,然后加入ReplicationExecutor::_readyQueue队列中。最终,_handleHeartbeatResponse函数会在ReplicationExecutor::run函数中执行。

1) mongod的本地配置保存在local.system.replset的collection中。
2) 默认配置下,复制集的节点每隔2s会向其他成员发送一次心跳请求,心跳消息主要包含replSetName、本机的节点地址、复制集版本等。
3) 复制集成员收到心跳请求后,就开始处理请求,并将处理的结果回复给请求的节点。

如果自身不是复制集模式、或复制集名称不匹配,则返回错误应答。
如果源节点的复制集配置(rs.conf()的内容)版本比自己低,则将自身的配置加入到心跳应答消息里。
将节点自身的oplog及其他状态信息等加入到心跳应答消息,如lastOpApplied、lastOpDurable等。
如果自身是未初始化状态,则立即向源节点发送心跳请求,以更新复制集配置。

4) 节点收到心跳应答后,会调用_handleHeartbeatResponse函数,根据应答消息来更新对端节点的状态,并根据最终的状态确定是否需要进行重新选举。

收到心跳应答时,如果是错误应答,则:

  1. 如果当前重试次数 <= kMaxHeartbeatRetries(默认为2),并且上一次发送心跳在kDefaultHeartbeatTimeoutPeriod(默认为10)时间内,则立即发送下一次心跳。
  2. 当失败次数超过kMaxHeartbeatRetries,或者上一次心跳时间到现在超过kDefaultHeartbeatTimeoutPeriod,则认为节点down。

心跳消息超时未应答相当于收到了错误应答

void _validateAndRun(AsyncOp* op, std::error_code ec, Handler&& handler) {
   if (op->canceled())
      return _completeOperation(op, 
            Status(ErrorCodes::CallbackCanceled, "Callback canceled"));
   if (op->timedOut())
      return _completeOperation(op, 
           Status(ErrorCodes::ExceededTimeLimit, "Operation timed out"));
   if (ec) return _networkErrorCallback(op, ec);

   handler();
}

5) 如果对端的复制集版本比自己高,则通过makeReconfigAction创建action,然后更新自己的配置并持久化到local数据库中,同时,根据应答消息更新对端的状态信息。
6) v1版本的心跳协议中,先扫描副本集成员,找到最新的primary副本,如果发现自身的term跟primary副本的term相等,且自身的优先级高于primary副本的优先级,则通过makePriorityTakeoverAction调用_startElectSelfIfEligibleV1函数启动选举,选举过程送发送replSetRequestVotes进行投票。

  1. 在该优先级高的节点通过_startElectSelfIfEligibleV1选举成功时,在第二阶段发送提交消息时(dryRun为false),由于增加的term值,因此,无论是其他primary节点还是secondary节点,都会在processReplSetRequestVotes函数中调用updateTerm函数更新自己的term值。如果是master节点,还会触发stepdown操作。
  2. 在_handleHeartbeatResponse函数中处理心跳响应时,如果响应消息正常,则会调用_updateTerm_incallback尝试更新term值,如果本身是primarfy节点,则会触发stepdown操作。

leader选举

选举触发条件

初始化一个副本集

rs.initiate(调用的replSetInitiate)命令实现初始化副本集的功能,命令会调用CmdReplSetInitiate::run()函数,CmdReplSetInitiate函数初始化副本集时,会将配置文件写入本地local.oplog.rs集合中,然后调用_finishReplSetInitiate函数,在_setCurrentRSConfig_inlock函数中调用_startHeartbeats_inlock初始化心跳流程,刚开始每个节点都不是leader,_setCurrentRSConfig_inlock函数中设置的定时选举函数不会被不断重设(_handleHeartbeatResponse函数中检测primary节点有心跳响应才会不断重设),因此,等待一段时间后(本地electionTimeoutMillis配置默认值10s+随机值),就会开始启动选举流程,选举出一个leader并通知副本集其他节点。

主节点挂掉或断开连接

在心跳响应处理函数_handleHeartbeatResponse中,如果是leader节点,并且响应是正常的,那么slave节点会调用cancelAndRescheduleElectionTimeout函数重设定时选举回调函数_startElectSelfIfEligibleV1,因此,主节点挂掉或主节点断开连接,定时选举函数不会被不断重设,等待一段时间后(本地electionTimeoutMillis配置默认值10s+随机值),就会开始启动选举流程,选举出一个leader并通知副本集其他节点,slave节点挂掉或断开连接,则不会触发选举。

存在优先级更高的节点

_handleHeartbeatResponse函数中处理收到心跳响应,在processHeartbeatResponse函数中会调用_updatePrimaryFromHBDataV1函数update leader,如果发现本节点与最新的leader中term值相等,同时,优先级更高,则会做PriorityTakeover操作,从而启动选举流程。

if (_hbdata[primaryIndex].getTerm() == _term && updatedConfigIndex == primaryIndex &&
rsConfig.getMemberAt(primaryIndex).getPriority() < rsConfig.getMemberAt(_selfIndex).getPriority()) {
      LOG(4) << "I can take over the primary due to higher priority."
              << " Current primary index: " << primaryIndex << " in term "
              << _hbdata[primaryIndex].getTerm();

       return HeartbeatResponseAction::makePriorityTakeoverAction();
   }

选举流程

leader选举流程图

Created with Raphaël 2.1.0 Replication CoordinatorImpl Replication CoordinatorImpl VoteRequester VoteRequester ScatterGatherRunner ScatterGatherRunner VoteRequester. Algorithm VoteRequester. Algorithm ReplicationExecutor ReplicationExecutor _startElectSelfV1 start start getRequests scheduleRemoteCommand _processResponse _signalSufficient ResponsesRComplete _onDryRunComplete _writeLastVoteForMyElection _startVoteRequester start start getRequests scheduleRemoteCommand _processResponse _signalSufficient ResponsesRComplete _onVoteRequestComplete processWinElection _performPostMemberStateUpdateAction(kActionWinElection)

secondary选举流程图

Created with Raphaël 2.1.0 network network CmdReplSet RequestVotes CmdReplSet RequestVotes Replication CoordinatorImpl Replication CoordinatorImpl ReplicationExecutor ReplicationExecutor TopologyCoord inatorImpl TopologyCoord inatorImpl ReplicationCoordina torExternalStateImpl ReplicationCoordina torExternalStateImpl request run processReplSetRequestVotes scheduleWork _readyQueue.splice (_processReplSet RequestVotes_finish) ReplicationExecutor::run() _processReplSet RequestVotes_finish() processReplSetRequestVotes storeLocalLastVoteDocument response request run processReplSetRequestVotes scheduleWork _readyQueue.splice (_processReplSet RequestVotes_finish) ReplicationExecutor::run() _processReplSet RequestVotes_finish() processReplSetRequestVotes storeLocalLastVoteDocument response

mongodb选举基于raft协议实现(redis cluster的leader选举也是基于raft实现),选举过程中的判断条件如下代码所示:

   if (args.getTerm() < _term) {
        response->setVoteGranted(false);
        response->setReason("candidate's term is lower than mine");
    } else if (args.getConfigVersion() != _rsConfig.getConfigVersion()) {
        response->setVoteGranted(false);
        response->setReason("candidate's config version differs from mine");
    } else if (args.getSetName() != _rsConfig.getReplSetName()) {
        response->setVoteGranted(false);
        response->setReason("candidate's set name differs from mine");
    } else if (args.getLastCommittedOp() < lastAppliedOpTime) {
        response->setVoteGranted(false);
        response->setReason("candidate's data is staler than mine");
    } else if (!args.isADryRun() && _lastVote.getTerm() == args.getTerm()) {
        response->setVoteGranted(false);
        response->setReason("already voted for another candidate this term");
    } else {
        if (!args.isADryRun()) {
            _lastVote.setTerm(args.getTerm());
            _lastVote.setCandidateIndex(args.getCandidateIndex());
        }
        response->setVoteGranted(true);
    }

如上所示,leader选举过程中,会基于term、configversion、replsetname、lastAppliedOpTime等条件判断,主要是选择term最大、term相同下lastAppliedOpTime的节点作为leader。

数据同步

同步场景

  1. 副本集初始化

    初始化选出Primary后,此时Secondary上无有效数据,oplog是空的,会先进行initial sync,然后不断的应用新的oplog。

  2. 新成员加入

    因新成员上无有效数据,oplog是空的,会先进行initial sync,然后不断的应用新的oplog。

  3. 有数据的节点加入

    有数据的节点加入有如下情况:

    1. 该节点与副本集其他节点断开连接,一段时间后恢复。
    2. 该节点从副本集移除(处于REMOVED)状态,通过replSetReconfig命令将其重新加入。

    该场景下,如果该节点最新的oplog时间戳,比所有节点最旧的oplog时间戳还要小,该节点将找不到同步源,会一直处于RECOVERING而不能服务;反之,如果能找到同步源,则直接进入replication阶段,不断的应用新的oplog。
    设置合理的oplog大小非常重要,如果因为oplog太旧而处于RECOVERING的节点目前无法自动恢复,需人工介入处理,最简单的方式是发送resync命令,让该节点重新进行initial sync。

initial sync

Secondary节点启动后,判断如果满足如下条件,即会进行initial sync。
1) Secondary上oplog为空,比如新加入的空节点
2) 本地local.replset.minvalid集合里doingInitialSync标记被设置为true。

当initial sync开始时,同步线程会调用setInitialSyncFlag函数设置该标记为true,当initial sync结束时会调用clearInitialSyncFlag函数清除该标记,如果initial sync过程中出现失败的情况,那么节点重启后发现该标记被设置为true,就会重新进行initial sync操作。

3) BackgroundSync::_initialSyncRequestedFlag被设置。

当向节点发送resync命令时,该标记会被设置,此时会强制进行initial sync操作。

initial sync时序图

Created with Raphaël 2.1.0 Replication CoordinatorImpl Replication CoordinatorImpl ReplicationCoordinator ExternalStateImpl ReplicationCoordinator ExternalStateImpl thread thread OplogReader OplogReader BackgroundSync BackgroundSync _finishLoadLocalConfig startThreads new stdx::thread runSyncThread syncDoInitialSync getLastOp setInitialSyncFlag dropAllDatabasesExceptLocal get lastOptime 0 _initialSyncClone(data) get lastOptime 1 _initialSyncApplyOplog get lastOptime 2 _initialSyncApplyOplog _initialSyncClone(index) get lastOptime 3 _initialSyncApplyOplog setInitialSync RequestedFlag(false) clearInitialSyncFlag

如上图所示,intial sync主要包括如下流程:
1. 调用setInitialSyncFlag函数,设置minValid集合中doingInitialSync标记。
2. 获取同步源当前最新的oplog时间戳lastOptime 0。
3. 调用_initialSyncClone函数从同步源Clone所有的集合数据。
4. 获取同步源最新的oplog时间戳lastOptime 1。
5. 调用_initialSyncApplyOplog函数同步lastOptime 0 ~ lastOptime 1的所有的oplog。
6. 获取同步源最新的oplog时间戳lastOptime 2。
7. 调用_initialSyncApplyOplog函数同步lastOptime 1 ~ lastOptime 2所有的oplog。
8. 调用_initialSyncClone函数从同步源读取index信息,并建立索引。
9. 获取同步源最新的oplog时间戳lastOptime 3。
10. 调用_initialSyncApplyOplog函数同步lastOptime 2~ lastOptime 3所有的oplog。
11. 调用BackgroundSync::get()->setInitialSyncRequestedFlag函数清除_initialSyncRequestedFlag标记,调用clearInitialSyncFlag函数清除doingInitialSync标记。

sync oplog

当Secondary节点initial sync操作完成后,就进入sync oplog的状态,Secondary节点存在后台线程持续不断的从复制源同拉取新oplog,并在Secondary节点进行重放。

producer thread

这个线程不断的从同步源上拉取oplog,并加入到一个BlockQueue的队列里保存着。

OpQueueBatcher thread

这个线程在OpQueueBatcher对象初始化的时候启动,负责逐个从producer thread的BlockQueue队列里取出oplog,并放到自己维护的队列里。

writerPool

writerPool线程池中默认启动16个线程,sync线程将OpQueueBatcher thread的队列中的oplog分发到writerPool线程池中默认的16个writerThread线程进行处理,由writerthread来最终重放每条oplog。

writeOpsToOplog

默认会将sync的oplog写入到本地的local.oplog.rs集合中。

该过程中可能出现的两种问题:

1) 复制源数据写入速度过快(或者相对的,本地数据写入速度过慢),复制源的oplog覆盖了本地用于同步源oplog而维持在源的游标。

这个问题一般可以通过检查拉取回来的oplog的optime是否对应来判断,出现这种问题,节点本身会通过更换同步源的方式来尝试解决,如果更换同步源的方式也无法解决,则通过resync命令启动initial sync来解决。

2) 本节点在宕机之前是Primary,在重启后本地oplog有和当前Primary不一致的Oplog。

这个问题一般可以通过检查oplog的hash值是否相等来判断,如果不相同,则说明oplog出现冲突了,对应位置上的oplog不一致。

Write Concern

内部实现


实现流程

  1. Client向Primary发起请求,指定writeConcern为{w: 3},Primary收到请求,本地写入并记录写请求到oplog,然后等待另外2个Secondary节点都同步了这条oplog(Secondary应用完oplog会通过replSetUpdatePosition命令向Primary报告最新进度)。
  2. Secondary拉取到Primary上新写入的oplog,本地重放并记录oplog。find命令支持一个awaitData的选项,当find没有任何符合条件的文档时,并不立即返回,而是等待最多maxTimeMS(默认为2s)时间看是否有新的符合条件的数据,如果有就返回;所以当新写入oplog时,Secondary能在第一时间内拉取到主上的oplog。

    BSONObjBuilder cmdBob;
    cmdBob.append("find", nsToCollectionSubstring(rsOplogName));
    cmdBob.append("filter", BSON("ts" << BSON("$gte" << lastOpTimeFetched.getTimestamp())));
    cmdBob.append("tailable", true);
    cmdBob.append("oplogReplay", true);
    cmdBob.append("awaitData", true);
    cmdBob.append("maxTimeMS", durationCount<Milliseconds>(Minutes(1)));  // 1 min initial find.
    ... ...
  3. Secondary上有单独的SyncSourceFeedback线程,当oplog的最新时间戳发生更新时,就会向Primary发送replSetUpdatePosition命令更新自己的oplog时间戳。当Primary发现满足writeConcern条件的的Secondary已经同步了该条oplog,就会向客户端发送response。

应答机制

mongodb应答机制主要分为2种:

应答式写入(缺省情形,安全写入,适用于数据强一致性场景)
非应答式写入(非安全写入,适用于数据弱一致性场景)

应答机制实现方式:

通过Write Concern来实现,客户端驱动调用db.getLastError()方法,错误返回给客户端。
如果捕获到错误,则可以通过客户端定义的逻辑尝试再次写入或记录到特定日志等。

Write Concern用法

writeConcern:{ { w: value, j: boolean, wtimeout: number } }

w : 该选项要求确认操作已经传播到指定数量的mongod实例或指定标签的mongod实例,w可选的值如下:

number

w:1(应答式写入):要求确认操作已经传播到指定的单个mongod实例或副本集主实例(缺省为1)
w:0(非应答式写入):不返回任何响应,所以无法知道写入是否成功,但是对于尝试向已关闭的套接字写入或者网络故障会返回异常信息。
w:>1(用于副本集环境):该值用于设定写入节点的数目,包括主节点。

“majority”(大多数)

适用于集群架构,要求写入操作已经传递到绝大多数投票节点以及主节点后进行应答。

tag set

要求写入操作已经传递到指定tag标记副本集中的成员后进行应答。

j : 该选项要求确认写操作已经写入journal日志之后应答客户端(需要开启journal功能)

在意外重启,宕机等情形下可以通过journal来进行数据恢复。
写入journal操作必须等待直到下次提交日志时完成写入。
为降低延迟,MongoDB可以通过增加commit journal的频率来加快journal写入。

wtimeout:该选项指定一个时间限制,以防止写操作无限制被阻塞导致无法应答给客户端。

wtimeout的单位为ms,当w值大于1时生效,该参数即仅适用于集群环境。
当某个节点写入时超出指定wtimeout之后,mongod将返回一个错误。
在捕获到超时之前,mongod并不会撤销其他节点已成功完成的写入。
wtimeout值为0时等同于没有配置wtimeout选项,容易导致由于某个节点挂起而无法应答。

Write Concern使用实例

其他

mongo-cxx-driver编译

cd build &&
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=.. -DCMAKE_CXX_FLAGS=”-static-libgcc -static-libstdc++” -DPKG_CONFIG_PATH=/usr/local/lib/pkgconfig -DLIBBSON_DIR=/root/data1/projs/deps/mongo-c-driver/mongodb_c_bin/ -DLIBMONGOC_DIR=/root/data1/projs/deps/mongo-c-driver/mongodb_c_bin/ ..

参考文档

Raft 实现日志复制同步
Raft 一致性算法论文译文
mongodb副本集
MongoDB Wiredtiger存储引擎实现原理
MongoDB WiredTiger 存储引擎(1) cache_pool设计
MongoDB集群均衡
MongoDB sharding迁移那些事
MongoDB 3.2.9 请求 hang 分析及 wiredtiger 调优
MongoDB sharding chunk 分裂与迁移详解

猜你喜欢

转载自blog.csdn.net/shangshengshi/article/details/78408598