【redis】redis主从哨兵、主从复制和主从切换
Redis 基于状态机的设计,实现主从复制
主从复制技术我们应该都比较熟悉,因为在使用 Redis 或 MySQL 数据库时,我们经常会使用主从复制来实现主从节点间的数据同步,以此提升服务的高可用性。
从原理上来说,Redis 的主从复制主要包括了全量复制、增量复制和长连接同步三种情况。
全量复制传输 RDB 文件,增量复制传输主从断连期间的命令,而长连接同步则是把主节点正常收到的请求传输给从节点。
这三种情况看似简单,但是在实现的时候,我们通常都需要考虑主从连接建立、主从握手和验证、复制情况判断和数据传输等多种不同状态下的逻辑处理。
那么,如何才能高效地实现主从复制呢?
实际上,Redis 是采用了基于状态机的设计思想,来清晰地实现不同状态及状态间的跳转。
我们实现网络功能的时候,这种设计和实现方法其实非常重要,它可以避免我们在处理不同状态时的逻辑冲突或遗漏。
主从复制的四大阶段首先,我们可以根据主从复制时的关键事件,把整个复制过程分成四个阶段,分别是初始化、建立连接、主从握手、复制类型判断与执行。
下面,我们就来依次了解下每个阶段的主要工作。
1. 初始化阶段当我们把一个 Redis 实例 A 设置为另一个实例 B 的从库时,实例 A 会完成初始化操作,主要是获得了主库的 IP 和端口号。而这个初始化过程,我们可以用三种方式来设置。
方式一:在实例 A 上执行 replicaof masterip masterport 的主从复制命令,指明实例 B 的 IP(masterip)和端口号(masterport)。
方式二:在实例 A 的配置文件中设置 replicaof masterip masterport,实例 A 可以通过解析文件获得主库 IP 和端口号。
方式三:在实例 A 启动时,设置启动参数–replicaof [masterip] [masterport]。实例 A 解析启动参数,就能获得主库的 IP 和端口号。
2. 建立连接阶段接下来,一旦实例 A 获得了主库 IP 和端口号,该实例就会尝试和主库建立 TCP 网络连接,并且会在建立好的网络连接上,监听是否有主库发送的命令。
3. 主从握手阶段当实例 A 和主库建立好连接之后,实例 A 就开始和主库进行握手。简单来说,握手过程就是主从库间相互发送 PING-PONG 消息,同时从库根据配置信息向主库进行验证。最后,从库把自己的 IP、端口号,以及对无盘复制和 PSYNC 2 协议的支持情况发给主库。那么,和前两个阶段相比,主从握手阶段要执行的操作会比较多,涉及的状态也比较多,所以我们需要先掌握这个阶段要完成的操作,一会儿我就来给你具体介绍。
4. 复制类型判断与执行阶段这样,等到主从库之间的握手完成后,从库就会给主库发送 PSYNC 命令。紧接着,主库会根据从库发送的命令参数作出相应的三种回复,分别是执行全量复制、执行增量复制、发生错误。最后,从库在收到上述回复后,就会根据回复的复制类型,开始执行具体的复制操作。
基于状态机的主从复制实现首先你要知道,基于状态机实现主从复制的好处,就是当你在开发程序时,只需要考虑清楚在不同状态下具体要执行的操作,以及状态之间的跳转条件就行了。
所以,Redis 源码中采用的基于状态机跳转的设计思路和主从复制的实现,就是很值得你学习的一点。
主从复制是 Redis、MySQL 等数据库或存储系统,用来实现高可用性的方法。
要实现主从复制,则需要应对整个过程中 Redis 在不同状态下的各种处理逻辑,因此,如何正确实现主从复制,并且不遗漏可能的状态,是我们在实际开发中需要面对的问题。
状态机驱动的设计方法是一种通用的设计方法,在涉及网络通信的场景中应用广泛。
Redis 对主从复制的实现为我们提供了良好的参考示例,当你需要自行设计和实现网络功能时,就可以把状态机驱动的方法使用起来。
我们介绍的状态机是当实例为从库时会使用的。那么,当一个实例是主库时,为什么不需要使用一个状态机来实现主库在主从复制时的流程流转呢?
因为复制数据的发起方是从库,从库要求复制数据会经历多个阶段(发起连接、握手认证、请求数据),而主库只需要「被动」接收从库的请求,根据需要「响应数据」即可完成整个流程,所以主库不需要状态机流转。
从哨兵Leader选举学习Raft协议实现
我们了解了哨兵实例的初始化过程。哨兵实例一旦运行后,会周期性地检查它所监测的主节点的运行状态。
当发现主节点出现客观下线时,哨兵实例就要开始执行故障切换流程了。
不过,我们在部署哨兵实例时,通常会部署多个哨兵来进行共同决策,这样就避免了单个哨兵对主节点状态的误判。
但是这同时也给我们带来了一个问题,即当有多个哨兵判断出主节点故障后,究竟由谁来执行故障切换?
实际上,这就和哨兵 Leader 选举有关了。而哨兵 Leader 选举,又涉及到分布式系统中经典的共识协议:Raft 协议。
学习和掌握 Raft 协议的实现,对于我们在分布式系统开发中实现分布式共识有着非常重要的指导作用。
哨兵 Leader 选举和 Raft 协议当哨兵发现主节点有故障时,它们就会选举一个 Leader 出来,由这个 Leader 负责执行具体的故障切换流程。
但因为哨兵本身会有多个实例,所以,在选举 Leader 的过程中,就需要按照一定的协议,让多个哨兵就“Leader 是哪个实例”达成一致的意见,这也就是分布式共识。
Raft 协议可以用来实现分布式共识,这是一种在分布式系统中实现多节点达成一致性的算法,可以用来在多个节点中选举出 Leader 节点。
为了实现这一目标,Raft 协议把节点设计成了三种类型,分别是 Leader、Follower 和 Candidate。
Raft 协议对于 Leader 节点和 Follower 节点之间的交互有两种规定:
正常情况下,在一个稳定的系统中,只有 Leader 和 Follower 两种节点,并且 Leader 会向 Follower 发送心跳消息。
异常情况下,如果 Follower 节点在一段时间内没有收到来自 Leader 节点的心跳消息,那么,这个 Follower 节点就会转变为 Candidate 节点,并且开始竞选 Leader。
然后,当一个 Candidate 节点开始竞选 Leader 时,它会执行如下操作:
给自己投一票;向其他节点发送投票请求,并等待其他节点的回复;
启动一个计时器,用来判断竞选过程是否超时。
在这个 Candidate 节点等待其他节点返回投票结果的过程中,如果它收到了 Leader 节点的心跳消息,这就表明,此时已经有 Leader 节点被选举出来了。
那么,这个 Candidate 节点就会转换为 Follower 节点,而它自己发起的这轮竞选 Leader 投票过程就结束了。
而如果这个 Candidate 节点,收到了超过半数的其他 Follower 节点返回的投票确认消息,也就是说,有超过半数的 Follower 节点都同意这个 Candidate 节点作为 Leader 节点,那么这个 Candidate 节点就会转换为 Leader 节点,从而可以执行 Leader 节点需要运行的流程逻辑。
这里,你需要注意的是,每个 Candidate 节点发起投票时,都会记录当前的投票轮次,Follower 节点在投票过程中,每一轮次只能把票投给一个 Candidate 节点。而一旦 Follower 节点投过票了,它就不能再投票了。
如果在一轮投票中,没能选出 Leader 节点,比如有多个 Candidate 节点获得了相同票数,那么 Raft 协议会让 Candidate 节点进入下一轮,再次开始投票。
好了,现在你就了解了 Raft 协议中 Leader 选举的基本过程和原则。
不过你还要清楚一点,就是 Redis 哨兵在实现时,并没有完全按照 Raft 协议来实现,这主要体现在,Redis 哨兵实例在正常运行的过程中,不同实例间并不是 Leader 和 Follower 的关系,而是对等的关系。
只有当哨兵发现主节点有故障了,此时哨兵才会按照 Raft 协议执行选举 Leader 的流程。
1、Redis 为了实现故障自动切换,引入了一个外部「观察者」检测实例的状态,这个观察者就是「哨兵」
2、但一个哨兵检测实例,有可能因为网络原因导致「误判」,所以需要「多个」哨兵共同判定
3、多个哨兵共同判定出实例故障后(主观下线、客观下线),会进入故障切换流程,切换时需要「选举」出一个哨兵「领导者」进行操作
4、这个选举的过程,就是「分布式共识」,即多个哨兵通过「投票」选举出一个都认可的实例当领导者,由这个领导者发起切换,这个选举使用的算法是 Raft 算法
5、严格来说,Raft 算法的核心流程是这样的:
1) 集群正常情况下,Leader 会持续给 Follower 发心跳消息,维护 Leader 地位
2) 如果 Follower 一段时间内收不到 Leader 心跳消息,则变为 Candidate 发起选举
3) Candidate 先给自己投一票,然后向其它节点发送投票请求
4) Candidate 收到超过半数确认票,则提升为新的 Leader,新 Leader 给其它 Follower 发心跳消息,维护新的 Leader 地位
5) Candidate 投票期间,收到了 Leader 心跳消息,则自动变为 Follower
6) 投票结束后,没有超过半数确认票的实例,选举失败,会再次发起选举
6、但哨兵的选举没有按照严格按照 Raft 实现,因为多个哨兵之间是「对等」关系,没有 Leader 和 Follower 角色,只有当 Redis 实例发生故障时,哨兵才选举领导者进行切换,选举 Leader 的过程是按照 Raft 算法步骤 3-6 实现的
1、一个哨兵检测判定主库故障,这个过程是「主观下线」,另外这个哨兵还会向其它哨兵询问(发送 sentinel is-master-down-by-addr 命令),多个哨兵都检测主库故障,数量达到配置的 quorum 值,则判定为「客观下线」
2、首先判定为客观下线的哨兵,会发起选举,让其它哨兵给自己投票成为「领导者」,成为领导者的条件是,拿到超过「半数」的确认票 + 超过预设的 quorum 阈值的赞成票
3、投票过程中会比较哨兵和主库的「纪元」(主库纪元 < 发起投票哨兵的纪元 + 发起投票哨兵的纪元 > 其它哨兵的纪元),保证一轮投票中一个哨兵只能投一次票
无论对主库还是从库,哨兵都判断了「主观下线」,但只有对主库才判断「客观下线」和「故障切换」。
如1主1从3哨兵, 从库挂了 ,不会触发 3哨兵的leader哨兵选举流程,不需要做主从切换。
Pub/Sub在主从故障切换时是如何发挥作用的?
一个哨兵是如何获得其他哨兵的信息的呢?
这其实就和哨兵在运行过程中,使用的发布订阅(Pub/Sub)通信方法有关了。
Pub/Sub 通信方法可以让哨兵订阅一个或多个频道,当频道中有消息时,哨兵可以收到相应消息;
同时,哨兵也可以向频道中发布自己生成的消息,以便订阅该频道的其他客户端能收到消息。
发布订阅通信方法的实现
发布订阅通信方法的基本模型是包含发布者、频道和订阅者,发布者把消息发布到频道上,而订阅者会订阅频道,一旦频道上有消息,频道就会把消息发送给订阅者。
一个频道可以有多个订阅者,而对于一个订阅者来说,它也可以订阅多个频道,从而获得多个发布者发布的消息。
下图展示的就是发布者 - 频道 - 订阅者的基本模型,你可以看下。
1、哨兵是通过 master 的 PubSub 发现其它哨兵的:每个哨兵向 master 的 PubSub(__sentinel__:hello 频道)发布消息,同时也会订阅这个频道,这样每个哨兵就能拿到其它哨兵的 IP、端口等信息
2、每个哨兵有了其它哨兵的信息后,在判定 Redis 实例状态时,就可以互相通信、交换信息,共同判定实例是否真的故障
3、哨兵判定 Redis 实例故障、发起切换时,都会向 master 的 PubSub 的频道发布消息
4、客户端可以订阅 master 的 PubSub,感知到哨兵工作到了哪个状态节点,从而作出自己的反应
5、PubSub 的实现,其实就是 Redis 在内存中维护了一个「发布-订阅」映射表,订阅者执行 SUBSCRIBE 命令,Redis 会把订阅者加入到指定频道的「链表」下。发布者执行 PUBLISH,Redis 就找到这个映射表中这个频道的所有「订阅者」,把消息「实时转发」给这些订阅者
参考
Redis 核心技术与实战
https://time.geekbang.org/column/intro/100056701
Redis 源码剖析与实战
https://time.geekbang.org/column/intro/100084301