redis的高可用性
前一篇文章提到,redis用复制来解决可靠性,一份数据做多份冗余。但是,当master节点因为某种原因下线,还必须有一种机制能让slave节点自动地转化成master节点,对外提供写服务,保证系统的高可用性。redis的sentinel就是这样的一种机制。
sentinel与raft
redis的sentinel是一个独立的进程,其代码的基础仍然是redis的IO多路复用,单线程多客户端的结构,但在启动时候通过指定启动项和配置文件,将创建一个独立的进程,与普通的redis进程进行解耦。一个master节点往往需要多个sentinel进程来监控,当master节点下线时,多个sentinel需要通过一个分布式协议来进行leader选举,选出的leader将负责从该master的slave节点内选出一个,成为新的master,让其他slave对其进行复制。由于当前比较流行且可靠的raft协议正是解决分布式系统中节点下线或者脑裂等问题,redis就用raft协议来进行leader选举。这个功能只用到raft协议的一小部分。
配置文件
sentinel是通过配置文件启动的,在配置文件里会保存sentinel所监控的master,slave和其他sentinel等信息,每当有其他节点的状态变化时,都会修改配置文件,让这些更改落盘,这样即便sentinel下线,再重启之后也可以恢复。
定时任务
redis的sentinel进程也是通过定时事件来处理主要的业务,包括向其他节点发送心跳,发送自身的信息以及接收其他sentinel,slave和master的心跳以及redis进程的主要信息等。在定时任务中还会发起leader选举以及进行slave向master的切换。这些任务的完成需要一个状态机,sentinel主要就是通过这个状态机来实现raft协议以及slave到master的升级。定时任务会把所有sentinel知道的master,slave,还有其他sentinel都遍历一遍,处理相关的状态变化。
sentinel的定时任务还会每次都修改一下下一次定时任务的触发时间,从而让多个sentinel的定时任务事件错开,避免在选leader时大家都差不多时间开始,导致票数被拆分。这与一般raft协议的做法有区别,raft协议是在每次发起投票时,每个节点先等一个随机的时间,但两者达到的效果都是一样的。
titl模式
sentinel是通过心跳来获取其他slave,master和sentinel的在线以及详细信息的,但是心跳除了因为网络分区,节点下线等问题丢失以外,自身机器的繁忙程度或者修改系统时间这些意外的操作也可能影响sentinel的心跳正常工作,因此redis为sentinel提供了一个特殊的模式,称为titl,这个模式下,sentinel 仍然会进行监控并收集信息,它只是不执行诸如故障转移、下线判断之类的操作而已。
|
|
每次定时任务redis都会执行上面这个函数,每次执行这个函数时都会产生一个sentinel.previous_time,表示这次执行sentinel逻辑的时间。如果上次执行sentinel的时间早于当前时间(说明系统时间发生改变)或者两次执行sentinel的时间相隔过长(代表当前系统繁忙,进程不能正常工作),sentinel就会进入titl模式。
发送心跳
sentinel会在配置文件里指定监控哪些master,但这些master可能会有其他sentinel同时在监控,这些master每一个都会跟着一个或多个slave,sentinel对这些slave和sentinel都是通过心跳来动态发现的,而不是在初始的配置文件里指定。当然,在发现以后,这些状态会保存在配置文件中,在下次启动时就会加载。
sentinel的心跳分为3种,一种是ping命令,用来检测进程是否在线;一种是info命令,用来获取进程的详细信息,master的slave就是通过这个命令的返回来动态发现的;一种是频道信息,用来获取其他sentinel的信息,master的sentinel就是通过这个频道信息来动态发现的。
sentinel中通过哈希表保存所有的master,每一个master都是sentinelRedisInstance结构体。对于每一个master,都会有一个slave表,一个sentinel表,都是哈希表,保存了sentinelRedisInstance结构体,用来保存这个master的所有slave和sentinel。
在info命令的返回中,如果发现了新的slave,则会创建新的sentinelRedisInstance;在频道信息返回中,如果发现了新的sentinel,则也会创建新的sentinelRedisInstance。这时候仅仅初始化一些状态信息,并不会真正创建网络连接。
创建网络连接及重连
sentinel在定时任务中创建对所有master,sentinel,slave的网路连接,以及对一些长期没有响应的连接进行重连。
|
|
对所有实例创建一个命令连接,对所有非sentinel实例创建频道连接。如果连接创建成功,把sentinelRedisInstance状态的SRI_DISCONNECTED标志位去掉。
故障转移
主观下线
当前sentinel认为某个实例下线,称为主观下线。redis只需要判断上次发送ping的时间与当前的时间是否超过一定的值,如果超过的时间过长,说明当前sentinel与该实例的通信有问题,因为sentinel只有当上次ping返回之后,才会发送下一个ping命令。这时候,sentinel会把该实例的状态设为主观下线SRI_S_DOWN,并按需要可能会断开当前的连接,在下一次定时任务的时候尝试重连。
客观下线
只有在判断某个实例客观下线(即大部分的节点都认为该实例下线),sentinel才会进行slave到master的升级的故障恢复操作。在隔一定时间,sentinel都会向sentinel针对某个master实例发送SENTINEL is-master-down-by-addr
命令,收集其他sentinel对于该master是否在线的意见,当大部分的sentinel都认为该master不在线时,触发一次故障转移。
SENTINEL_FAILOVER_STATE_WAIT_START
当大部分的sentinel判断某master客观下线,那么该master进入SENTINEL_FAILOVER_STATE_WAIT_START状态,处于该状态时sentinel会发起一次leader选举,并进入一个新的纪元,这个纪元的概念与raft协议的term是一致的。
leader选举的过程跟raft协议比较相似,且简单很多,因为不涉及log的一致性判断,不需要让选出来的leader比较log的term和index,基本是采取先到先得的原则,sentinel收到其他sentinel的投票请求,会采纳第一个sentinel的票,而返回给其他sentinel自己之前采纳过的sentinel。由于每个sentinel只投一个sentinel,且只有获得大部分的sentinel的票时才当选,因此必然能够保证有且仅有一个leader被选上。且之前提到过,每个sentinel的定时任务的执行频率都是随机化的,基本不会产生票数被拆分的问题。
所有的sentinel都可以发起leader选举,但只有一个sentinel来执行这个故障转移,当已经发起了一次选leader,但发现自己没选上,则会等一段时间(故障转移超时时间)后解除这一状态,重新判断客观下线,然后触发故障转移;但一般情况下,如果别的sentinel先发起选举,当前sentinel收到投票请求时,会更新一个叫做failover_start_time的变量,那么即便在判断客观下线后,也要判断当前时间是否与failover_start_time相隔2*故障转移超时时间,从而避免多个sentinel同时发起故障转移。raft协议里本身是没有这一逻辑的,那是因为当某个节点当选了master,会发心跳给slave,slave收到心跳就表明集群里已经存在leader,就不会再发起leader选举。而redis里没有这一leader到follower的心跳通信,所以需要用这个faileover_start_time来过滤一下。
SENTINEL_FAILOVER_STATE_SELECT_SLAVE
如果当前sentinel成为leader,则进入SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,进行故障转移。这一步是在该master的所有slave中选出一个作为新的master。
当选的几个条件:
- 没有下线
- 没有长时间没有收到该slave的info信息
- 没有与master长时间断开。
这几个条件限定了slave的数据比较新,且与sentinel的通信可靠。
然后,对候选的slave进行排序:
- 比较优先级,较小的优先级优先。
- 复制偏移量较大者优先
- 运行id较小者优先
SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
选出了slave,就进入SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE状态,给该slave发送slaveof no one
命令。
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
在info命令的返回里判断该slave的角色是否变成master,是则进入下一个状态,否则超时等到下一次定时任务重新触发故障转移。
SENTINEL_FAILOVER_STATE_RECONF_SLAVES
当选中的slave角色已变成master,就要向原master的其他slave发送slaveof命令,让它们复制新的master,这时候它们的状态均加上SRI_RECONF_SENT标志位,代表复制请求已发出。保证一定数量的slave并发发起向新master的同步,同样在info命令的返回里判断这些slave是否同步完毕。info命令里如果发现slave复制的master的ip地址和端口号与新master一致,那么slave进入SRI_RECONF_INPROG状态;当发现slave的slave_master_link_status == SENTINEL_MASTER_LINK_STATUS_UP
成立,那么说明slave已经完成对master的同步,此时slave进入SRI_RECONF_DONE状态。当所有slave都进入SRI_RECONF_DONE状态,则故障转移基本结束,sentinel进入SENTINEL_FAILOVER_STATE_UPDATE_CONFIG状态。
SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
该状态主要是修改配置文件,让sentinel的master,slave和sentinel都在故障转移后进行相应更新,例如把原master的ip,port等修改成新master等等,把状态都初始化,并重置所有网络连接。