实现一个Redis Sentinel客户端的基本步骤如下:
从上面的模型可以看出,Redis Sentinel客户端只有在初始化和切换主节点时需要和Sentinel节点集合进行交互来获取主节点信息,所以在设计客户端时需要将Sentinel节点集合考虑成配置(相关节点信息和变化)发现服务。
上述过程只是从客户端设计的角度进行分析,在开发客户端时要考虑的细节还有很多,但是这些问题并不需要深究,
下面将介绍如何使用Java的Redis客户端操作Redis Sentinel,并结合本节的内容分析一下相关源码。
本节将介绍Redis Sentinel的基本实现原理,具体包含以下几个方面:
Redis Sentinel的三个定时任务、主观下线和客观下线、Sentinel领导者选举、故障转移,相信通过本节的学习读者能对Redis Sentinel的高可用特性有更加深入的理解和认识。
一套合理的监控机制是Sentinel节点判定节点不可达的重要保证,RedisSentinel通过三个定时监控任务完成对各个节点发现和监控:
例如下面就是在一个主节点上执行info replication的结果片段:
# Replication role:master connected_slaves:2 slave0:ip=127.0.0.1,port=6380,state=online,offset=4917,lag=1 slave1:ip=127.0.0.1,port=6381,state=online,offset=4917,lag=1
Sentinel节点通过对上述结果进行解析就可以找到相应的从节点。这个定时任务的作用具体可以表现在三个方面:
每隔2秒,每个Sentinel节点会向Redis数据节点的__sentinel__:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息(如图9-27所示),同时每个Sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:
Sentinel节点publish的消息格式如下:
<Sentinel节点IP> <Sentinel节点端口> <Sentinel节点runId> <Sentinel节点配置版本> <主节点名字> <主节点Ip> <主节点端口> <主节点配置版本>
上一小节介绍的第三个定时任务,每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。
从字面意思也可以很容易看出主观下线是当前Sentinel节点的一家之言,存在误判的可能,如图9-29所示。
当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel ismaster-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过<quorum>个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分
Sentinel节点都对主节点的下线做了同意的判定,那么这个判定就是客观的,如图9-30所示。
注意
从节点、Sentinel节点在主观下线后,没有后续的故障转移操作。
这里有必要对sentinel is-master-down-by-addr命令做一个介绍,它的使用方法如下:
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
当runid等于“*”时,作用是Sentinel节点直接交换对主节点下线的判定。
当runid等于当前Sentinel节点的runid时,作用是当前Sentinel节点希望目标Sentinel节点同意自己成为领导者的请求,有关Sentinel领导者选举,后面会进行介绍。
例如sentinel-1节点对主节点做主观下线后,会向其余Sentinel节点(假设sentinel-2和sentinel-3节点)发送该命令:
sentinel is-master-down-by-addr 127.0.0.1 6379 0 *
返回结果包含三个参数,如下所示:
假如Sentinel节点对于主节点已经做了客观下线,那么是不是就可以立即进行故障转移了?
当然不是,实际上故障转移的工作只需要一个Sentinel节点来完成即可,所以Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。
Redis使用了Raft算法实现领导者选举,因为Raft算法相对比较抽象和复杂,以及篇幅所限,所以这里给出一个Redis Sentinel进行领导者选举的大致思路:
s1(sentinel-1)最先完成了客观下线,它会向s2(sentinel-2)和s3(sentinel-3)发送sentinel is-master-down-by-addr命令,s2和s3同意选其为领导者。
s1此时已经拿到2张投票,满足了大于等于max(quorum,num(sentinels)/2+1)=2的条件,所以此时s1成为领导者。
由于每个Sentinel节点只有一票,所以当s2向s1和s3索要投票时,只能获取一票,而s3由于最后完成主观下线,当s3向s1和s2索要投票时一票都得不到,整个过程如图9-32和9-33所示。
实际上Redis Sentinel实现会更简单一些,因为一旦有一个Sentinel节点获得了max(quorum,num(sentinels)/2+1)的票数,其他Sentinel节点再去确认已经没有意义了,因为每个Sentinel节点只有一票,如果读者有兴趣的话,可以修改sentinel.c源码,在Sentinel的执行命令列表中添加monitor命令:
struct redisCommand sentinelcmds[] = { {"monitor",monitorCommand,1,"",0,NULL,0,0,0,0,0}, {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0}, {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0}, ... }
重新编译部署Redis Sentinel测试环境,在3个Sentinel节点上执行monitor命令:
// 因为最后参数是"*",所以此时是Sentinel节点之间交换对主节点的失败判定 [0 127.0.0.1:38440] "SENTINEL" "is-master-down-by-addr" "127.0.0.1" "6379" "0" "*" // 因为最后参数是具体的runid,所以此时代表runid="2f4430bb62c039fb125c5771d7cde2571a7 a5ab4"的节点希望目标Sentinel节点同意自己成为领导者。 [0 127.0.0.1:38440] "SENTINEL" "is-master-down-by-addr" "127.0.0.1" "6379" "1" "2f4430bb62c039fb125c5771d7cde2571a7a5ab4"
注意
有关Raft算法可以参考其GitHub主页https://raft.github.io/。
领导者选举出的Sentinel节点负责故障转移,具体步骤如下:
a. 过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节点ping响应、与主节点失联超过down-after-milliseconds*10秒。 b. 选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。 c. 选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。 d. 选择runid最小的从节点。
Sentinel领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点。
Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。
Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。
本次故障转移的分析直接使用9.2节的拓扑和配置进行说明,为了方便分析故障转移的过程,表9-4列出了每个节点的角色、ip、端口、进程号、runId。
因为故障转移涉及节点关系的变化,所以下面说明中用端口号代表节点。
模拟故障的方法有很多,比较典型的方法有以下几种:
本次我们使用方法一进行测试,因为从实际经验来看,数百上千台机器偶尔宕机一两台是会不定期出现的,为了方便分析日志行为,这里记录一下操作的时间和命令。
用kill-9使主节点的进程宕机,操作时间2016-07-2409:40:35:
$ kill -9 19661
6380节点晋升为主节点,6381节点成为6380节点的从节点。
相信故障转移的效果和预想的一样,这里重点分析相应节点的日志。
(1)6379节点日志
两个复制请求,分别来自端口为6380和6381的从节点:
19661:M 24 Jul 09:22:16.907 * Slave 127.0.0.1:6380 asks for synchronization 19661:M 24 Jul 09:22:16.907 * Full resync requested by slave 127.0.0.1:6380 ... 19661:M 24 Jul 09:22:16.919 * Synchronization with slave 127.0.0.1:6380 succeeded 19661:M 24 Jul 09:22:23.396 * Slave 127.0.0.1:6381 asks for synchronization 19661:M 24 Jul 09:22:23.396 * Full resync requested by slave 127.0.0.1:6381 ... 19661:M 24 Jul 09:22:23.432 * Synchronization with slave 127.0.0.1:6381 succeeded
09:40:35做了kill-9操作,由于模拟的是宕机效果,所以6379节点没 有看到任何日志(这点和shutdown操作不太相同)。
(2)6380节点日志
6380节点在09:40:35之后发现它与6379节点已经失联:
19667:S 24 Jul 09:40:35.788 # Connection with master lost. 19667:S 24 Jul 09:40:35.788 * Caching the disconnected master state. 19667:S 24 Jul 09:40:35.974 * Connecting to MASTER 127.0.0.1:6379 19667:S 24 Jul 09:40:35.974 * MASTER <-> SLAVE sync started 19667:S 24 Jul 09:40:35.975 # Error condition on socket for SYNC: Connection refused ...
09:41:06时它接到Sentinel节点的命令:清理原来缓存的主节点状态,Sentinel节点将6380节点晋升为主节点,并重写配置:
19667:M 24 Jul 09:41:06.161 * Discarding previously cached master state. 19667:M 24 Jul 09:41:06.161 * MASTER MODE enabled (user request from 'id=7 addr=127.0.0.1:46759 fd=10 name=sentinel-7044753f-cmd age=1111 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=rw cmd=exec') 19667:M 24 Jul 09:41:06.161 # CONFIG REWRITE executed with success. 6381节点发来了复制请求: 19667:M 24 Jul 09:41:07.499 * Slave 127.0.0.1:6381 asks for synchronization 19667:M 24 Jul 09:41:07.499 * Full resync requested by slave 127.0.0.1:6381 ... 19667:M 24 Jul 09:41:07.548 * Background saving terminated with success 19667:M 24 Jul 09:41:07.548 * Synchronization with slave 127.0.0.1:6381 succeeded
(3)6381节点日志
6381节点同样与6379节点失联:
19685:S 24 Jul 09:40:35.788 # Connection with master lost. 19685:S 24 Jul 09:40:35.788 * Caching the disconnected master state. 19685:S 24 Jul 09:40:36.425 * Connecting to MASTER 127.0.0.1:6379 19685:S 24 Jul 09:40:36.425 * MASTER <-> SLAVE sync started 19685:S 24 Jul 09:40:36.425 # Error condition on socket for SYNC: Connection refused ...
后续操作如下:
19685:S 24 Jul 09:41:06.497 # Error condition on socket for SYNC: Connection refused 19685:S 24 Jul 09:41:07.008 * Discarding previously cached master state. 19685:S 24 Jul 09:41:07.008 * SLAVE OF 127.0.0.1:6380 enabled (user request from 'id=7 addr=127.0.0.1:55872 fd=10 name=sentinel-7044753f-cmd age=1111 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=133 qbuf-free=32635 obl=36 oll=0 omem=0 events=rw cmd=exec') 19685:S 24 Jul 09:41:07.008 # CONFIG REWRITE executed with success.
19685:S 24 Jul 09:41:07.498 * Connecting to MASTER 127.0.0.1:6380 ... 19685:S 24 Jul 09:41:07.549 * MASTER <-> SLAVE sync: Finished with success
(4)sentinel-1节点日志
09:41:05对6379节点作了主观下线(+sdown),注意这个时间正好是kill-9后的30秒,和down-after-milliseconds的配置是一致的。
Sentinel节点更新自己的配置纪元(new-epoch):
19697:X 24 Jul 09:41:05.850 # +sdown master mymaster 127.0.0.1 6379 19697:X 24 Jul 09:41:05.928 # +new-epoch 1
后续操作如下:
19697:X 24 Jul 09:41:05.929 # +vote-for-leader 7044753f564e42b1578341acf4c49dca 3681151c 1 19697:X 24 Jul 09:41:06.913 # +odown master mymaster 127.0.0.1 6379 #quorum 3/2
19697:X 24 Jul 09:41:06.913 # Next failover delay: I will not start a failover before Sun Jul 24 09:47:06 2016 19697:X 24 Jul 09:41:07.008 # +config-update-from sentinel 127.0.0.1:26381 127.0.0.1 26381 @ mymaster 127.0.0.1 6379 19697:X 24 Jul 09:41:07.008 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380 19697:X 24 Jul 09:41:07.008 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380 19697:X 24 Jul 09:41:07.008 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 19697:X 24 Jul 09:41:37.060 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
(5)sentinel-2节点日志
整个过程和sentinel-1节点是一样的,这里就不占用篇幅分析了。
(6)sentinel-3节点日志
从sentinel-1节点和sentinel-2节点的日志来看,sentinel-3节点是领导者,所以分析sentinel-3节点的日志至关重要。后续操作如下。
19713:X 24 Jul 09:41:05.854 # +sdown master mymaster 127.0.0.1 6379 19713:X 24 Jul 09:41:05.909 # +odown master mymaster 127.0.0.1 6379 #quorum 2/2 19713:X 24 Jul 09:41:05.909 # +new-epoch 1
19713:X 24 Jul 09:41:05.909 # +try-failover master mymaster 127.0.0.1 6379 19713:X 24 Jul 09:41:05.911 # +vote-for-leader 7044753f564e42b1578341acf4c49dca 3681151c 1 19713:X 24 Jul 09:41:05.929 # 127.0.0.1:26379 voted for 7044753f564e42b1578341a cf4c49dca3681151c 1 19713:X 24 Jul 09:41:05.930 # 127.0.0.1:26380 voted for 7044753f564e42b1578341a 547 cf4c49dca3681151c 1 19713:X 24 Jul 09:41:06.001 # +elected-leader master mymaster 127.0.0.1 6379
表9-5展示了3个Sentinel节点完成客观下线的时间点,从时间点可以看到sentinel-3节点最先完成客观下线。
3) 故障转移。每一步都可以通过发布订阅来获取,对于每个字段的说明可以参考表9-6。寻找合适的从节点作为新的主节点:
19713:X 24 Jul 09:41:06.001 # +failover-state-select-slave master mymaster 127.0.0.1 6379
选出了合适的从节点(6380节点):
19713:X 24 Jul 09:41:06.077 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
命令6380节点执行slaveof no one,使其成为主节点:
19713:X 24 Jul 09:41:06.077 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
等待6380节点晋升为主节点:
19713:X 24 Jul 09:41:06.161 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
确认6380节点已经晋升为主节点:
19713:X 24 Jul 09:41:06.927 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379
故障转移进入重新配置从节点阶段:
19713:X 24 Jul 09:41:06.927 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6379
命令6381节点复制新的主节点:
19713:X 24 Jul 09:41:07.008 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
6381节点正在重新配置成为6380节点的从节点,但是同步过程尚未完成:
19713:X 24 Jul 09:41:07.955 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
6381节点完成对6380节点的同步:
19713:X 24 Jul 09:41:07.955 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6379
故障转移顺利完成:
19713:X 24 Jul 09:41:08.045 # +failover-end master mymaster 127.0.0.1 6379
故障转移成功后,发布主节点的切换消息:
19713:X 24 Jul 09:41:08.045 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380
表9-6记录了Redis Sentinel在故障转移一些重要的事件消息对应的频道。
<instance details>格式如下: <instance-type> <name> <ip> <port> @ <master-name> <master-ip> <master-port>
重新启动原来的6379节点:
redis-server redis-6379.conf操作时间: 2016-07-24 09:46:21
(1)6379节点
启动后接到Sentinel节点的命令,让它去复制6380节点:
22223:M 24 Jul 09:46:21.260 * The server is now ready to accept connections on port 6379 22223:S 24 Jul 09:46:31.323 * SLAVE OF 127.0.0.1:6380 enabled (user request from 'id=2 addr=127.0.0.1:51187 fd=6 name=sentinel-94dde2f5-cmd age=10 idle=0 flags=x db=0 sub=0 psub=0 multi=3 qbuf=0 qbuf-free=32768 obl=36 oll=0 omem=0 events=rw cmd=exec') 22223:S 24 Jul 09:46:31.323 # CONFIG REWRITE executed with success. ...
(2)6380节点
接到6379节点的复制请求,做复制的相应处理:
19667:M 24 Jul 09:46:32.284 * Slave 127.0.0.1:6379 asks for synchronization 19667:M 24 Jul 09:46:32.284 * Full resync requested by slave 127.0.0.1:6379 ... 19667:M 24 Jul 09:46:32.353 * Synchronization with slave 127.0.0.1:6379 succeeded
(3)sentinel-1节点日志
撤销对6379节点主观下线的决定:
19707:X 24 Jul 09:46:21.406 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
(4)sentinel-2节点日志
撤销对6379节点主观下线的决定:
19713:X 24 Jul 09:46:21.408 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
(5)sentinel-3节点日志
撤销对6379节点主观下线的决定,更新Sentinel节点配置:
19697:X 24 Jul 09:46:21.367 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380 19697:X 24 Jul 09:46:31.322 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
部署各个节点的机器时间尽量要同步,否则日志的时序性会混乱,例如可以给机器添加NTP服务来同步时间,具体可以参考第12章Linux配置章节。
在介绍如何进行节点下线之前,首先需要弄清两个概念:临时下线和永久下线。
所以运维人员需要弄清楚本次下线操作是临时下线还是永久下线。
通常来看,无论是主节点、从节点还是Sentinel节点,下线原因无外乎以下几种:
(1)主节点
如果需要对主节点进行下线,比较合理的做法是选出一个“合适”(例如性能更高的机器)的从节点,使用sentinel failover功能将从节点晋升主节点,sentinel failover已经在9.3节介绍过了,只需要在任意可用的Sentinel节点
执行如下操作即可。
sentinel failover <master name>
如图9-35所示,在任意一个Sentinel节点上(例如26379端口节点)执行sentinel failover即可。
运维提示
Redis Sentinel存在多个从节点时,如果想将指定从节点晋升为主节点,
可以将其他从节点的slavepriority配置为0,但是需要注意failover后,将 slave-priority调回原值。
(2)从节点和Sentinel节点
如果需要对从节点或者Sentinel节点进行下线,只需要确定好是临时还是永久下线后执行相应操作即可。
如果使用了读写分离,下线从节点需要保证应用方可以感知从节点的下线变化,从而把读取请求路由到其他节点。
需要注意的是,Sentinel节点依然会对这些下线节点进行定期监控,这是由Redis Sentinel的设计思路所决定的。
下面日志显示(需要设置loglevel=debug),6380节点下线后,Sentinel节点还是会定期对其监控,会 造成一定的网络资源浪费。
-cmd-link slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379 #Connection refused -pubsub-link slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6379 #Connection refused ...
(1)添加从节点
添加从节点的场景大致有如下几种:
添加方法:添加slaveof{masterIp}{masterPort}的配置,使用redis-server启动即可,它将被Sentinel节点自动发现。
(2)添加Sentinel节点
添加Sentinel节点的场景可以分为以下几种:
添加方法:添加sentinel monitor主节点的配置,使用redis-sentinel启动即可,它将被其余Sentinel节点自动发现。
(3)添加主节点
因为Redis Sentinel中只能有一个主节点,所以不需要添加主节点,如果需要替换主节点,可以使用Sentinel failover手动故障转移。
有关Redis数据节点和Sentinel节点配置修改以及优化的方法,前面的章节已经介绍过了,这里给出Sentinel节点配置时要注意的地方:
运维提示
Sentinel节点只支持如下命令:ping、sentinel、subscribe、unsubscribe、 psubscribe、punsubscribe、publish、info、role、client、shutdown。 具体可以参考源码中sentinel.c。
上面介绍了Redis Sentinel节点运维的场景和方法,但在实际运维中,故障的发生通常比较突然并且瞬息万变,影响的范围也很难预估,
所以建议运维人员将上述场景提前做好预案,当事故发生时,可以用脚本或者可视化工具快速处理故障。
从节点一般可以起到两个作用:
但上述模型中,从节点不是高可用的,如果slave-1节点出现故障,首先客户端client-1将与其失联,其次Sentinel节点只会对该节点做主观下线,因为Redis Sentinel的故障转移是针对主节点的。
所以很多时候,Redis Sentinel中的从节点仅仅是作为主节点一个热备,不让它参与客户端的读操作,就是为了保证整体高可用性,但实际上这种使用方法还是有一些浪费,尤其是在有很多从节点或者确实需要读写分离的场景,所以如何实现从节点的高可用是非常有必要的。
Redis Sentinel在对各个节点的监控中,如果有对应事件的发生,都会发出相应的事件消息(见表9-6),其中和从节点变动的事件有以下几个:
所以在设计Redis Sentinel的从节点高可用时,只要能够实时掌握所有从节点的状态,把所有从节点看做一个资源池(如图9-37所示),无论是上线还是下线从节点,客户端都能及时感知到(将其从资源池中添加或者删除),这样从节点的高可用目标就达到了。
本文作者:Eric
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!