当主机宕机后出现故障无法及时恢复,可以在从机执行slave no one
命令使其上位变为主机,其他主机会自动跟随,原主机恢复后也会变为从机跟随新的主机。但这样的人工操作带来一个问题,难道半夜主机宕机还要通知运维起来输命令吗?这显然不符合常理,而且有个弊端,即宕机后的主机恢复后就是孤零零的一个普通服务器,主从服务器中就少了一个服务器,降低了集体性能。为了自动化管理这个过程,引入了哨兵机制。
什么是哨兵(Sentinel)?
简单来讲,Sentinel
就是一个特殊的Redis服务器,负责监控其他服务器的在线状态。
复杂一点来说,Sentinel
(哨兵、哨岗)是Redis的高可用性解决方案:由一个个Sentinel
组成的Sentinel
系统(至少由3个哨兵组成)可以监视多个主服务器以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。如下图:
因为有哨兵机制,所以当主服务器发生故障时,会发生下面的过程:
当然其中的往细了讲挺复杂的,比如新的主服务器该选哪个从服务器啊,旧主服务器怎么变成新主服务器的从服务器啊等等各种问题具体实现。简单来说,哨兵建立的过程包括:启动并初始化哨兵、获取主/从服务器信息、检测主观/客观下线状态、故障转移工作。
哨兵建立及后续分析
启动并初始化Sentinel
启动一个Sentinel
需要使用redis-sentinel
命令:
redis-sentinel /usr/locat/etc/redis-sentinel.conf
其中redis-sentinel.conf
为Sentinel
的配置信息,和Redis提供的默认redis.conf
文件一样,Redis也提供了一份默认的名为redis-sentinel.conf
的哨兵配置文件,部分内容如下:
port 26379 //端口号
daemonize no //是否守护
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
sentinel monitor mymaster 127.0.0.1 6379 2
代表哨兵监控名为mymaster
的主服务器,它位于地址127.0.0.1和端口6379,仲裁人数为2,仲裁代表当主服务器挂掉后:
- 如果两个Sentinels同时同意主服务器无法访问,则其中一个将尝试启动故障转移。
- 如果至少总共有三个Sentinel可达,则故障转移将被授权并实际开始。
down-after-milliseconds
:主服务器主观下线时间
failover-timeout
parallel-syncs
初始化的Sentinel
因为Sentinel
本质上是一个运行在特殊模式的Redis服务器,所以启动的第一步,就是初始化一个普通的Redis服务器,但它的初始化过程和普通服务器的初始过程不同,它们功能不同:
使用Sentinel专用代码
启动Sentinel
的第二步就是将一部分Redis服务器使用的代码换成Sentinel
的专用代码。比如普通服务器的get
、set
、setnx
等等命令不再使用,而是使用它专属的代码处理其他事情,如sentinel
、subscribe
、info
等等命令负责监控相关的功能。本质就是原先某些函数库不再使用,而是使用新的函数库,毕竟旧功能咱用不到,新的功能普通Redis服务器原来没有,而Sentinel
就加上,如下图:
这也解释了为什么Sentinel
为什么不能执行原先Redis服务器里的set
、get
等等命令,因为Sentinel
根本没有载入这些命令
初始化Sentinel状态结构
在应用了Sentinel
的专用代码之后,接下来,服务器会初始化一个叫做哨兵状态(sentinelState
)的结构,保存了服务器中所有和Sentinel
功能相关的状态。
创建与主服务器的网络连接
初始Sentinel
的最后一步是创建与被监视主服务器的网络连接,Sentinel
将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中回去相关的信息。
对于每个被Sentinel
监视的主服务器,Sentinel
都会创建2个与主服务器的异步网络连接:
- 命令连接:专门用于向主服务器发送命令,并接受回复信息
-
订阅连接:专门用于订阅主服务器的
_sentinel_:hello
频道
获取主/从服务器信息
如上图,对主服务器:Sentinel
默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送info
命令来获取主服务器的当前信息(包括主服务器本身的信息,及其属下所有从服务器的信息),根据这些信息来更新主服务器的实例结构(若主服务器重启的ID将和实例结构不一致,将其更新)及从服务器的实例结构。
如上图,对从服务器:当Sentinel
发现主服务器有新的从服务器出现时,其出了会为新的从服务器创建对应的实例结构之外,还会创建连接到从服务器的命令连接和订阅连接。
在创建命令连接,Sentinel
在默认情况下,以每十秒一次的频率通过命令连接向从服务器发送info
命令,获取其信息,并对从服务器的实例结构进行更新。
向主/从服务器发送信息到频道
默认情况下,Sentinel
会以每两秒一次的频率,通过命令向所有被监控的主和从服务器发送以下格式的命令:
这个命令包含的信息包括
Sentinel
本身的信息,也包括主服务器的信息。举个例子,一个Sentinel
通过publish
命令向主服务器发送的信息实例:
"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0,mymaster,127.0.0.1,6379,0"
这些信息包含:
-
Sentinel
的IP地址为127.0.0.1,端口号为26379,运行ID为e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa
,当前配置纪元为0; - 主服务器的名字为
mymaster
,IP地址为127.0.0.1,端口号为6379,当前的配置纪元为0
接受主/从服务器的频道信息
当Sentinel
与一个主服务器或从服务器建立起订阅连接之后,Sentinel
就会通过订阅连接,向服务器发送以下命令:
SUBSCRIBE _sentinel_:hello
Sentinel
对_sentinel_:hello
频道的订阅会一致持续到Sentinel
与服务器的连接断开为止。
这也就是说,对于每个与Sentinel
连接的服务器,Sentinel
既通过命令连接向服务器的_sentinel_:hello
频道发送信息,又通过订阅连接从
服务器的_sentinel_:hello
频道接受信息,如下图:
对于监视同一个服务器的多个Sentinel
来说,一个Sentinel
发送的信息会被其他Sentinel
接收到,这些信息会被其他Sentinel
用于更新对发送信息的Sentinel
的认知,也会被其他Sentinel
用于更新对被监视服务器的认知,如下图:
当一个
Sentinel
从_sentinel_:hello
频道接收到一条信息时,会对该信息进行分析(提取信息中的Sentinel
IP、port、run ID等等信息),并进行下列检查:
- 如果信息中记录的
Sentinel run ID
和接受信息的Sentinel run ID
相同,说明是自己发送的,将丢弃这条信息,不做处理 - 如果两者run ID不同,说明是其他
Sentinel
发送的,将根据信息中的各个参数,Sentinel
将对自身创建的主服务器的实例结构进行更新
更新sentinels字典
Sentinel
为主服务器创建的实例结构中的sentinels
字典除了保存Sentinel
本身之外,还保存了所有同样监视这个主服务器的其他Sentinel
资料(sentinels
字典的键为Sentinel
名字,值为对应的Sentinel
的实例结构)
当一个Sentinel
(目标Sentinel
)接收到其他Sentinel
(源Sentinel
)发来的信息时,目标Sentinel
会从信息分析并提取相关参数(源Sentinel
相关参数如IP、端口号、run ID和配置纪元,被监视主服务器参数如名字、IP、端口号和配置纪元),对自身的实例结构进行更新
创建连向其他Sentinel的命令连接
当Sentinel
通过频道信息发现一个新的Sentinel
时,它不仅会为新Sentinel
在sentinels
字典中创建相应的实例结构,还会创建一个连接向新Sentinel
的命令连接,而新Sentinel
也会创建与这个Sentinel
的命令连接,最后监视同一主服务器的多个Sentinel
形成一个相互连接的网络,如下图:
注:``Sentinel
在连接主服务器或这从服务器时,会同时创建命令连接和订阅连接,但连接其他Sentinel
时,只创建命令连接。因为Sentinel
需要通过接受主服务器或从服务器发来的频道信息来发现未知的新Sentinel
,所以需要建立订阅连接,而相互已知的Sentinel
只要使用命令连接来进行通信就足够了。
检测主观/客观下线状态
在默认情况下,Sentinel
会以每秒一次的频率向所有与它创建了命令连接的实例(包括所有主/从服务器,所有其他Sentinel
)发送ping
命令,并通过实例返回的有效或无效回复来判断实例是否在线。
什么是主观下线状态?
前面配置文件中的down-after-milliseconds
选项指定了Sentinel
判断实例进入主观下线的时间长短,如果在该时间内实例连续向Sentinel
发送无效回复,则Sentinel
会修改这个实例的实例结构,来表示这个实例已经进入主观下线状态。
举个例子,如果配置文件指定的down-after-milliseconds
的选项值为50000
毫秒,那么当主服务器连续50000
毫秒都返回给Sentinel
无效回复是,Sentinel
就会将主服务器实例结构修改并标记为主观下线。
而且配置文件指定的down-after-milliseconds
选项也会被Sentinel
用来判断主服务器下所有从服务器的主观下线限时时间标准。
哦,对了,因为基本不可能只有一个Sentinel
,所以其他Sentinel
也会执行上面的判断,只不过判断的down-after-milliseconds
时间可能不同,因为配置文件设置的不同。所以可能会有这个Sentinel
判断主服务器50000
毫秒就主观下线了,而另一个Sentinel
却判断主服务器是70000
秒才主观下线的情况发生。
什么是客观下线状态?
当Sentinel
将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel
进行询问,看它们是否也认为主服务器已经进入下线状态(可以是主观下线或客观下线)。当Sentinel
从其他Sentinel
那里接收到足够数量(总数为设置的仲裁值)的已下线判断之后,Sentinel
就会将服务器判断为客观下线,并对主服务器执行故障转移操作。
选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个现象主服务器的各个Sentinel
会进行协商,选举出一个领头Sentinel
,并由领头Sentinel
对下线主服务器执行故障转移工作,理所当然选举会基于一定规则,此处不介绍。
故障转移
在选举出领头Sentinel
之后,其将对已下线的主服务器执行故障转移操作,包含3个步骤:
1):在已下线的主服务器下的所有从服务器基于一定挑选一个将其转换为主服务器(根据从服务器权值大小、复制偏移量大小等规则挑选);
2):让已下线主服务器下的所有从服务器改为复制新的主服务器;
3):将已下线主服务器设置为新的主服务器的从服务器,当这个旧主服务器上线时就会变成新的主服务器的从服务器
具体过程如下图:
在Redis服务器中使用哨兵
哨兵使用前提:至少需要3个Redis服务器(一主二仆)。哨兵的使用有以下步骤:
- 创建哨兵配置文件
cd /usr/local/etc/master-slave
touch redis-sentinel1.conf
- 配置哨兵配置文件
redis-sentinel.conf1
(一个例子所以简略配置),加入:
sentinel monitor mymaster 127.0.0.1 6380 1
- 启动哨兵:
redis-sentinel /usr/local/etc/master-slave /redis-sentinel1.conf
启动界面如下:
31682:X 12 Apr 2019 15:19:27.937 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
31682:X 12 Apr 2019 15:19:27.937 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=31682, just started
31682:X 12 Apr 2019 15:19:27.937 # Configuration loaded
31682:X 12 Apr 2019 15:19:27.939 * Increased maximum number of open files to 10032 (it was originally set to 256).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 5.0.4 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in sentinel mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 26379
| `-._ `._ / _.-' | PID: 31682
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
31682:X 12 Apr 2019 15:19:27.940 # Sentinel ID is 6712a29afc6e6e776bb6aaa661d087ceb8170e89
31682:X 12 Apr 2019 15:19:27.940 # +monitor master mymaster 127.0.0.1 6380 quorum 1
31682:X 12 Apr 2019 15:19:27.940 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:19:27.941 * +slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
很显然,哨兵监控了主服务器,并知道了其从服务器信息,当主机宕机后(模拟主机关闭):
127.0.0.1:6380> shutdown
过了一段时间后,哨兵日志有如下内容:
31682:X 12 Apr 2019 15:21:14.993 # +sdown master mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:14.993 # +odown master mymaster 127.0.0.1 6380 #quorum 1/1
31682:X 12 Apr 2019 15:21:14.993 # +new-epoch 1
31682:X 12 Apr 2019 15:21:14.993 # +try-failover master mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:14.996 # +vote-for-leader 6712a29afc6e6e776bb6aaa661d087ceb8170e89 1
31682:X 12 Apr 2019 15:21:14.996 # +elected-leader master mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:14.996 # +failover-state-select-slave master mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:15.064 # +selected-slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:15.064 * +failover-state-send-slaveof-noone slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:15.130 * +failover-state-wait-promotion slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:15.566 # +promoted-slave slave 127.0.0.1:6382 127.0.0.1 6382 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:15.566 # +failover-state-reconf-slaves master mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:15.638 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:16.588 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:16.588 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:16.692 # +failover-end master mymaster 127.0.0.1 6380
31682:X 12 Apr 2019 15:21:16.692 # +switch-master mymaster 127.0.0.1 6380 127.0.0.1 6382
31682:X 12 Apr 2019 15:21:16.692 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6382
31682:X 12 Apr 2019 15:21:16.692 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6382
31682:X 12 Apr 2019 15:21:46.776 # +sdown slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6382
可以看到,哨兵将本地6382端口的从机升级为了主机:
127.0.0.1:6382> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=25036,lag=1
master_replid:9a87c8ae6ea203b87991d4edb3584b3f83e73439
master_replid2:a5183310e466a5a9e7d2c465a1eea051575ab130
master_repl_offset:25036
second_repl_offset:24482
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:25036
而6381变为了其从机:
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6382
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:25316
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:9a87c8ae6ea203b87991d4edb3584b3f83e73439
master_replid2:a5183310e466a5a9e7d2c465a1eea051575ab130
master_repl_offset:25316
second_repl_offset:24482
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:25316
当旧主机重新上线后,变为6382的从机:
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6382
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:43259
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:9a87c8ae6ea203b87991d4edb3584b3f83e73439
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:43259
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:43104
repl_backlog_histlen:156
参考资料
《redis设计与实现》(第二版)