Redis高可用之哨兵

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的通信命令

Sentinel 节点连接一个 Redis 实例的时候,会创建 cmdpub/sub 两个 连接Sentinel 通过 cmd 连接给 Redis 发送命令,通过 pub/sub 连接到 Redis 实例上的其他 Sentinel 实例。

SentinelRedis 主节点从节点 交互的命令,主要包括:

命令 作 用
PING SentinelRedis 节点发送 PING 命令,检查节点的状态
INFO SentinelRedis 节点发送 INFO 命令,获取它的 从节点信息
PUBLISH Sentinel 向其监控的 Redis 节点 __sentinel__:hello 这个 channel 发布 自己的信息主节点 相关的配置
SUBSCRIBE Sentinel 通过订阅 Redis 主节点从节点__sentinel__:hello 这个 channnel,获取正在监控相同服务的其他 Sentinel 节点

SentinelSentinel 交互的命令,主要包括:

命令 作 用
PING Sentinel 向其他 Sentinel 节点发送 PING 命令,检查节点的状态
SENTINEL:is-master-down-by-addr 和其他 Sentinel 协商 主节点 的状态,如果 主节点 处于 SDOWN 状态,则投票自动选出新的 主节点

哨兵工作方式

  1. 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令

  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。

  3. 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

  4. 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线

  5. 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令

  6. 当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次

  7. 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。

  8. 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

主观下线和客观下线

默认情况下,每个 Sentinel 节点会以 每秒一次 的频率对 Redis 节点和 其它Sentinel 节点发送 PING 命令,并通过节点的 回复 来判断节点是否在线。

主观下线

主观下线 适用于所有 主节点从节点。如果在 down-after-milliseconds 毫秒内,Sentinel 没有收到 目标节点 的有效回复,则会判定 该节点主观下线

客观下线

客观下线 只适用于 主节点。如果 主节点 出现故障,Sentinel 节点会通过 sentinel is-master-down-by-addr 命令,向其它 Sentinel 节点询问对该节点的 状态判断。如果超过 <quorum> 个数的节点判定 主节点 不可达,则该 Sentinel 节点会判断 主节点客观下线

哨兵部署

注意

  1. 一个稳健的哨兵集群,至少需要3个实例,并放到不同机器上
  2. 哨兵保证集群的高可用,不保证数据的完整性
  3. 常见的客户端应用库都支持哨兵
  4. 哨兵需要不断测试和验证才能应用到生产环境

环境

服务器名称 IP地址
r1 192.168.10.1
r2 192.168.10.2
r3 192.168.10.3

安装redis

1
yum install redis -y

配置Redis

r1:/etc/redis.conf

1
2
3
bind 0.0.0.0
requirepass Aa123456
masterauth Aa123456

r2:/etc/redis.conf

1
2
3
4
bind 0.0.0.0
requirepass Aa123456
slaveof 192.168.10.1 6379
masterauth Aa123456

r3:/etc/redis.conf

1
2
3
4
bind 0.0.0.0
requirepass Aa123456
slaveof 192.168.10.1 6379
masterauth Aa123456

查看主从复制信息

r1

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.10.3,port=6379,state=online,offset=155,lag=1
slave1:ip=192.168.10.2,port=6379,state=online,offset=155,lag=1
master_repl_offset:155
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:154

r2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.10.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:309
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

r3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.10.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:365
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

部署哨兵

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected-mode no
# 哨兵sentinel实例运行的端口,默认26379
port 26379
# 哨兵sentinel的工作目录
dir ./

# 哨兵sentinel监控的redis主节点的
## ip:主机ip地址
## port:哨兵端口号
## master-name:可以自己命名的主节点名字(只能由字母A-z、数字0-9 、这三个字符".-_"组成。)
## quorum:当这些quorum个数sentinel哨兵认为master主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 192.168.10.1 6379 2

# 当在Redis实例中开启了requirepass <foobared>,所有连接Redis实例的客户端都要提供密码。
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster Aa123456

# 指定主节点应答哨兵sentinel的最大时间间隔,超过这个时间,哨兵主观上认为主节点下线,默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000

# 指定了在发生failover主备切换时,最多可以有多少个slave同时对新的master进行同步。这个数字越小,完成failover所需的时间就越长;反之,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为1,来保证每次只有一个slave,处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1

# 故障转移的超时时间failover-timeout,默认三分钟,可以用在以下这些方面:
## 1. 同一个sentinel对同一个master两次failover之间的间隔时间。
## 2. 当一个slave从一个错误的master那里同步数据时开始,直到slave被纠正为从正确的master那里同步数据时结束。
## 3. 当想要取消一个正在进行的failover时所需要的时间。
## 4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来同步数据了
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000

# 当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本。一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
# 对于脚本的运行结果有以下规则:
## 1. 若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10。
## 2. 若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
## 3. 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>

启动哨兵服务

1
systemctl start redis-sentinel

查看哨兵刷新写入的配置

r1
刷新写入r2、r3从节点及r2、3两个哨兵节点的配置信息。

1
2
3
4
5
6
7
8
9
# Generated by CONFIG REWRITE
supervised systemd
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel known-slave mymaster 192.168.10.2 6379
sentinel known-slave mymaster 192.168.10.3 6379
sentinel known-sentinel mymaster 192.168.10.3 26379 109b82938685fa6fd406a5b4714f1ccd2fbdc215
sentinel known-sentinel mymaster 192.168.10.2 26379 1b8f4eae194429b94dd368c990070641c83b11e0
sentinel current-epoch 0

r2
刷新写入r2、r3从节点及r1、r3两个哨兵节点的配置信息。

1
2
3
4
5
6
7
8
9
# Generated by CONFIG REWRITE
supervised systemd
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel known-slave mymaster 192.168.10.2 6379
sentinel known-slave mymaster 192.168.10.3 6379
sentinel known-sentinel mymaster 192.168.10.3 26379 109b82938685fa6fd406a5b4714f1ccd2fbdc215
sentinel known-sentinel mymaster 192.168.10.1 26379 794c10425169c727265fa3388460209a8595d2e4
sentinel current-epoch 0

r3
刷新写入r2、r3从节点及r1、r2两个哨兵节点的配置信息。

1
2
3
4
5
6
7
8
9
# Generated by CONFIG REWRITE
supervised systemd
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel known-slave mymaster 192.168.10.2 6379
sentinel known-slave mymaster 192.168.10.3 6379
sentinel known-sentinel mymaster 192.168.10.1 26379 794c10425169c727265fa3388460209a8595d2e4
sentinel known-sentinel mymaster 192.168.10.2 26379 1b8f4eae194429b94dd368c990070641c83b11e0
sentinel current-epoch 0

哨兵客户端命令

检查其他 Sentinel 节点的状态,返回 PONG 为正常。

1
PING sentinel

显示被监控的所有 主节点 以及它们的状态。

1
SENTINEL masters

显示指定 主节点 的信息和状态。

1
SENTINEL master <master_name>

显示指定 主节点 的所有 从节点 以及它们的状态。

1
SENTINEL slaves <master_name>

返回指定 主节点IP 地址和 端口。如果正在进行 failover 或者 failover 已经完成,将会显示被提升为 主节点从节点IP 地址和 端口

1
SENTINEL get-master-addr-by-name <master_name>

重置名字匹配该 正则表达式 的所有的 主节点 的状态信息,清除它之前的 状态信息,以及 从节点 的信息。

1
SENTINEL reset <pattern>

强制当前 Sentinel 节点执行 failover,并且不需要得到其他 Sentinel 节点的同意。但是 failover 后会将 最新的配置 发送给其他 Sentinel 节点。

1
SENTINEL failover <master_name>

哨兵故障切换测试

将r1 redis进程kill,查看r3变为主节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
127.0.0.1:26379> SENTINEL master master
1) "name"
2) "master"
3) "ip"
4) "192.168.10.3"
5) "port"
6) "6379"
7) "runid"
8) "5384b3eb8501c0c2a2f1d22ba3843e42c0ed53b4"
9) "flags"
10) "master"
11) "link-pending-commands"
12) "0"
13) "link-refcount"
14) "1"
15) "last-ping-sent"
16) "0"
17) "last-ok-ping-reply"
18) "830"
19) "last-ping-reply"
20) "830"
21) "down-after-milliseconds"
22) "5000"
23) "info-refresh"
24) "9130"
25) "role-reported"
26) "master"
27) "role-reported-time"
28) "40139"
29) "config-epoch"
30) "1"
31) "num-slaves"
32) "2"
33) "num-other-sentinels"
34) "2"
35) "quorum"
36) "2"
37) "failover-timeout"
38) "180000"
39) "parallel-syncs"
40) "1"

哨兵日志

1
2
3
4
5
6
7
8
9
10
25864:X 09 Aug 13:29:12.668 # +sdown master master 192.168.10.1 6379
25864:X 09 Aug 13:29:12.800 # +new-epoch 1
25864:X 09 Aug 13:29:12.802 # +vote-for-leader 8a12b84bd15e848a607ed4fbf5debcecdc7670a2 1
25864:X 09 Aug 13:29:13.806 # +odown master master 192.168.10.1 6379 #quorum 3/2
25864:X 09 Aug 13:29:13.806 # Next failover delay: I will not start a failover before Mon Aug 9 13:35:13 2019
25864:X 09 Aug 13:29:13.976 # +config-update-from sentinel 8a12b84bd15e848a607ed4fbf5debcecdc7670a2 192.168.10.3 26379 @ master 192.168.10.1 6379
25864:X 09 Aug 13:29:13.976 # +switch-master master 192.168.10.1 6379 192.168.10.3 6379
25864:X 09 Aug 13:29:13.976 * +slave slave 192.168.10.2:6379 192.168.10.2 6379 @ master 192.168.10.3 6379
25864:X 09 Aug 13:29:13.976 * +slave slave 192.168.10.1:6379 192.168.10.1 6379 @ master 192.168.10.3 6379
25864:X 09 Aug 13:29:18.979 # +sdown slave 192.168.10.1:6379 192.168.10.1 6379 @ master 192.168.10.3 6379

分析日志

r1先进入 sdown 主观下线 状态

1
+sdown master master 192.168.10.1 6379

哨兵检测到 r1 出现故障,Sentinel 进入一个 新纪元,从 0 变为 1

1
+new-epoch 1

三个 Sentinel 节点开始协商 主节点 的状态,判断其是否需要 客观下线

1
+vote-for-leader 8a12b84bd15e848a607ed4fbf5debcecdc7670a2 1

超过 quorum 个数的 Sentinel 节点认为 主节点 出现故障,r1 节点进入 客观下线 状态

1
+odown master master 192.168.10.1 6379 #quorum 3/2

Sentinal 进行 自动故障切换,协商选定 r3 节点作为新的 主节点

1
+switch-master master 192.168.10.1 6379 192.168.10.3 6379

r1、r2成为r3的从节点

1
2
+slave slave 192.168.10.2:6379 192.168.10.2 6379 @ master 192.168.10.3 6379
+slave slave 192.168.10.1:6379 192.168.10.1 6379 @ master 192.168.10.3 6379

Python连接redis哨兵集群

安装redis模块

1
2
yum install python-pip
pip install redis

redis_test.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import redis
from redis.sentinel import Sentinel

# 连接哨兵服务器(主机名也可以用域名)
sentinel = Sentinel([('192.168.10.1', 26379),('192.168.10.2', 26379),('192.168.10.2', 26379)],socket_timeout=1)

# 获取主节点地址
master = sentinel.discover_master('master')
print 'master is', master

# 获取从节点地址
slave = sentinel.discover_slaves('master')
print 'slave is', slave

# 获取主节点并写入
print '#set key abc=123'
master = sentinel.master_for('master', socket_timeout=1, password='Aa123456', db=15)
w_ret = master.set('abc', '123')

# 获取从节点并读取(默认是round-roubin)
slave = sentinel.slave_for('master', socket_timeout=1, password='Aa123456', db=15)
r_ret = slave.get('abc')
print 'abc value is', r_ret

执行脚本

1
2
3
4
5
> python redis_test.py
master is ('192.168.10.3', 6379)
slave is [('192.168.10.1', 6379), ('192.168.10.2', 6379)]
#set key abc=123
abc value is 123

r3为主节点,r1和r2为从节点。

将r3上的redis进程kill,再执行脚本

1
2
3
4
> master is ('192.168.10.2', 6379)
slave is [('192.168.10.1', 6379)]
#set key abc=123
abc value is 123

r2变为主节点,r1为从节点。此时集群读写正常。

将r2上的redis进程也kill掉,再执行脚本

1
2
3
4
5
> python redis_test.py
master is ('192.168.10.1', 6379)
slave is []
#set key abc=123
abc value is 123

r1变为主节点,没有从节点。此时集群读写正常。