MongoDB 高可用之复制集架构
MongoDB复制集
在生产环境中一般不会使用单机的MongoDB
- 单机节点一旦宕机,业务将直接不可用。
- 单机节点如果硬盘存坏,数据则永久丢失。
所以我们一般会搭建分片集群或者 复制集来确保服务高可用。
1. 复制集架构
复制集是由 一组MongoDB实例组成的,包含了一个Primary节点和多个Secondary节点。MongoDB 客户端的数据会写入到Primary中,Secondary会从Primary的opLog文件同步数据,而Secondary提供数据冗余的作用。
复制集可以理解为主从架构,拥有Primary节点(提供读写)和Secondary节点(冗余备份),当Primary宕机后 Secondary会马上进行选举,选举出新的Primary来对外提供读写服务。
注意:mongoDB集群最多支持50个节点,参与选举的节点只能有7个。
复制集架构优点:
- 高可用:当Primary节点宕机后,Secondary节点会进行选举,选举出新的Primary对外提供服务,确保服务的可用性
- 冗余备份:所有的数据在Secondary都会提供备份
- 数据分发:将一个区域的数据快速复制到另外一个区域
- 读写分离:默认情况下,读写服务均由 Primary 节点提供
- 必要情况下可以进行读写分离,Primary节点提供写入服务,Secondary提供读取服务
- 异地容灾:同城跨机房备份,或者两地三中心,当数据中心故障了进行快速切换
关于复制集的一些注意事项
只有一个单机节点也要搭建复制集
- 为了便于以后的扩展,即便是单机节点也要以单节点启动复制集,复制集里只存在一个Primary节点,以后扩展的话只需要加入其他的节点到复制集就可以。
为了避免出现不可预知的问题,集群中每个节点的版本号必须一致
增加节点无法提高写性能
- 因为写操作只能通过 主节点,从节点不能提供写操作。在怎么增加节点,也只能增加从节点。如果想提高写入的性能,只能采用分片架构,进行写热点分散。
2. 三节点复制集模式
三节点是最常见的复制集架构,三节点也存在两种模式:1、PSS 2、PSA。
PSS模式(官方推荐模式)
PSS模式由一个主节点和两个备用节点组成,即 Primary+Secondary+Secondary
当Primary节点宕机后,Secondary节点会进行选举,选举出新的Primary对外提供服务。
PSA模式(基本没人用)
PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter
Arbiter节点(仲裁节点)不存储数据,也不提供读写服务,仅影响选举,目的是为了在Secondary中选举出新的节点。
3. 复制集群搭建
环境准备
- 需要3台Linux服务器,并且配置好MongoDB环境变量,且3台节点 MongoDB版本必须保持一致
- 确保每台节点至少有10G以上的空间
1)配置别名解析
vim /etc/hosts
在3个节点上都修改hosts,这样就可以通过别名直接访问对应的IP节点
1 | vim /etc/hosts |
2)初始化MongoDB服务端
启动mongoDB有两种方式:
- 命令行的形式直接启动
- 通过配置文件启动,这里我们用后者
初始化mongoDB服务端
1 | 下载MongoDB并解压 |
修改配置文件
在3个节点上都新增配置文件 vim /data/db/mongod.conf
1 | systemLog: |
该配置等同于命令行
mongod –bind_ip 0.0.0.0 –replSet rs0 –dbpath /data/db –logpath /data/log/mongod.log –port 27010 –fork wiredTigerCacheSizeGB 1
replication 等同于 –replSet
启动MongoDB服务
1 | 在3个节点上都启动mongoDB |
3)配置复制集
初始化复制集
只需要在一个节点上 进行初始化就行,members 其他的节点会自动参与选举并完成初始化。
1 | 进入mongod shell,注意:这里实验我们没有开启auth 认证模式,可以根据实际情况开启 |
初始化的_id 必须是 启动时指定的复制集名称
后续如果需要增加节点的话,可以通过命令:rs.add(“xxxx:27010”) 来动态添加节点
查看复制集状态
1 | rs0:SECONDARY> rs.status() |
查看各成员状态,健康与否,全量同步信息,心跳信息,增量同步,选举信息等。
- members:复制集成员状态和信息
- health 健康状态,1 健康 0 不健康,节点之间会通过心跳进行检测
- state/stateStr 节点状态,PRIMARY表示主节点,而SECONDARY则表示备节点,当节点故障后会显示其他状态
- uptime 成员启动时间
- optime/optimeDate:成员最后一条同步oplog的时间。
- optimeDurable/optimeDurableDate:成员最后一条同步oplog的时间。
- pingMs:成员与当前节点的ping时延。
- syncingTo:成员的同步来源。
查看Master节点
1 | rs0:SECONDARY> db.isMaster() |
4)验证测试
从Master节点写入数据
1 | rs0:PRIMARY> db.employee.insert([{name:"xing"},{name:"monkey"}]) |
从Slave节点读取数据
slave节点默认是没有读取数据权限的,如果直接访问的话会报错:
1 | 指定可以从slave节点读取数据 |
查看同步进度oplog信息
1 | rs0:SECONDARY> rs.printSecondaryReplicationInfo() |
返回oplog 大小、保留时长、 起始时间等信息
1 | rs0:SECONDARY> rs.printReplicationInfo() |
5)开启安全认证
默认情况下mongoDB是不进行认证的,但是这样安全隐患很高,所以我们要开启认证模式。
需要开启认证模式的话,有2个主要步骤:
- 创建用户并授予权限
- 创建keyFile文件
- 在集群中想开启安全认证的话,除了要创建用户外,还要创建keyFile文件(集群节点之前通信的秘钥)
在主节点上创建用户
1 | 进入admin数据库 |
关闭服务
关闭服务的话,不可以直接kill,而是应该通过命令关闭服务端
1 | [root@S2 bin]# mongod -f /data/db/mongod.conf --shutdown |
创建keyFile文件
keyFile用于集群节点之间通信数据加密,保证集群节点安全。如果开启了auth模式的话,集群节点必须设置keyFile,否则无法启动服务
1 | 通过openssl创建秘钥 |
注意:创建keyFile之前必须要停掉集群节点内所有的mongoDB服务,否则可能会出现 服务启动不了的情况。
同步keyFile文件
将master节点生成好的keyFile文件同步到其他集群节点上,并设置权限为600,文件权限必须设置否则会无法运行
1 | 为keyFile 设置600 可运行的权限 |
以keyFile文件启动服务
1 | [root@S1 /]# mongod -f /data/db/mongod.conf --keyFile /data/mongo.key |
登录mongo shell
1 | [root@S1 data]# mongo --port=27010 -uxing -p123123 --authenticationDatabase=admin |
6)MongoDB客户端连接方式
连接Primary节点
MongoDB 客户端直接连接Primary节点,但是当Primary节点宕机后,就会无法正常访问
高可用Uri连接方式(推荐)
通过高可用 Uri 的方式连接 MongoDB,当 Primary 故障切换后,MongoDB Driver 可自动感知并把流量路由到新的 Primary 节点。
SpringBoot application.yaml
1 | spring: |
在uri里面将所有的主节点和备用节点都写上,当主节点故障后,Mongo DB客户端会将流量 转发到 到新的主节点
4. 复制集成员角色
- Priority 选举优先级,Priority = 0 不可以选举成主节点。Priority越高,选举成主节点概率越大。
- Vote 选举权限,Vote = 0 表示该节点不可以参与选举(Priority 也必须为0),Vote = 1 表示可以参与选举。
- 一个复制集中最多有50个成员,只有7个成员可以参与选举,其他的成员vote必须设置成0。
1)成员角色
- Primary 主节点,负责所有写请求。一个复制集只存在一个主节点,当主节点挂掉后,其他Secondary会选举出新的Primary节点。
- Secondary 从节点,拥有和主节点同样的数据集,当主节点宕机后,从节点会参与选举并选举出新的主节点
- Hidden = false (默认):正常是 不隐藏的只读节点,是否参与选举,优先级具体看Priority、Vote
- Hidden = true 隐藏节点,不会接收客户端的请求
- 可以参与选举,但是 Priority 必须为 0,不可以选举为主节点
- 一般用于 冗余备份,数据处理任务
- Delayed 延迟节点,必须同时满足 隐藏节点+不可以选举成主节点(Priority = 0)
- 延迟同步:延迟一定时间(取决于 slaveDelay)从上游同步数据
- 我们将C节点设置延迟同步一小时,当主节点A 误删数据后,需要一个小时后 才会同步到C节点上。那么就可以在这一小时内进行数据回滚。
- 延迟同步:延迟一定时间(取决于 slaveDelay)从上游同步数据
- Arbiter 仲裁节点,只参与投票(给Secondary投票),不处理也不同步任何数据。
2)配置隐藏节点
隐藏节点更多情况下是为了协助配置 延迟节点的,配置隐藏节点后 该节点不会接收客户端请求。
1 | # 将配置文件保存到config变量 |
没有修改配置之前
修改配置之后
3)配置延时节点
配置延时节点 如 延时60秒,同步时间将会比复制集中其他的Secondary节点 延时60秒。
1 | # 将配置文件保存到config变量 |
配置延时节点的话,必须先设置成 隐藏节点 + 延时时间 。
查询所有备节点成员的同步延迟情况
1 | rs0:SECONDARY> rs.printSecondaryReplicationInfo() |
4)复制集节点操作
添加复制集节点
1 | # 1.关闭节点实例 |
更改复制集节点
1 | config = rs.conf() |
移除复制集节点
1 | # 1.关闭节点实例 |
5. 复制集高级特性
1)复制集选举
MongoDB的复制集选举使用Raft算法(https://raft.github.io/)来实现,在Raft基础之上增加了一些扩展:
- 支持chainingAllowed链式复制,即备节点不只是从主节点上同步数据,还可以选择一个离自己最近(心跳延时最小)的节点来复制数据。
- 增加了预投票阶段,即preVote,这主要是用来避免网络分区时产生Term(任期)值激增的问题
- 支持投票优先级,如果备节点发现自己的优先级比主节点高,则会主动发起投票并尝试成为新的主节点。
一个复制集最多可以有50 个成员,但只有 7 个投票成员。如果投票成员过多,反而会造成性能下降。
投票成员数 | 大多数 | 容忍失效数 |
---|---|---|
1 | 1 | 0 |
2 | 2 | 0 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 3 | 2 |
6 | 4 | 2 |
7 | 4 | 3 |
如果有3个投票成员,损失1个投票节点,也可以完成选举。
如果有5个投票成员,损失2个投票节点,也可以完成选举。以此类推。
注意:当复制集内一半以上的节点宕机后,整个集群都无法选举出新的主节点,没有主节点就无法对外提供写入服务,但是从节点依然可以提供读服务。
为了避免出现选举平票的情况,搭建的集群节点数量最好是 奇数节点,如3、5、7等。
2)Raft算法选举规则
每台节点 每次选举的时候 都会有一个随机计时器,第一次选举时 先往自身投一票,然后谁的计时器先过期,在往自身再投一票,此时一共有2票,然后向集群中所有节点发送拉票请求
其他节点收到拉票请求后,如果自身计时器还没过期的话,就会重置 随机计时器,并成为从节点,然后 返回ACK给拉票方
S5第一次选举收到所有节点的投票回复后,自己就会成为PRIMARY节点。
后续Primary节点会持续的给集群中所有从节点发送心跳检测,从节点收到心跳检测后 会重置随机计时器并回复Primary节点。
- 如果有哪台从节点没有 返回ACK的话,Primary节点就会认定 该节点挂了。
如果S5 主节点挂掉了,那其他的从节点 随机计时器到期后 会重复第一个动作,给自己再投一票(那就有2票),然后给集群中其他从节点发送拉票请求,重新选举主节点。
-
主节点宕机后,从节点的计时器到期后会尝试将自身选举为主节点
-
选举时特殊情况
- S5和S1的计时器很接近,但是S5的计时器先到期随后就是S1到期。
- S5和S1都会向其他节点发送拉票请求。至于最后谁成为了主节点,就看谁先收到半数以上的节点 ACK确认。
- 谁先成为主节点(如 S5)后,会给集群中所有节点发送心跳检测告知 他自己成为了主节点,而之前和它竞争的S1节点 就会主动变成 从节点。
平票问题
假设S5和S1 计时器时间一致,就会同时向集群中其他从节点发送拉票请求,其他节点如果回复的时间也一致的话,就很可能出现 S5和S1收到的票数一样,就会出现 平票的情况。
那么S5和S1就会Term(任期)加1,用于记录选举的次数,然后继续开启新的一轮投票,直到选举出新的主节点。
网络分区导致的问题
1、第一次选举时,S1成了主节点,S2、S3成了从节点,S1正常给其他从节点发送心跳检测。
2、如果突然网络出现了波动,出现了网络分区。主节点的心跳检测无法发送给从节点了。那么从节点计时器到期后就会开始新的一轮选举。
而主节点由于一直选举失败,开启新的选举时,Term 会自增。当网络分区消失后,S2成了新的主节点,而S1也是主节点,但是S1的Term 比较高,所以 S1 依然会成为 主节点。这里就会存在一个问题,因为S1的数据是旧的,就会导致 数据不一致。
MongoDB为了解决这个问题,就引入了一个新的概念:预投票阶段 preVote,如果主节点发送投票失败的话,就不会增加Term 任期。
3)自动故障转移
- 复制集组建完成后,各成员 根据 心跳间隔时间(默认2s) 向其他成员发送心跳检测。
- 如果收到正确响应则持续以2s的频率保持心跳
- 如果心跳失败,则立即重试 直到成功或超时
- 如果超过 心跳超时时间(默认10s) 没有收到响应就认定对方节点已经宕机
如果超过10s没有收到 主节点正确响应,则认为主节点已经宕机,触发electionTimeout任务进行主节点选举。
触发electionTimeout任务必备条件:
- 当前节点 必须是备用节点且具备选举权限
- 超过心跳超时时间(默认10s)后,依然没有与主节点心跳检测成功
rs.conf()
可重试写入降低主从切换时间
当主节点宕机后,从节点开始进行选举,当选举出主节点后 进行主备切换。在切换的过程中会 主节点无法对外提供写入服务。
为了避免等待过程太长,我们可以开启retryWrite来降低等待时间:
开启retryWrite来降低影响
1 | # MongoDB Drivers 启用可重试写入 |
如何不丢数据重启复制集
如果正常重启复制集的话,会丢失部分数据。如果想不丢失数据的话,我们应该按以下流程 进行重启复制集:
- 逐个重启复制集所有的Secondary节点
- 对Primary发送rs.stepDown()命令,等待primary降级为Secondary,降级成功后进行重启
- Secondary节点会重新选举Primary节点
4)复制集数据同步机制
什么是oplog
oplog是mongodb local库下的一个 固定集合,用来保存 写操作产生的增量日志,从节点根据oplog完成 数据同步。
它是固定集合,当写入的数据超出了集合最大限制,会自动删除之前的旧数据。
在复制集架构中,主节点与备节点之间是通过oplog来同步数据的。
同步流程:
- 客户端向主节点写入数据,主节点会 插入数据、插入索引(如果有的话)、插入oplog日志 到local下的local.oplog.rs集合中
- 从节点会与主节点建立长连接,然后不断去主节点中读取oplog数据 并存到自己本地进行重放,完成数据同步
查看oplog日志
local数据库里面包含的表:
1 | rs0:SECONDARY> use local |
ts: 日志写入时间,当前timestamp + 计数器,计数器每秒都被重置。
v:oplog版本信息
op:操作类型:
i:插⼊操作
u:更新操作
d:删除操作
c:执⾏命令(如createDatabase,dropDatabase)
n:空操作,特殊⽤途
ns:操作针对的集合
o:操作内容
o2:操作查询条件,仅update操作包含该字段
每个从节点都会维护一个offset(游标),就是oplog日志的ts时间戳字段。从节点拉取数据会保存时间戳,下次再次拉取时在该时间戳之后拉取新的数据。
oplog集合的大小
oplog集合的大小可以通过参数replication.oplogSizeMB设置,默认值:oplogSizeMB = min(磁盘可用空间*5%,50GB)。
MongoDB在4.0版本之后可以实现不重启服务器 动态修改oplogSize:
1 | 将复制集成员的oplog大小修改为60g 指定大小必须大于990M |
幂等性
为了避免oplog 多次重放,导致 数据不一致的情况发生,对于oplog来说必须保证幂等性。不论多少次重放,数据始终保持不变。
oplog如何保证幂等性
不论是新增还是修改数据,主节点都只会把最终数据写入oplog。这样不论重放多少次,数据始终不变。
举个例子:age = 23 , 客户端发送了一条自增命令 {$inc: {age: 1}},primary会自动转换成{$set: {age: 24 } 。 把修改之后的结果 set到oplog日志。
幂等性带来的代价
简单元素的操作,$inc 转成$set并没有什么影响。但是如果是数组元祖的话且数据量大的话,就会带来较大的开销。
实验如下:
插入数组数据
1 | rs0:PRIMARY> db.user.insertOne({name:"test-02",tags:[1,2,3]}) |
往尾部push2个元素
1 | rs0:PRIMARY> db.user.update({ "_id" : ObjectId("65ae2224ff7763cced2441b0")}, {$push: {tags: { $each: [4, 5] }}}) |
往数组头部push元素(出问题的点)
1 | rs0:PRIMARY> db.user.update({ "_id" : ObjectId("65ae2224ff7763cced2441b0")}, {$push: {tags: { $each: [-1,0],$position: 0 }}}) |
从数组头部插入数据的话,为了保证幂等性,oplog日志 会整个数组 以$set的形式 写入到oplog,如果数据量大的话,这样效率非常低,$sort,$slice,$pull, $addToSet等更新操作符也是类似。
大数组更新导致的oplog覆写
当对大数组进行更新时,哪怕只是一个微小的更新,也可能会将整个数组写入到oplog中。导致oplog日志没同步完就又被写满了,旧的数据会被删除掉(覆盖写),导致部分从节点更新不上,就会出现同步中断的情况。
案例
在生产环境中,用户文档有一个数组字段,数组里面有1000个元素 64KB,这个数组的元素按时间反序存储,每次插入数据都会放到数组的最前面。
Primary每次往数组里面插入新元素时,oplog会将整个数组以$set的形式 写入到oplog。Secondary会拉取oplog日志进行重放达到同步数据。当有大量的修改或插入操作时,oplog很快就会被占满,部分Secondary节点还没来得及同步,Primary的oplog日志就就被覆写了,导致部分Secondary节点数据同步不一致。
如何避免oplog覆写问题
- 数组的元素不要太多
- 尽量避免对数组的更新操作
- 如果一定要更新,只能在尾部插入元素
复制延迟
由于oplog集合时固定大小的,如果Secondary节点因为 延迟或其他原因 同步不够快而oplogoplog日志满了被Primary节点删除掉的话,就会出现 复制延迟的情况,导致最终结果 主从数据不一致。
为了避免复制延迟的情况发生,我们可以根据实际情况采取以下的手段:
- 增加oplog容量
- 优化主从节点之间的线路,提高访问速度
- 避免字段使用大数据数组从而导致oplog覆写
数据回滚
由于复制延迟是不可避免的,这意味着主备节点之间的数据无法保持绝对的同步。当复制集中的主节点宕机时,备节点会重新选举成为新的主节点。
那么,当旧的主节点重新加入时,必须回滚掉之前的一些“脏日志数据”,以保证数据集与新的主节点一致。主备复制集合的差距越大,发生大量数据回滚的风险就越高,这个回滚的动作MongoDB会自动完成。
同步源选择
如果从节点与主节点之间延迟较大或网络不稳定,可以考虑 通过其他的备用节点进行同步数据。
默认情况下,mongoDB不一定会优先同步主节点数据,而会选择 最近的一个节点(ping 延迟最低) 进行同步数据。
如果我们想临时指定一个节点进行同步的话,可以通过以下命令进行指定:
1 | cfg = rs.config() |