MongoDB 4.x副本集1、副本集架构2、集群选举2.1、Raft选举算法2.1.1、范例2.1.2、协议中的角色2.1.3、选举流程2.1.4、冲突2.2、MongoDB实现的扩展2.3、MongoDB选举介绍2.4、副本集模式2.4.1、PSS模式2.4.2、PSA模式2.4.3、PSH模式3、实时复制3.1、oplog复制3.2、幂等性3.3、复制延迟3.4、初始化同步3.5、数据回滚4、自动故障转移5、搭建副本集5.1、安装副本集5.2、创建用户5.3、写入数据5.4、主备节点切换6、检查复制的延迟情况6.1、rs.status命令6.2、查看复制延迟1、副本集架构在前面我们已经完成了MongoDB在单机上的安装并进行了基本的功能体验。然而在生产环境中不建议使用单机版的MongoDB服务器。原因如下单机版的MongoDB无法保证可靠性一旦进程发生故障或是服务器宕机业务将直接不可用。一旦服务器上的磁盘损坏数据会直接丢失而此时并没有任何副本可用。于是任何生产环境的数据库都应该拥有一个或多个可用的副本实例在出现任何异常情况时能第一时间恢复数据库的读写访问。这通常可以称之为数据库的高可用而如何实现高可用的架构也一定是现代数据库需要解决的关键问题。对于MongoDB来说数据库高可用是通过副本集架构Replication Set实现的一个副本集由一个主节点Primary和若干个备节点Secondary所组成。一个典型的副本集架构如图所示。客户端通过数据库主节点写入数据后由备节点进行复制同步这样所有备节点都会同时拥有这些业务数据的副本当主节点发生故障而变得不可用时备节点能主动发起选举并产生新的主节点进行接管此时客户端仍然能继续进行访问这保证了业务的连续性。下面的这个过程更准确地描述了副本集的高可用机制搭建副本集各节点会进行初始化选举决定谁是主、谁是备。各节点都开始工作主节点负责接收客户端写入的数据备节点则负责复制这些数据。主节点发生故障备节点通过心跳检测到了问题。某个备节点率先发起新一轮选举一举成为新的主节点。客户端感知到主节点的变化将后续的数据写入指向新的主节点。可以发现在上述过程中并没有用到任何其他的技术。相比一些需要借助第三方HA组件实现高可用的数据库来说MongoDB自身就提供了高可用的能力。早期版本的MongoDB使用了一种Master-Slave的架构该做法在MongoDB 3.4版本之后已经废弃。为了进一步理解MongoDB副本集架构我们通常需要了解如下几个关键点选举机制。实时复制。故障转移。2、集群选举2.1、Raft选举算法MongoDB的副本集选举使用Raft算法来实现这是一种使用广泛的分布式一致性算法为了让读者更深入地理解MongoDB副本集中的一些概念我们先来了解一下这个算法。2.1.1、范例Raft选举的设计思路基本来自现实中的场景以民主选举中的总统大选为例每个总统通常都有一个任期阶段这是体制决定的一个周期性时间比如三到五年。在任期结束后又会重新进行选举来决定总统的任命。由于是民主社会总统的人选可以从普通的民众当中产生这些人需要先成为总统候选者Candidate​然后到处发表演讲以获得更多的支持。最终由选民进行公平的投票谁的票数多谁就能当上总统Leader​。当然在选举时可能会发生平票这种小概率事件那么就会进行新一轮的选举直到总统被选举出来。在总统上任之后他还要到处去演讲告诉所有人自己成为总统这个事实而这些事情都是为了巩固总统的地位。在上述案例中基本上已经说明了Raft协议的一些关键要素。那么接下来我们看看Raft协议具体是怎么定义的。2.1.2、协议中的角色Leader领导者Leader会向其他节点发送心跳同时负责处理客户端的读写操作包括将数据同步到其他节点。Follower追随者响应来自Leader和Candidate的投票请求如果在一定时间内没有收到Leader的心跳则会转换为Candidate。Candidate候选者Follower主动选举转换成为Candidate获得大多数投票后会成为Leader。在选举中有一个重要的概念叫任期Term​一个任期对应一次选举在Raft协议中任期被设计为单调递增的数字。每个节点上都会记录一个对应的任期代表它所处于的任期阶段​。在选举的过程中节点之间会通过任期的比较来解决冲突问题此时任期比较新的节点会被接受。在一定条件下上述几种角色会发生相互转换如图所示。2.1.3、选举流程在开始时所有节点都是Follower此时大家都没有办法收到Leader的心跳。接下来A节点出现等待超时率先发起选举并成为Candidate。A节点先是给自己投一票然后接着向其他节点发送投票请求一旦A节点获得了集群中大多数节点的投票则会成为Leader同时开始向其他节点广播心跳以此来声明自己的Leader角色。这个过程如图所示。上面的过程仅仅是最简单的情况实际上的投票选举则可能比这要复杂一些并且会伴随一些冲突或异常出现。为了保证能达到最终的一致性Raft协议还加入了以下细节。在同一个任期内每个节点最多只能给一个Candidate投票节点内部进行记录​任期内投票采用先到先得的原则。节点在收到Candidate的投票请求时只有当对方的任期、操作日志时间至少与自己的一样新时才会给它投票。Candidate发起投票后如果一直没有得到大多数票则会一直保持这个状态直到超时此后将继续发起新一轮任期的选举Term自增​如果在投票期间检测到了Leader的心跳其他Candidate率先完成选主​则会判断当前Leader的任期是否至少跟自己一样新如果是则降级为Follower并承认对方的Leader角色否则不予理会。无论是Candidate还是Leader节点一旦发现了其他节点有更新的任期Term值​都会自动降级为Follower。2.1.4、冲突选举过程中解决冲突的关键在于对Term值的判断。而在分布式环境中冲突的情况是必然会产生的下面列举了一些可能出现的场景。场景A.多个候选者竞争大多数获胜如图所示。场景B.多个候选者竞争平票如图所示。如果集群节点个数是偶数那么可能会产生平票也就需要进行下一轮选举。通过为每个节点增加一个随机的选举延期时间可以大大降低出现平票的概率。场景C.网络分区造成Term值不一致如图所示。如图所示当网络出现分区时B节点变得不可达仅剩下A、C节点进行选举。此时由于B节点会一直无法选举成功且会一直超时重试最终则造成该节点的Term值激增。一旦网络情况恢复则B节点将在很长时间内不会给其他节点投票由于自身Term值过高​而同时自身也无法获得其他节点的投票由于自身的日志太旧​这对于集群选举来说都是非常不利的。因此Raft协议中提出了一种预投票的手段即实现通过预先投票的方式试探自己能否选举成功只有预投票通过了才进行真正的投票而预投票不会造成Term值自增这样就解决了Term值差距太大的问题。2.2、MongoDB实现的扩展如前面所述MongoDB是基于Raft协议的在副本集选举、复制的机制中都能看到与标准Raft协议的影子。但在其具体的实现中MongoDB仍然添加了一些自己的扩展这包括支持chainingAllowed链式复制即备节点不只是从主节点上同步数据还可以选择一个离自己最近心跳延时最小的节点来复制数据。增加了预投票阶段即preVote这主要是用来避免网络分区时产生Term值激增的问题可以参照前面内容中提到的“4.冲突—场景C”​。支持投票优先级如果备节点发现自己的优先级比主节点高则会主动发起投票并尝试成为新的主节点。2.3、MongoDB选举介绍有了前面的理论基础我们就可以轻松地理解MongoDB副本集的一些设计了比如“大多数原则”的由来这是因为Raft协议的选举机制中Leader必须通过大多数节点投票才能产生。我们假设副本集内的投票成员数量为N则大多数为N/21。这个计算见下表。当副本集内存活的成员数量不足大多数时整个副本集将无法选举出主节点此时无法提供写服务这些节点都将处于只读状态。此外如果希望避免平票结果的产生最好使用奇数个节点成员比如3个或5个。当然在MongoDB副本集的实现中对于平票问题已经提供了解决方案为选举定时器增加少量的随机时间偏差这样避免各个节点在同一时刻发起选举提高成功率。使用仲裁者角色该角色不做数据复制也不承担读写业务仅仅用来投票。此外在一个MongoDB副本集中最多只能有50个成员而参与投票的成员最多只能有7个。这是因为一旦过多的成员参与数据复制、投票过程将会带来更多可靠性方面的问题。成员角色MongoDB为副本集成员提供了多种角色具体如下。Primary主节点其接收所有的写请求然后把修改同步到所有备节点。一个副本集只能有一个主节点当主节点“挂掉”后其他节点会重新选举出来一个主节点。Secondary备节点与主节点保持同样的数据集。当主节点“挂掉”时参与竞选主节点。Arbiter仲裁者节点该节点只参与投票不能被选为主节点并且不从主节点中同步数据。当节点宕机导致复制集无法选出主节点时可以给复制集添加一个仲裁者节点这样即使有节点宕机仍能选出主节点。仲裁者节点本身不存储数据是非常轻量级的服务。当复制集成员为偶数时最好加入一个仲裁者节点以提升复制集的可用性。Priority0优先级为0的节点该节点永远不会被选举为主节点也不会主动发起选举。通常在跨机房方式下部署副本集可以使用该特性。假设使用了机房A和机房B由于主要业务与机房A更近则可以将机房B的复制集成员Priority设置为0这样主节点就一定会是A机房的成员。Hidden隐藏节点具备Priority0的特性即不能被选为主节点Priority为0​同时该节点对客户端不可见。由于隐藏节点不会接受业务访问因此可通过隐藏节点做一些数据备份、离线计算的任务这并不会影响整个副本集。Delayed延迟节点必须同时具备隐藏节点和Priority0的特性并且其数据落后于主节点一段时间该时间是可配置的。由于延迟节点的数据比主节点落后一段时间当错误或者无效的数据写入主节点时可通过延迟节点的数据来恢复到之前的时间点。Vote0无投票权的节点必须同时设定为Priority0节点。由于一个副本集中最多只有7个投票成员因此多出来的成员则必须将其vote属性值设置为0即这些成员将无法参与投票。一般来说成员能否成为主节点主要受某些因素的影响这包括节点之间的心跳、节点优先级以及OpLog时间戳。而触发一次选举通常会来自下面的场景初始化一个副本集时。备节点在一段时间内发现不了主节点默认10s超时​由备节点发起选举。主节点放弃自己的角色比如执行rs.stepDown命令。2.4、副本集模式常见的副本集架构由3个成员节点组成其中存在几种不同的模式。2.4.1、PSS模式PSS模式由一个主节点和两个备节点所组成即PrimarySecondarySecondary如图所示。2.4.2、PSA模式PSA模式由一个主节点、一个备节点和一个仲裁者节点组成即PrimarySecondaryArbiter如图所示。其中Arbiter节点不存储数据副本也不提供业务的读写操作。Arbiter节点发生故障不影响业务仅影响选举投票。2.4.3、PSH模式PSH模式由一个主节点、一个备节点和一个隐藏节点组成即PrimarySecondaryHidden如图所示。其中Hidden节点对业务不可见同时无法被选举为主节点。一般利用Hidden节点来执行数据备份任务可以避免备份对业务性能产生影响。3、实时复制3.1、oplog复制在副本集架构中主节点与备节点之间是通过oplog来同步数据的这里的oplog是一个特殊的固定集合当主节点上的一个写操作完成后会向oplog集合写入一条对应的日志而备节点则通过这个oplog不断拉取到新的日志在本地进行回放以达到数据同步的目的。如果我们将oplog看作缓冲队列那么整个复制过程就是一个典型的”生产者-消费者“模式的应用。如图所示。这里的主节点就是生产者负责向自身的oplog队列中写入增量日志也就是产生数据变更的记录。备节点则作为消费者一方不断通过“pull”的方式拉取到这些增量日志进行消费。由于日志会不断增加因此oplog被设计为固定大小的集合它本身就是一个特殊的固定集合capped collection​当oplog的容量达到上限时旧的日志会被滚动删除。一个典型的oplog如下所示字段说明见下表。ts字段描述了oplog产生的时间戳可称之为optime。optime是备节点实现增量日志同步的关键它保证了oplog是节点有序的其由两部分组成当前的系统时间即UNIX时间至现在的秒数32位。整数计时器不同时间值会将计数器进行重置32位。optime属于BSON的Timestamp类型这个类型一般在MongoDB内部使用。既然oplog保证了节点级有序那么备节点便可以通过轮询的方式进行拉取这里会用到可持续追踪的游标tailable cursor技术如图所示。每个备节点都分别维护了自己的一个offset也就是从主节点拉取的最后一条日志的optime在执行同步时就通过这个optime向主节点的oplog集合发起查询。为了避免不停地发起新的查询链接在启动第一次查询后可以将cursor挂住通过将cursor设置为tailable​。这样只要oplog中产生了新的记录备节点就能使用同样的请求通道获得这些数据。tailable cursor只有在查询的集合为固定集合时才允许开启。通过db.currentOp命令可以看到具体的实现代码如下local.oplog.rs指向了oplog集合它存在于本地的local数据库中。local数据库里面的集合不会被同步到其他节点而且除了oplog, local库还包含一些具有特殊用途的集合具体如下。local.system.replset用来记录当前副本集的成员。local.startup_log用来记录本地数据库的启动日志信息。local.replset.minvalid用来记录副本集的跟踪信息如初始化同步需要的字段。3.2、幂等性每一条oplog记录都描述了一次数据的原子性变更对于oplog来说必须保证是幂等性的。也就是说对于同一个oplog无论进行多少次回放操作数据的最终状态都会保持不变。比如在一些原子性操作更新中我们用i n c 来使字段自增这个操作就不是幂等的对文档字段多次执行 inc来使字段自增这个操作就不是幂等的对文档字段多次执行inc来使字段自增这个操作就不是幂等的对文档字段多次执行inc操作每次都会产生新的结果。这些非幂等的更新命令在oplog中通常会被转换为$set操作这样无论执行了多少次文档的最终状态始终与第一次执行的效果一样。$inc操作代码如下{$inc:{count:1}}在oplog中转换为$set操作直接写入变更后的值代码如下{$set:{count:199}}3.3、复制延迟由于oplog集合是有固定大小的因此存放在里面的oplog随时可能会被新的记录冲掉。如果备节点的复制不够快就无法跟上主节点的步伐从而产生复制延迟replication lag问题。这是不容忽视的一旦备节点的延迟过大则随时会发生复制断裂的风险这意味着备节点的optime最新一条同步记录已经被主节点老化掉于是备节点将无法继续进行数据同步。为了尽量避免复制延迟带来的风险我们可以采取一些措施比如增加oplog的容量大小并保持对复制窗口的监视。通过一些扩展手段降低主节点的写入速度。优化主备节点之间的网络。避免字段使用太大的数组可能导致oplog膨胀​。oplog集合的大小oplog集合的大小可以通过参数replication.oplogSizeMB设置对于64位系统来说oplog的默认值为oplogSizeMBmin(磁盘可用空间*5%,50GB)对于大多数业务场景来说很难在一开始评估出一个合适的oplogSize所幸的是MongoDB在4.0版本之后提供了replSetResizeOplog命令可以实现动态修改oplogSize而不需要重启服务器。3.4、初始化同步在开始时备节点仍然需要向主节点获得一份全量的数据用于建立基本快照这个过程就称为初始化同步initial sync​。在MongoDB 3.4版本之后对于初始化同步做了不少改进我们来看看它是怎么完成的备节点记录当前的同步optimet1来自主节点的同步时间戳​进入STARTUP2状态。从主节点上复制所有非local数据库的集合数据同时创建这些集合上的索引。在这个过程中备节点会开启另外一个线程将集合复制过程中的增量oplogt1之后产生也复制到本地。将拉取到t1之后的增量oplog进行回放在完成之前节点一直处于RECOVERING状态此时是不可读的。oplog回放结束后恢复SECONDARY状态进入正常的增量同步流程。最关键的一点就是在全量复制过程中同时拉取了增量oplog因此我们不需要担心在复制完成之后主节点上的t1oplog记录被冲掉而导致初始化同步失败这大大提升了该过程的性能和可靠性。初始化同步对主节点仍然会有一定的性能影响因此在执行初始化同步之前需要考量当前系统的压力情况尽量选择在业务不繁忙时进行。同步源在前面的描述中笔者只提到了备节点和主节点之间的复制但实际上MongoDB是允许通过备节点进行复制的这会发生在以下的情况中。在settings.chainingAllowed开启的情况下备节点自动选择一个最近的节点ping命令时延最小进行同步。使用replSetSyncFrom命令临时更改当前节点的同步源比如在初始化同步时将同步源指向备节点来降低对主节点的影响。settings.chainingAllowed选项默认是开启的也就是说默认情况下备节点并不一定会选择主节点进行同步这个副作用就是会带来延迟的增加你可以通过下面的操作进行关闭cfgrs.confg()cfg.settings.chainingAllowedfalsers.reconfig(cfg)尽管存在备节点向备节点同步数据的情况笔者仍然选择将主节点同步作为主要的场景描述相比之下这样更加容易理解。3.5、数据回滚由于复制延迟是不可避免的这意味着主备节点之间的数据无法保持绝对的同步。当副本集中的主节点宕机时备节点会重新选举成为新的主节点。那么当旧的主节点重新加入时必须回滚掉之前的一些“脏日志数据”​以保证数据集与新的主节点一致。主备复制集合的差距越大发生大量数据回滚的风险就越高。对于写入的业务数据来说如果已经被复制到了副本集的大多数节点则可以避免被回滚的风险。应用上可以通过设定更高的写入级别writeConcernmajority来保证数据的持久性。这些由旧主节点回滚的数据会被写到单独的rollback目录下必要的情况下仍然可以恢复这些数据。4、自动故障转移在故障转移场景中我们所关心的问题是备节点是怎么感知到主节点已经发生故障的如何降低故障转移对业务产生的影响下面来看一些细节。下图是一个PSS一主两备架构的副本集主节点除了与两个备节点执行数据复制三个节点之间还会通过心跳感知彼此的存活。一旦主节点发生故障以后备节点将在某个周期内检测到主节点处于不可达的状态此后将由其中一个备节点事先发起选举并最终成为新的主节点如图所示。一个影响检测机制的因素是心跳在副本集组建完成之后各成员节点会开启定时器持续向其他成员发起心跳这里涉及的参数为heartbeatIntervalMillis即心跳间隔时间默认值是2s。如果心跳成功则会持续以2s的频率继续发送心跳如果心跳失败则会立即重试心跳一直到心跳恢复成功。另一个重要的因素是选举超时检测一次心跳检测失败并不会立即触发重新选举。实际上除了心跳成员节点还会启动一个选举超时检测定时器该定时器默认以10s的间隔执行具体可以通过electionTimeoutMillis参数指定如果心跳响应成功则取消上一次的electionTimeout调度保证不会发起选举​并发起新一轮electionTimeout调度。如果心跳响应迟迟不能成功那么electionTimeout任务被触发进而导致备节点发起选举并成为新的主节点。因此在electionTimeout任务中触发选举必须要满足以下条件当前节点是备节点。当前节点具备选举权限。在检测周期内仍然没有与主节点心跳成功。整个选举切换的逻辑如图所示。在MongoDB的实现中选举超时检测的周期要略大于electionTimeoutMillis设定。该周期会加入一个随机偏移量大约在1011.5s如此的设计是为了错开多个备节点主动选举的时间提升成功率。业务影响评估在副本集发生主备节点切换的情况下会出现短暂的无主节点阶段此时无法接受业务写操作。如果是因为主节点故障导致的切换则对于该节点的所有读写操作都会产生超时。如果使用MongoDB 3.6及以上版本的驱动则可以通过开启retryWrite来降低影响。如果主节点属于强制掉电那么整个Failover过程将会变长很可能需要在Election定时器超时后才被其他节点感知并恢复这个时间窗口一般会在12s以内。然而实际上对于业务呼损的考量还应该加上客户端或mongos对于副本集角色的监视和感知行为真实的情况可能需要长达30s以上​。对于非常重要的业务建议在业务层面做一些防护策略比如设计重试机制。5、搭建副本集5.1、安装副本集假设已经完成了MongoDB单节点的安装并且环境变量path中已经包含了MongoDB执行程序的配置。接下来我们需要为副本集定义一个名称例如myReplSet。1.准备安装目录2.配置文件执行cd/opt/work/mongoReplSet/进入安装目录。编辑配置文件mongo.conf内容如下上述配置仅包含一些公共的配置由于每个副本集成员会使用不同的端口、数据目录我们将在命令行中进行指定。3.创建keyfilemongo.key采用随机算法生成用作节点内部通信的密钥文件。4.启动副本集成员执行mongod程序启动3个副本集成员代码如下除了keyFile、config文件我们还指定了几个参数分别如下。--port数据库的监听端口。--dbpath数据的存储目录。--logpath数据库进程的日志文件路径。执行命令之后可以看到启动成功的输出日志通过netstat命令同样可以看到启动的几个端口输出如下5.初始化配置连接其中一个成员节点并执行初始化命令代码如下此处cfg._id表示的是副本集的名称myReplSet​该值必须和副本集成员启动时指定的–replSet参数保持一致。members则表示当前副本集的成员列表包括每个成员的主机IP、端口号。使用rs.initiate命令执行副本集的初始化当前成员会自动向其他成员同步该配置之后这些成员在内部完成选举。6.查看状态执行db.isMaster命令用于查看副本集的其他节点代码如下从输出上看当前节点已经成为主节点“ismaster”true​而hosts字段也展示了整个副本集的所有成员。5.2、创建用户由于使用了–keyFile作为成员节点的启动参数此时MongoDB会启用鉴权相当于–auth​因此在操作数据之前需要创建用户代码如下在开启鉴权的情况下MongoDB允许创建首个用户。一旦数据库中存在用户所有的操作就必须经过鉴权了。副本集之间的用户数据会自动进行同步因此可以使用同一个用户在任一成员节点登录。5.3、写入数据登录主节点向test集合写入一条数据代码如下登录某个备节点查看test集合可发现新增的数据已经同步如下5.4、主备节点切换接下来验证副本集的主备节点切换功能登录主节点并执行stepDown命令代码如下myReplSet:PRIMARYrs.stepDown()如果执行成功则当前节点将会降备并开启新一轮的选举。通过多次执行isMaster命令可以确认最后的选举结果代码如下从结果中可以看出在主备节点切换之后127.0.0.127002成为新的主节点。6、检查复制的延迟情况由于分布式环境中的各种不确定性因此对副本集的成员状态、复制延迟状态进行检查就变得非常重要。6.1、rs.status命令MongoDB对复制成员的监视可以使用rs.status命令我们可以登录任一节点进行查询代码如下members一列体现了所有副本集成员的状态主要如下。health成员是否健康通过心跳进行检测。state/stateStr成员的状态PRIMARY表示主节点而SECONDARY则表示备节点如果节点出现故障则可能出现一些其他的状态例如RECOVERY。uptime成员的启动时间。optime/optimeDate成员最后一条同步oplog的时间。optimeDurable/optimeDurableDate成员最后一条同步oplog写入Journal日志的时间。pingMs成员与当前节点的ping时延。syncingTo成员的同步来源。6.2、查看复制延迟如果希望查看当前节点oplog的情况则可以使用rs.printReplicationInfo命令代码如下这里清晰地描述了oplog的大小、最早一条oplog以及最后一条oplog的产生时间log length start to end所指的是一个复制窗口时间差​。通常在oplog大小不变的情况下业务写操作越频繁复制窗口就会越短。在节点上执行rs.printSlaveReplicationInfo命令可以一并列出所有备节点成员的同步延迟情况代码如下