MongoDB - 副本集简介
简介
在 MongoDB 中,副本集指的是一组 MongoDB 服务器实例掌管同一个数据集,实例可以在不同的机器上。
其中一个用于处理写操作的是主节点(Primary),还有多个用于保存主节点的数据副本的从节点(Secondary)。如果主节点崩溃了,则从节点会从其中选取出一个新的主节点。
副本集保证数据在生产部署时的冗余和可靠性,通过在不同的机器上保存副本来保证数据不会因为单点损坏而丢失,能够随时应对数据丢失、机器损坏带来的风险。
从另一个角度上看,还能提高读取能力,用户的读取服务器和写入服务器在不同的地方,由不同的服务器为不同的用户提供服务,提高了整个系统的负载能力。
节点成员
副本集中的节点主要分为三种:主节点 Primary、从节点 Seconary、仲裁节点 Arbiter。
主节点
主节点包含了所有的写操作的日志。
但是副本服务器集群包含有所有的主服务数据,因此当主服务器挂掉了,就会在副本服务器上重新选取一个成为主服务器。
从节点
正常情况下,副本集的从节点会参与主节点选举,并从主节点同步最新写入的数据,以保证与主节点存储相同的数据。
通常,从节点提供读服务,增加从节点可以提供副本集的读服务能力,同时提升副本集的可用性。
仲裁节点
仲裁节点只参与投票,不能被选举为主节点,并且不从主节点同步数据。
当副本集成员为偶数时,最好加入一个仲裁节点,以提升副本集的可用性。
当然,如果可以的话,最好使用没有仲裁者的部署。添加额外的仲裁者并不能加快选举速度,也不能提供更好的数据安全性,仅仅能使得副本集成员数为奇数防止选举出现平票。
被动成员
给从节点设置 priority
可以指定其成为主节点的优先级,它的取值范围是 0 到 100,默认是 1。
优先级为 0 的从节点不参与选举,这样的从节点被称为被动成员。
拥有最高优先级的成员总是会被选举为主节点(只要它能连接到副本集中的大多数成员,并且拥有最新的数据)。
隐藏成员
给从节点设置 hidden
为 true
可以将其作为隐藏成员,隐藏成员只对 isMaster 不可见。
客户端不会向隐藏成员发送请求,隐藏成员也不会优先作为副本集的数据源(尽管当其他复制源不可用时隐藏成员也会被使用)。
通常会将性能较弱的服务器或者备份服务器隐藏起来,因此,隐藏成员适合做数据备份、离线计算的任务。
成员状态
成员之间通过心跳来传达自己的状态。最常见的状态就是“主节点”和“从节点”状态,其他的一些状态如下:
- STARTUP: 成员在第一次启动时的状态,正在尝试加载副本集配置
- STARTUP2: 配置被加载后进入这个状态,初始化同步过程会持续处于这个状态
- RECOVERING: 成员运行正常,但不能处理读请求
- ARBITER: 仲裁节点独有的特殊状态
- DOWN: 一个成员被正常启动,但后来变为不可访问
- UNKNOWN: 如果一个成员未能访问到另一个成员,那么就不知道它处于什么状态
- REMOVED: 此成员已被从副本集中移除
- ROLLBACK: 成员正在回滚数据中会处于此状态
部署架构
一主两从
当主节点宕机时,两个节点都会参与选举,其中一个会变成主节点。
当原主节点恢复后,将会作为从节点加入当前的副本集群。
一主一从一仲裁
当主节点宕机时,将会选择从节点称为主节点。
当原主节点恢复后,将会作为从节点加入当前的副本集群。
推荐配置
第一种方案是:将“大多数”成员放在一个数据中心。只要主数据中心正常运转,就会有一个主节点。如果主数据中心不可用了,那么备份数据中心将无法选举出主节点。
第二种方案是:在两个数据中心各自放置数量相等的成员,在第三个地方放置一个用于打破僵局的副本集成员。
复杂的需求可能需要不同的配置,但都需要考虑副本集在不利条件下如何满足“大多数”的要求。
数据同步
操作日志
MongoDB 的操作日志是一个特殊的有上限的集合(老的日志会被覆盖),保存了所有数据库中存储数据的修改操作的滚动记录。
当主节点执行数据库写操作时,会将这些操作记录到主节点 local 数据库中的一个固定集合中,然后从节点通过异步进程复制和应用(数据同步)这些操作。
每个从节点都维护自己的操作日志,用于记录从主节点复制的每个操作,这使得每个成员都可以被用作其他成员的同步源。
操作日志中的每个操作都是幂等的,同一个操作执行多次和只执行一次效果是一样的。
在大多数情况下,默认的操作日志大小就足够了。通常以下情况需要更大的操作日志空间:
- 一次更新多个文档。操作日志为了保持幂等性会将多文档更新转换成多个单独操作
- 删除数据量与插入数据量相同。这种情况的磁盘占用变化不大,但是操作日志的大小可能很大
- 大量的原地(in-place)更新。这种情况的磁盘占用变化不大,但是操作日志的大小可能很大
初始同步
这里的初始同步可以理解成全量同步,会使用完整的数据集填充新成员。会有以下场景触发:
- 新节点加入副本集时,没有任何操作日志,此时会触发初始同步
- 上次全量同步中途失败后重新加入副本集,此时会触发初始同步
- 当用户发送
resync
命令时,内存标记initialSyncRequested
被设置为true
,此时会触发初始同步
全量同步的流程如下:
- 全量同步开始,设置 minvalid 集合的
_initialSyncFlag
为true
- 获取同步源上最新操作日志时间戳为 t1
- 全量同步集合数据(耗时)
- 获取同步源上最新操作日志时间戳为 t2
- 重放 [t1, t2] 范围内的所有操作日志
- 获取同步源上最新操作日志时间戳为 t3
- 重放 [t2, t3] 范围内所有的操作日志
- 建立集合所有索引(耗时)
- 获取同步源上最新操作日志时间戳为 t4
- 重放 [t3, t4] 范围内所有的操作日志
- 全量同步结束,清除 minvalid 集合的
_initialSyncFlag
复制数据
这里的复制可以理解为增量同步,在初始同步结束之后,从节点就会持续同步新的操作日志并重放。
复制的流程比较复杂,会涉及到几个线程,其流程如下:
- 生产者线程会不断从同步源上拉取操作日志,并加入到一个阻塞队列里保存
- 批处理线程会逐步将阻塞队列里的操作日志取出,并放到自己维护的队列里
- 同步线程将批处理线程的队列分发到默认 16 个重放线程,由重放线程最终重放每条操作日志
拉取操作日志是单线程进行的,如果把重放的操作也放在这个线程,同步势必会很慢,所以设计上生产者线程只做拉取操作日志的工作。
在重放操作日志时,要保持顺序性,而且遇到 createCollection()
、dropCollection()
等 DDL 命令时,这些命令与其他的增删查改是不能并行执行的,这些控制都有批处理线程处理。
注意事项
- 初始同步是单线程复制数据,效率比较低,在生产上应尽量避免做全量同步
- 合理配置操作日志的大小,按默认 5% 的可用磁盘空间配置可满足绝大多数场景
- 新加入节点时,可以通过物理复制的方式来避免初始同步,将主节点的数据拷贝到新的节点,这样效率更高
- 当从节点需要的操作日志在同步源上已经滚掉,从节点的同步将无法进行,需要从节点主动发送
rsync
命令同步 - 生产环境使用
db.printSlaveReplicationInfo()
命令监控主备同步滞后的情况 - 当从节点因为主节点并发写入太高导致同步滞后,可通过调整从节点的重放线程数来提升
数据高可用
选举机制
在副本集中,通过选举机制来选择主节点,选举主节点的规则如下:
假设副本集内能够投票的成员是 N 个,当副本集内存活数量不足 \(\frac{N}{2} + 1\) 个时,整个副本集将无法选举出主节点,副本集将无法提供写服务,处于只读状态。
举例:3 个投票节点需要 2 个节点的赞成票,容忍选举失败次数为 1;5 个投票节点需要 3 个节点的赞成票,容忍选举失败次数为 2;通常投票节点为奇数,这样可以减少选举失败的概率。
触发时机
当出现以下情况时,会触发选举机制:
- 初始化副本集时
- 往副本集中新加入节点
- 对副本集进行维护时,比如执行
rs.stepDown()
或者rs.reconfig()
操作时 - 从节点失联时,比如超时(默认是 10 秒)
影响因素
以下因素会影响到选举结果:
- 副本集的选举协议
- 心跳
- 成员权重
- 数据中心失联
- 网络分区
- 镜像读取
故障转移回滚
回滚指的是,当成员在故障转移后重新加入其副本集时,将还原之前主节点上的写操作,并恢复成现在主节点的状态数据。
仅当节点接收到主节点降级前未成功复制的写操作后,重新加入副本集群之后发现与现有主节点的数据不一致时,才需要回滚。
当节点重新加入到副本集群时,它会还原或“回滚”其不一致的写操作,以保持与其他成员的一致性。
与副本集交互
客户端连接
对于副本集,默认情况下,驱动程序会连接到主节点,并将所有流量都路由到此节点。
对于应用程序,可以像与单机服务器通信一样执行读写操作,同时副本集会在后台悄悄地处理热备份。
你不需要列出服务器地址列表中的所有成员(尽管这样做也可以)。当驱动程序连接到服务器时,它可以从其中发现其他成员。一个连接字符串通常看起来像下面这样:
mongodb://server-1:27017,server-2:27017,server-3:27017
如果想提供更强的容错能力,那么也可以使用 DNS 种子列表连接格式来指定应用程序连接到副本集的方式。
使用 DNS 的优点是可以轮流更改 MongoDB 副本集成员所在的服务器,而无须重新配置客户端。
重试策略
用户希望驱动程序对其隐藏整个选举过程(主节点退位,新的主节点被选举出来)。然而,由于一些原因,没有驱动程序能够以这种方式处理故障转移。
驱动程序经常因为操作失败而发现主节点已停止运行,这意味着驱动程序不知道主节点在停止运行之前是否处理了该操作。
这是一个不可避免的分布式系统问题。事实证明,正确的策略是最多重试一次。
要解释清楚这一点,需要先看一下都有哪些策略可供选择。归结起来就是:不重试、在重试一定次数后放弃或者最多只重试一次。
我们还需要考虑错误的类型,这可能是问题的根源。在尝试对副本集进行写操作的过程中,可能
会遇到 3 种类型的错误:
- 短暂的网络错误
- 持续的中断(网络或服务器)
- 由服务器拒绝的错误命令(比如未授权)引起的错误
对于短暂的网络错误而言,如果遵循重试一定次数的策略,则可能会发生计数过多现象(在第一次尝试成功的情况下)。对于持续中断或命令错误,多次重试只会浪费资源。
再来看一下仅重试一次的策略。对于短暂的网络错误,可能会发生计数过多现象。对于持续的中断或命令错误,这是正确的策略。
然而,如果可以确保操作是幂等的会如何?无论做一次还是多次,幂等操作都会有相同的结果。利用幂等操作,在发生网络错误时重试一次最有可能正确处理所有 3 种类型的错误。
读写优先级
默认情况下,副本集的所有读请求都发送到主节点,Driver 可通过设置 Read Preference
来将请求路由到其他节点。规则如下:
primary
: 默认规则,所有读请求发送到主节点primaryPreferred
: 主节点优先,如果主节点不可达,请求从节点secondary
: 所有读请求发送到从节点secondaryPreferred
: 从节点优先,当所有从节点不可达时请求主节点nearest
: 读请求发送到最近的可达节点上(通过ping
探测得出最近的节点)
读请求选择
其实,将读请求发送到从节点通常并不是一个好主意,在一些情况下,通常更建议将读请求发送到主节点而不是从节点。
对于数据一致性要求非常高的应用程序,更推荐从主节点读取数据。这是由于从节点的数据通常会落后于主节点,并且这个时间有可能因其他原因导致更长。
如果将读请求发送到从节点以分配负载,有可能会因为一个节点崩溃而导致整个副本集出现过载的情况,这个会导致恶性循环。一个更好的选择是使用分片来分配负载。
注意事项
在生产环境中,应该始终使用副本集并为每个成员分配一个专用主机,以避免资源争用,并针对服务器故障提供隔离。
为了提供更多的弹性,还应该使用 DNS 种子列表连接格式指定应用程序如何连接到副本集。其优点在于可以轮流更改托管 MongoDB 副本集成员的服务器,而无须重新配置客户端。
副本集中的每个成员都必须能够连接到其他成员(包括自身)。但是 MongoDB 3.6 中 mongod 仅在默认情况下绑定到 localhost(127.0.0.1) 地址上,这个通常需要根据服务本身的地址做配置修改。
并且,在绑定到非 localhost 的地址之前,应该启用授权控制并指定身份验证机制。
另外,最好对磁盘上的数据和副本集成员之间以及副本集与客户端之间的通信进行加密。
需要注意的是,不能在不停止运行的情况下将单机服务器转换为副本集,以重新启动并初始化该副本集。
因此,即便一开始只有一台服务器,你也希望将其设置为一个单成员的副本集。这样,就可以在不停止运行的情况下进行添加。
常见问题
MongoDB 副本集和 MySQL 主从的区别?
从节点读写模式
MySQL 中将主从同步的从库设置为只读状态,限制了普通用户只能进行读的操作,但限制不了超级权限用户对数据进行修改操作,这种情况容易造成主键冲突。
MongoDB 中只有主节点才可进行写操作,从节点是决不允许写数据的,对数据的一致性有着更高的保证。
主节点唯一性
MongoDB 中主节点是唯一的,其余均为从节点,但主节点不是固定不变的,集群内部有容灾机制。
MySQL 提供了双主架构方案,MasterA 和 MasterB,MasterA 可以做为 MasterB 的主库,而 MasterB 也可以做为 MasterA 的主库,两者互为主从。
复制过程中是同步还是异步
MySQL 在 5.5 版本之后提供了半同步复制模式,是介于异步复制和同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到 relay log
中才返回给客户端。相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个 TCP/IP 往返的时间。所以,半同步复制最好在低延时的网络中使用。
MongoDB 的同步模式是完全异步的。
MongoDB 副本集的最大节点数为多少?
在副本集中,每个节点会向其他节点发送心跳请求,间隔时间为 2 秒,默认 10 秒为超时。从这个角度上看,副本集也相当于无中心架构。
当副本集中节点增加时,心跳请求的数量将会以指数级的数量增加,单单是心跳请求对资源的占用也会很大。
因此,在 MongoDB 中副本集的限制为最大 50 个,同时只有 7 个成员拥有投票权。
MongoDB 主节点宕机之后如何进行选举?
副本集中的健康节点大于集群节点的 \(\frac{1}{2}\) 时,集群才可正常选举,否则集群将不可写,只能读。
这个限制会存在一个情况:副本集原本有 3 个节点,但是其中 2 个从节点因为异常挂掉了,那么集群检测之后主节点也将会降级为从节点,只接受读,不再接受写入。
官方推荐在副本集中有投票权的节点数量为奇数个,主要是为了避免出现脑裂(一个集群被分成了多个集群)的情况。