文章目录
- 1.副本集介绍
- 2.Raft核心流程
- 3.副本集选举
- 4.副本集数据复制
- 5.副本集故障转移
1.副本集介绍
在生产环境中,不建议使用单机版的MongoDB服务器,因为有可能出现单点问题:
- 单机版的MongoDB无法保证可靠性,一旦进程发生故障或是服务器宕机,业务将直接不可用
- 一旦服务器上的磁盘损坏,数据会直接丢失,而此时并没有任何副本可用
为了保证MongoDB正常对外提供服务,需要搭建主备架构,确保在主节点发生故障时能够有从节点继续对外提供服务。
MongoDB在设计时已考虑到可用性问题,在其设计中原生支持了主备架构,也就是所谓的副本集架构。
通过docker compose可以简易地搭建副本集架构:
副本集的工作流程大致如下:
- 初始化,各节点会进行进行第一轮选举,决定主备节点
- 各节点都开始工作,主节点负责接收客户端写入的数据,备节点则负责同步这些数据
- 主节点发生故障,备节点通过心跳检测到了问题
- 某个备节点率先发起新一轮选举,成为新的主节点
- 客户端感知到主节点的变化,将后续的数据写入指向新的主节点
2.Raft核心流程
MongoDB副本集选举的作用在于在初始化副本集时以及主节点发生故障时能够选举出主节点,以保证有Master对外提供服务。
MongoDB通过Raft算法来实现集群选举,Raft是一种被广泛使用的选举算法,它将集群中的节点分为三类:
- Leader:领导者,Leader会向其他节点发送心跳,同时负责处理客户端的读写操作,包括将数据同步到其他节点
- Follower:追随者,响应来自Leader和Candidate的投票请求,如果在一定时间内没有收到Leader的心跳,则会转换为Candidate
- Candidate:候选者,Follower主动选举转换成为Candidate,获得大多数投票后会成为Leader
在开始时,所有节点都是Follower,此时大家都没有办法收到Leader的心跳。接下来,A节点出现等待超时,率先发起选举,并成为Candidate。A节点先是给自己投一票,然后接着向其他节点发送投票请求,一旦A节点获得了集群中大多数节点的投票,则会成为Leader,同时开始向其他节点广播心跳,以此来声明自己的Leader角色。
这里涉及到了一个等待超时时间,这个时间是150ms-300ms间的随机数,每个节点都随机分配等待超时时间,以降低同时升级为candidate并发起选举投票的可能性。
Raft协议还限定了以下细节:
- 在同一个任期内,每个节点最多只能给一个Candidate投票
- 节点在收到Candidate的投票请求时,只有当对方的任期、操作日志时间至少与自己的一样新时,才会给它投票
- Candidate发起投票后,如果一直没有得到大多数票,则会一直保持这个状态直到超时,此后将继续发起新一轮任期的选举(Term自增)
- 如果在投票期间检测到了Leader的心跳(其他Candidate率先完成选主),则会判断当前Leader的任期是否至少跟自己一样新,如果是则降级为Follower,并承认对方的Leader角色,否则不予理会
- 无论是Candidate还是Leader节点,一旦发现了其他节点有更新的任期(Term值),都会自动降级为Follower
这里的Term也就是任期值,每发生一次选举,参与者的Term值就会进行自增,以此来表明“谁处于最新的状态”。假设主节点A发生网络故障,此时B节点经过选举成为了主节点,当A恢复时发现B节点的Term值比自己高,说明A自身已经不是最新的状态了,要降级为Follower,以B节点为主节点。
如果在选举中,同时有两个及以上节点获取到了相同的投票数,那么这几个节点就会重新刷新选举超时时间,选举超时时间截止后继续重复上述选举流程,直到选出Leader。
Leader被选举出来后,就会向各个节点发送心跳包,同步自己的Leader身份,并且携带日志数据给Follower节点复制,以及刷新各个Follower节点的选举超时时间。
当Leader宕机时无法发送心跳包,那么Follower的选举超时时间流逝完,则立刻发起新一轮的选举。
需要注意的是,最好将集群中的副本集节点设置为奇数,这样可以避免投票数达不到“大多数”,假设集群中有N个节点,那么“大多数”=N/2+1:
3.副本集选举
MongoDB基于Raft的核心流程进行一些改造,将Leader节点称为Primary节点、Follower节点称为Secondary节点,并新增仲裁者节点和隐藏选举节点等角色:
- Primary:主节点,其接收所有的写请求,然后把修改同步到所有备节点。一个副本集只能有一个主节点,当主节点“挂掉”后,其他节点会重新选举出来一个主节点
- Secondary:备节点,与主节点保持同样的数据集。当主节点“挂掉”时,参与竞选主节点
- Arbiter:仲裁者节点,该节点只参与投票,不能被选为主节点,并且不从主节点中同步数据。当节点宕机导致复制集无法选出主节点时,可以给复制集添加一个仲裁者节点,这样即使有节点宕机,仍能选出主节点。仲裁者节点本身不存储数据,是非常轻量级的服务。当复制集成员为偶数时,最好加入一个仲裁者节点,以提升复制集的可用性。
- Priority0:优先级为0的节点,该节点永远不会被选举为主节点,也不会主动发起选举。通常,在跨机房方式下部署副本集可以使用该特性。假设使用了机房A和机房B,由于主要业务与机房A更近,则可以将机房B的复制集成员Priority设置为0,这样主节点就一定会是A机房的成员
- Hidden:隐藏节点,具备Priority0的特性,即不能被选为主节点(Priority为0),同时该节点对客户端不可见。由于隐藏节点不会接受业务访问,因此可通过隐藏节点做一些数据备份、离线计算的任务,这并不会影响整个副本集
- Delayed:延迟节点,必须同时具备隐藏节点和Priority0的特性,并且其数据落后于主节点一段时间,该时间是可配置的。由于延迟节点的数据比主节点落后一段时间,当错误或者无效的数据写入主节点时,可通过延迟节点的数据来恢复到之前的时间点
- Vote0:无投票权的节点,必须同时设定为Priority0节点。由于一个副本集中最多只有7个投票成员,因此多出来的成员则必须将其vote属性值设置为0,即这些成员将无法参与投票
副本集的架构模式常见的有PSS、PSA、PSH:
- PSS模式由一个主节点和两个备节点所组成,即Primary+Secondary+Secondary
- PSA模式由一个主节点、一个备节点和一个仲裁者节点组成,即Primary+Secondary+Arbiter
- PSH模式由一个主节点、一个备节点和一个隐藏节点组成,即Primary+Secondary+Hidden
4.副本集数据复制
在副本集架构中,主节点与备节点之间是通过oplog来同步数据的,这里的oplog是一个特殊的固定集合,当主节点上的一个写操作完成后,会向oplog集合写入一条对应的日志,而备节点则通过这个oplog不断拉取到新的日志,在本地进行回放以达到数据同步的目的,相当于MySQL中的binlog。
这里的主节点就是生产者,负责向自身的oplog队列中写入增量日志,也就是产生数据变更的记录。备节点则作为消费者一方,不断通过“pull”的方式拉取到这些增量日志进行消费。由于日志会不断增加,因此oplog被设计为固定大小的集合,它本身就是一个特殊的固定集合(capped collection),当oplog的容量达到上限时,旧的日志会被滚动删除。
在64位的Linux,Solaris,FreeBSD以及Windows系统上,MongoDB会分配磁盘剩余空间的5%作为oplog的大小,如果这部分小于1GB则分配1GB的空间。
oplog日志内容组成如下:
- ts:日志记录时间
- h:全局唯一标识
- v:版本信息
- op:数据操作类型,i:插入、u:更新、d:删除、c:命令、n:空命令
- ns:操作的集合是哪个
- o:操作内容
如这个oplog段所示,表示的就是以下命令:
db.app.T_AppInfo.deleteOne({"_id":"xxxx"})
oplog根据ts进行排序,保证了节点级有序,备节点便可以通过游标索引+轮询的方式进行拉取,每个备节点都保存一份自己的游标索引,记录同步到的位置:
每一条oplog记录都描述了一次数据的原子性变更,对于oplog来说,必须保证是幂等性的。对于同一个oplog,无论进行多少次回放操作,数据的最终状态都会保持不变。比如在一些原子性操作更新中使用$ inc来使字段自增,这个操作就不是幂等的,对文档字段多次执行$inc操作,每次都会产生新的结果。
这些非幂等的更新命令在oplog中通常会被转换为$ set操作,这样无论执行了多少次,文档的最终状态始终与第一次执行的效果一样。
5.副本集故障转移
一个PSS(一主两备)架构的副本集,主节点除了与两个备节点执行数据复制,三个节点之间还会通过心跳感知彼此的存活。一旦主节点发生故障以后,备节点将在某个周期内检测到主节点处于不可达的状态,此后将由其中一个备节点事先发起选举并最终成为新的主节点。
在副本集组建完成之后,各成员节点会开启定时器,持续向其他成员发起心跳,这里涉及的参数为heartbeatIntervalMillis(心跳间隔时间),默认值是2s。如果心跳成功,则会持续以2s的频率继续发送心跳。
一次心跳检测失败并不会立即触发重新选举,成员节点还会启动一个选举超时检测定时器,该定时器默认以10s的间隔执行(electionTimeoutMillis参数):
- 如果心跳响应成功,则取消上一次的electionTimeout调度(保证不会发起选举),并发起新一轮electionTimeout调度
- 如果心跳响应迟迟不能成功,那么electionTimeout任务被触发选举,进而导致备节点发起选举并成为新的主节点