近日,在MongoDB用户大会纽约站上,阿里云荣膺MongoDB“2024年度DBaaS认证合作伙伴奖”。这是阿里云连续第五年斩获 MongoDB 合作伙伴奖项,也是唯一获此殊荣的中国云厂商。
MongoDB是当今全球最受开发者欢迎的非关系型数据库。凭借灵活的模式和丰富的文档结构,MongoDB帮助企业客户及百万开发者们使用丰富的数据结构快速开发应用,多快好省地构建现代应用程序。
阿里云是国内唯一首发MongoDB新版本的云服务厂商。基于阿里云的云原生环境,客户可以更好体验到MongoDB数据库的崭新能力和性能,进而实现业务的快速迭代、开发,降低开销、增加效率。
自2019年阿里云与MongoDB达成战略合作伙伴关系以来,阿里云数据库MongoDB版已广泛应用于互联网、游戏、汽车出行、零售、金融等行业,累计为数万家客户提供MongoDB云数据库服务,MongoDB也取得了国内营收增长8倍的里程碑式成绩,目前阿里云已经成为MongoDB在中国最大的云服务提供商。
MongoDB全球合作伙伴高级副总裁Alan Chhabra表示,阿里云始终如一地提供优秀的云服务,集成新的 MongoDB产品功能,为中国各行各业提供强大、可扩展且安全的数据库解决方案。通过提供新的 MongoDB云服务,阿里云致力于本地化产品和服务以满足中国市场的新需求,进一步印证了阿里云领先的 DBaaS 提供商的地位。
阿里云数据库负责人李飞飞也表示,与MongoDB合作的四年是非常甜蜜的四年,阿里云不仅仅是单纯的OEM MongoDB数据内核,更多是把MongoDB能力和阿里云云原生的环境紧密结合起来。通过使用具有阿里云特色的MongoDB数据库,客户们能够实现业务的快速迭代、开发、降低开销、增加效率。
2023年,阿里云与MongoDB续签了战略合作协议,双方致力于将MongoDB的最新产品成果与阿里云企业级服务相结合,深耕国内市场,携手技术创新,打造最佳实践。
在今年的用户大会,MongoDB上也宣布推出一项新的云合作伙伴认证计划,该计划名为“Certified by MongoDB DBaaS”(数据库即服务),旨在为云基础设施合作伙伴提供最新MongoDB产品特性的访问权限,并允许他们向自己的客户提供一流的托管数据库服务,包括全文搜索、向量搜索和行业领先的数据加密等功能。阿里云NoSQL数据库产品线负责人张为表示,在当今AI应用飞速发展的时代,MongoDB与仅存储向量数据的附加解决方案不同,其向量搜索能力通过与原有的高性能和可扩展的主数据库紧密集成,客户能够安全地在整个数据库中充分利用生成式 AI 和专有数据,以更少的代码开发和运营开销来更快地实现业务价值。
不远的将来,阿里云作为MongoDB 重要的云合作伙伴之一,也将能够在阿里云MongoDB托管服务中提供向量搜索和全文搜索能力,客户将可以第一时间在阿里云MongoDB使用该能力来构建高级搜索和生成式AI应用程序。
MongoDB总裁兼首席执行官Dev Ittycheria 及 MongoDB全球合作伙伴高级副总裁Alan Chhabra 共同向 阿里云NoSQL数据库产品线负责人张为 授予“MongoDB 2024年度DBaaS认证合作伙伴”奖项
学习拓展:
MongoDB副本集选举分析
1. 背景
1.1 副本集架构
图1是官方普通三节点副本集的结构图,由一个Primary节点和两个Secondary节点组成,各节点通过Hearbeat判断其他节点是否存活。
图1 MongoDB副本集架构
图2是云MongoDB的三节点副本集架构,由一个Primary节点、一个Secondary节点和一个Hidden节点组成。Hidden节点是特殊的Secondary节点,它对客户端不可见,通过设置内核priority=0保证不参与选举,提供高可用和备份服务。
图2 云MongoDB副本集架构
1.2 名词说明
探活机制
MongoDB副本集的探活是通过Heartbeats实现。心跳检测的周期是默认2s,超过10s没有返回后会标记该节点为不可达。
优先级 priority
表示节点成为primary的相对可能性,在相同条件下更倾向于选举优先级最高的节点成为primary(优先级等于0的节点不会被选举为Primary)
投票 vote
选举投票权,副本集内最多允许7个节点有投票权(优先级大于0的节点投票不能为0)
2. 选举阶段解析
在选举过程中,副本集的Primary不支持读写,Secondary支持读。
图3是最常见的选举场景,副本集中Primary节点下线,触发选举。其中一个Secondary节点要求进行选举来保证副本集可用,在获得足够多的投票(超过1/2的总投票)后选举成为新的Primary。
图3
2.1 触发选举
a. 触发选举的原因
- 副本集无主超时:无主时间超过electionTimeoutMillis,图3
- 设置非Primary节点为更高的优先级:Secondary优先级提高,优先选举Secondary为Primary
- 选举的Primary在追赶阶段发生takeover:后面会解释追赶阶段
- 单节点选举:单节点结构下选举自己为Primary
b. 候选者需满足的条件
- 该节点是Secondary或Primary
- 有最新的oplog
- 可以"看到"大多数的节点(超过1/2)
- 不是新加入的节点且不是arbiter
- 该节点优先级大于0
- 近期没有执行stepDown命令
2.2 预选举
候选者会进行预选举,判断自身是否可以发起真正选举。
a. 节点投赞成票的条件
- 该节点不处于以下状态
- DOWN
- STARTUP
- 该节点具有投票能力 — votes > 0
- 该节点的任期不大于候选者的任期
- 该节点与候选者版本相同
- 该节点与候选者副本集名相同
- 该节点的oplog不比候选者新
- 当前Primary不健康或优先级低于候选者
b. 预选举失败的原因
- 投票不足
- 任期过期
- Primary投反对票
预选举可以避免进入无意义的选举阶段。若当前节点不可能被选举成功,则预选举就会失败,且不会增加任期term,反正预选举成功term加一,进入新的任期。
下面是没有预选举环节的选举示例:
图4 直接选举
如图4所示,一个互通的3节点集群发生网络隔离,S2与S1和S3不互通。
1) S2节点每次选举超时的时候都会发起一次选举并自增term;由于不能连接到S1和S3,无法获得大多数赞同,选举会失败。如此反复,term会增加到一个相对比较大的值;
2)由于S1和S3满足大多数条件,选举S1成为集群新的主节点,term变为2;
3)当网络连接恢复,S2又可以重新连接到S1和S3之后,其term会通过心跳传递给S1和S3,而这会导致S1 step down成为从节点;
4)选举超时时间过后,集群会重新触发一次选举,无论是S1还是S3成为新的主(S2由于落后所以不可能),其term值会变成11;
上述场景中有term跳变和最后一次无意义选举两个问题,预选举可以有效解决。在尝试自增term并发起选举之前,S2会看看自己有没有可能获得来自S1和S3的选票。如若不满足条件则不会发起真正的选举。
2.3 选举
预选举成功后会进入真正的选举阶段,此时会先自增term。节点会优先投自己一票,然后接收本次选举过程中其他节点的投票。收到超过1/2的赞成票认为预选举成功,进入状态变更阶段。
此时愿意投赞成票的节点限制与预选举大致相同,只多了一条限制:该节点当前任期没有给其他节点投票
2.4 状态变更
处理选举的结果
a.选举成功
- 更新节点状态,选举成功的节点从SECNONDAY变成PRIMARY状态
- 通知副本集内所有的secondary节点选举胜利
- Primary节点进入追赶阶段
b.选举失败
无操作
2.5 追赶
虽然在选举过程中要求候选者的oplog时间比投赞成票节点新,但是选举成功的Primary节点还是会检查是否有其他节点有更新的oplog。
如图4的情况,假如S2有更新的oplog,但是由于网络分区等原因没有参与选举,在选举结束后网络正常,此时Primary节点发现S2有更新的oplog,会去拉取S2的oplog。
追赶阶段有设置超时时间,所以部分情况无法获得其他节点全部的oplog,此时其他节点会进入ROLLBACK状态回滚操作。
2.6 日志示例
a. 副本集配置
Primary:priority=2, votes=1
Secondary:priority=1,votes=1
Hidden:priority=0,votes=1
b. 场景
副本集因为Primary节点不可用,触发选举
# 触发选举 REPL_HB Heartbeat to xx.xx.xx.xx:xx failed after 2 retries, response status: HostUnreachable: Error connecting to xx.xx.xx.xx:xx :: caused by :: Connection refused REPL Starting an election, since we've seen no PRIMARY in the past 10000ms # 预选举 REPL conducting a dry run election to see if we could be elected. current term: 3 REPL VoteRequester(term 3 dry run) failed to receive response from xx.xx.xx.xx:xx: HostUnreachable: Error connecting to xx.xx.xx.xx:xx :: caused by :: Connection refused REPL VoteRequester(term 3 dry run) received a yes vote from xx.xx.xx.xx:xx; response message: { term: 3, voteGranted: true, reason: "", ok: 1.0, operationTime: Timestamp(1706176453, 1), $clusterTime: { clusterTime: Timestamp(1706176453, 1) REPL VoteRequester(term 4) failed to receive response from xx.xx.xx.xx:xx: HostUnreachable: Error connecting to xx.xx.xx.xx:xx :: causedby :: Connection refused REPL dry election run succeeded, running for election in term 4 # 选举 REPL VoteRequester(term 4) received a yes vote from xx.xx.xx.xx:xx; response message: { term: 4, voteGranted: true, reason: "", ok: 1.0, operationTime: Timestamp(1706176453, 1), REPL election succeeded, assuming primary role in term 4 # 状态变更 REPL transition to PRIMARY from SECONDARY REPL Resetting sync source to empty, which was :xx # 追赶阶段 REPL Entering primary catch-up mode. REPL Caught up to the latest optime known via heartbeats after becoming primary. Target optime: { ts: Timestamp(1706176453, 1), t: 3 }. My Last Applied: { ts: Timestamp(1706176453, 1), t: 3 } REPL Exited primary catch-up mode. REPL Stopping replication producer # 允许写入 REPL transition to primary complete; database writes are now permitted
1.触发选举
在Heartbeat多次探活失败后,副本集内部确认无主,触发选举
2.预选举
由于当前副本集内只有Secondary priority > 0,所以只有其发起了预选举。在预选举中收到一个赞成票,由于自动投票给自己,所以满足大多数,预选举成功,term加1。
3.选举和状态变更
同上,获得大多数赞成票,选举成功,假定term 4期间为Primary,将节点状态设置为PRIMARY
4.追赶阶段
进入追赶阶段,根据heartbeats获得最新的oplog时间,确认与当前的oplog时间相同,结束追赶阶段。选举成功,允许写入。
3. 代码解析
3.1 触发选举
_startElectSelfIfEligibleV1(reason)
根据传递的选举原因判断该节点是否可以为候选者 (becomeCandidateIfElectable)
switch (reason) { case StartElectionReasonEnum::kElectionTimeout: # 无主,副本集内探活10s内无primary LOG_FOR_ELECTION(0) << "Starting an election, since we've seen no PRIMARY in the past " << _rsConfig.getElectionTimeoutPeriod(); break; case StartElectionReasonEnum::kPriorityTakeover: # 优先级抢占,有节点设置更高优先级 LOG_FOR_ELECTION(0) << "Starting an election for a priority takeover"; break; case StartElectionReasonEnum::kStepUpRequest: case StartElectionReasonEnum::kStepUpRequestSkipDryRun: # 主动提升为primary # 跳过预选举 LOG_FOR_ELECTION(0) << "Starting an election due to step up request"; break; case StartElectionReasonEnum::kCatchupTakeover: # 追赶抢占,primary处于catchup阶段时发生了takeover LOG_FOR_ELECTION(0) << "Starting an election for a catchup takeover"; break; case StartElectionReasonEnum::kSingleNodePromptElection: # 单节点选举 LOG_FOR_ELECTION(0) << "Starting an election due to single node replica set prompt election"; break; default: MONGO_UNREACHABLE; } _startElectSelfV1_inlock(reason)
_startElectSelfV1_inlock
处理中间过程,对于kStepUpRequestSkipDryRun跳过预选举直接进入选举步骤,否则进入预选举阶段。
预选举与跳过预选举
// 获取当前term long long term = _topCoord->getTerm(); int primaryIndex = -1; // 跳过预选举 if (reason == TopologyCoordinator::StartElectionReason::kStepUpRequestSkipDryRun) { // term自增 long long newTerm = term + 1; log() << "skipping dry run and running for election in term " << newTerm; // 真正选举 _startRealElection_inlock(newTerm, reason); lossGuard.dismiss(); return; } // 选举准备 _voteRequester.reset(new VoteRequester); // Only set primaryIndex if the primary's vote is required during the dry run. if (reason == TopologyCoordinator::StartElectionReason::kCatchupTakeover) { primaryIndex = _topCoord->getCurrentPrimaryIndex(); } // 预选举 _processDryRunResult(term, reason); lossGuard.dismiss();
3.2 预选举
_processDryRunResult
预选举失败原因
- kInsufficientVotes:获得的投票不足
- kStaleTerm:自身term过期
- kPrimaryRespondedNo:primary拒绝投票
- kSuccessfullyElected:未知问题
预选举成功则增加term,调用_startRealElection_inlock开始选举
预选举失败及成功
const VoteRequester::Result endResult = _voteRequester->getResult(); // 预选举失败 if (endResult == VoteRequester::Result::kInsufficientVotes) { log() << "not running for primary, we received insufficient votes"; return; } else if (endResult == VoteRequester::Result::kStaleTerm) { log() << "not running for primary, we have been superseded already"; return; } else if (endResult == VoteRequester::Result::kPrimaryRespondedNo) { log() << "not running for primary, the current primary responded no in the dry run"; return; } else if (endResult != VoteRequester::Result::kSuccessfullyElected) { log() << "not running for primary, we received an unexpected problem"; return; } // 预选举成功 long long newTerm = originalTerm + 1; log() << "dry election run succeeded, running for election in term " << newTerm; // 选举 _startRealElection_inlock(newTerm, reason);
3.3 选举
_startRealElection_inlock
选举
// 给自己投票 _topCoord->voteForMyselfV1() .. // 保存副本集投票情况 _writeLastVoteForMyElection() .... // 向其他节点发送投票请求 _startVoteRequester_inlock() ...... // 收到投票结果并处理 _onVoteRequestComplete() ........ // 副本集状态机更改 _postWonElectionUpdateMemberState_inlock()
状态机更改
// 修改role等配置 _topCoord->processWinElection(_electionId, ts); const PostMemberStateUpdateAction nextAction = _updateMemberStateFromTopologyCoordinator_inlock(nullptr); // 设置同步端为空 _onFollowerModeStateChange(); // 通知其他节点选举成功 _restartHeartbeats_inlock(); // 进入追赶阶段 _catchupState->start_inlock();
3.4 追赶阶段
// 通过心跳获得最新oplog来追赶 _handleHeartbeatResponse() // 判断自身和最新oplog的时间 CatchupState::signalHeartbeatUpdate_inlock() // 结束追赶阶段 CatchupState::abort_inlock()
4. 案例分析
a.背景
实例写入量过大,primary cpu被打满并开始出现主备延迟(secondary有延迟,hidden无延迟),客户主动下发主备切换希望降低流量,但是切换长时间不成功,引起客户担忧,最终在客户停止写入业务一分钟后,主备切换成功。
b.分析
实例状态更改前后如下表。
HA前状态 |
HA后状态 |
|
Primary |
A |
B |
Secondary |
B |
A |
Hidden |
C |
C |
客户下发正常主备切换后,B的优先级提高至2,A的优先级降低为1,B优先级提升,但是由于和其他节点延迟过高,导致无法成为候选者,没有触发预选举。
ELECTION Not starting an election for a priority takeover, since we are not electable due to: Not standing for election because member is not caught up enough to the most up-to-date member to call for priority takeover - must be within 2 seconds (mask 0x80)
客户停止写入业务后,切换快速成功,预期需要等待B同步结束后才会完成选举,但是B延迟7个小时,担心数据有丢失,因此需要确定当时选举成功的原因。
查看A日志中无Rollback,确认没有数据回滚操作。
查看B日志,第一条日志显示业务写入中追oplog,但是存在很大延时,符合监控数据。
第二条日志显示Secondary 最新的oplog已经和Primary时间相同,此时可以成为候选者,后续也正常选举成功。
REPL Scheduled new oplog query Fetcher source: A database: local query: { find: "oplog.rs", filter: { ts: { $gte: Timestamp(xxx1) } }, tailable: true, oplogReplay: true, awaitData: true, term: xx} REPL Choosing new sync source. Our current sync source is not primary and does not have a sync source, so we require that it is ahead of us. Current sync source: A, my last fetched oplog optime: { ts: Timestamp(xxx2), t: xx }, latest oplog optime of sync source: { ts: Timestamp(xxx2), t: xx } (sync source does not know the primary)