Sentinel(哨兵、哨岗)是Redis 的高可用性的解决方案:有一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
补充下:在替换了新的主服务器之后,如果之前下线的主服务器上线了,就会被降为新的主服务器的从服务器。
命令:
$ redis-sentinel /path/to/your/sentinel.conf
或者
$ redis-sentinel /path/to/your/sentinel.conf --sentinel
这两个命令都能启动Sentinel,效果都是一样的。
Sentinel启动后,会有五个步骤:
Sentinel的本质是一个运行在特殊模式下的Redis服务器,因此启动时必须对其进行初始化,但是由于Sentinel与普通的服务器不同,它的初始化需要执行的操作也不同。
下表是Sentinel 模式下Redis服务器的主要功能的使用情况
功能 | 使用情况 |
---|---|
数据库和键值对方面的命令,比如SET、DEL、FLUSHDB |
不使用 |
事务命令,比如MULTI 和WATCH |
不使用 |
脚本命令,比如EVAL |
不使用 |
RDB持久化命令,比如SAVE 和BGSAVE |
不使用 |
AOF持久化命令,比如BGREWRITEAOF |
不使用 |
复制命令,比如SLAVEOF |
Sentinel内部可以使用,但是客户端不能使用 |
发布和订阅命令,比如PUBLISH 和SUBSCRIBE |
SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE 四个命令在Sentinel内部和客户端都可以使用,但是PUBLISH 命令只能在Sentinel内部使用 |
文件事件处理器(负责发送命令请求、处理命令回复) | Sentinel内部使用,但关联的文件事件处理器和普通Redis服务器不同 |
时间事件处理器(负责serverCron 函数) |
Sentinel内部使用,时间事件的处理器仍然是serverCron 函数,serverCron 函数会调用sentinel.c/sentinelTimer 函数,后者包含了Sentinel要执行的操作 |
启动Sentinel的第二步,就是将普通Redis服务器使用的代码替换成Sentinel专用的代码。
比如 普通Redis服务器使用 redis.h/REDIS_SERVERPORT
常量作为服务端口(#define REDIS_SERVERPORT 6379
),使用 redis.h/redisCommandTable
作为服务器的命令表。
而Sentinel使用 reids.h/REDIS_SENTINEL_PORT
常量作为服务器端口,默认26379,使用 redis.h/sentinelcmds
作为服务器的命令表且其中的INFO
命令使用Sentinel模式下的专用实现 sentinel.c/sentinelInfoCommand
函数,而不是普通Redis服务器的 redis.h/infoCommand
函数。
在这我把sentinel命令表展示出来吧,普通redis服务器的命令表百度了解吧:
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};
接下来,服务器会初始化一个 sentinel.c/sentinelState
结构(简称“Sentinel状态”),这个结构保存了服务器所有和Sentinel功能有关的状态,服务器的一般状态仍然有 redis.h/redisServer
结构保存:
typedef struct sentinelState{
// 当前纪元,用于实现故障转移
uint64_t current_epoch;
// 保存了所有被这个 Sentinel监视的主服务器
// 字典的键是主服务器的名字
// 字典的值是一个指向 sentinelRedisInstance 结构的指针
dict *masters;
// 是否进入了 TILT 模式
int tilt;
// 目前正在执行的脚本数量
int running_scripts;
// 进入 TILT 模式的时间
mstime_t tilt_start_time;
// 最后一次执行事件处理器的时间
mstime_t previous_time;
// 一个 FIFO 队列,包含了所有需要执行的用户脚本
list *scripts_queue;
}sentinel;
masters
属性接下来要做的是将sentinel状态的 masters
属性进行初始化,上面已经说过了,masters
里面保存的是所有被监视的主服务器的信息以及其键值对应保存的是什么内容。
我们先介绍一下 sentinelRedisInstance
结构(简称“实例结构”),这个结构代表着一个被Sentinel监视的Redis服务器实例,可以是主服务器、从服务器或者另外一个Sentinel.
下面介绍下其数据结构:
typedef struct sentinelRedisInstance{
// 标识值,记录了实例的类型,以及该实例的当前状态
int flags;
// 实例的名字
// 主服务器的名字有用户在配置文件
// 从服务器以及sentinel的名字有Sentinel自动设置
// 格式 为ip:port,例如“127.0.0.1:26379”
char *name;
// 实例的运行ID
char *runid;
// 配置纪元,用于实现故障转移
uint64_t config_epoch;
// 实例的地址
sentinelAddr *addr;
// SENTINEL down-after-millseconds 选项设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线
mstime_t down_after_period;
// SENTINEL monitor 选项中的quorum参数
// 判断这个实例为客观下线(objectively down)所需的支持投票数量
int quorum;
// SENTINEL parallel-syncs 选项的值
// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;
// SENTINEL failover-timeout 选项的值
// 刷新故障迁移状态的最大时限
mstime_t failover_timeout;
// ...
}sentinelRedisInstance;
其中的 addr
属性是一个指向 sentinel.c/sentinelAddr
结构的指针,这个结构保存实例的IP地址和端口号:
typedef struct sentinelAddr{
char *p;
int port;
}sentinelAddr;
对Sentinel 状态的初始化将引发对 masters
字典的初始化,而 masters
字典的初始化是根据被载入的Sentinel配置文件来进行的。
这是最后一步啦,这一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息。
每个被Sentinel监视的主服务器,Sentinel会创建两个连向主服务器的异步网络连接:
__sentinel__:hello
频道Sentinel 默认每十秒一次,通过命令连接向被监视的主服务器发送 INFO
命令,并通过分析 INFO
命令回复来获取主服务器当前信息。两方面信息:
关于服务器本身的信息
包括 run_id
域记录的服务器运行ID,以及 role
域记录的服务器角色
关于主服务器属下的所有从服务器信息
每个从服务器都由一个“slave”字符串开头的行记录,每行的 ip=
域记录了从服务器的IP地址, port=
域记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。
根据 run_id
域和 role
域的信息,Sentinel将对主服务器的实例结构进行更新。而主服务器返回的从服务器信息,将会被用于更新主服务器实例结构的 slaves
字典(记录了属下从服务器的名单)。
Sentinel 分析 INFO
命令中包含的从服务器信息时,会检查从服务器对应的实例结构是否已经存在于 slaves
字典: 如果存在,就对从服务器的实例结构进行更新,如果不存在(表明这个从服务器是新发现的从服务器),Sentinel会在 slaves
字典中为这个从服务器创建一个新的实例结构。
注:
flags
值为 SRI_MASTER
,从服务器是 SRI_SLAVE
name
由用户使用Sentinel配置文件设置,从服务器的name
是由Sentinel根据服务器ip+port自动设置的当Sentinel发现主服务器有新的服务器出现时,除了会为这个新从服务器创建相应的实例结构之外,还会创建连接到从服务器的命令连接和订阅连接。
创建了命令连接之后,每10秒一次向从服务器发送 INFO
命令,并根据回复分析以下信息:
run_id
role
master_host
以及主服务器的端口号 master_port
master_link_status
slave_priority
slave_repl_offset
根据这些信息,Sentinel会对从服务器的实例结构进行更新。
每两秒一次,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:
PUBLISH __sentinel__:hello ",,,s_epoch>,
,,,"
这条命令就表示向服务器的 __sentinel__:hello
频道发送一条信息,信息由一下部分组成:
s_
开头的参数记录Sentinel本身的信息m_
开头的参数则是主服务器的信息,当然如果监视的是从服务器,这个信息表示的就是从服务器的信息下表是 该命令中的相关参数
参数 | 意义 |
---|---|
s_ip |
Sentinel的ip地址 |
s_port |
Sentinel的端口号 |
s_runid |
Sentinel的运行ID |
s_epoch |
Sentinel当前的配置纪元 |
m_ip |
主服务器的ip地址 |
m_port |
主服务器的端口号 |
m_runid |
主服务器的运行ID |
m_epoch |
主服务器当前的配置纪元 |
在建立起订阅连接之后,Sentinel会通过这个连接,向服务器发送以下命令:
SUBSCRIBE __sentinel__:hello
Sentinel对 __sentinel__:hello
这个定于会一直持续到Sentinel与服务器的连接断开之后。
可以通过这个频道发送和接收信息。
对于监视同一服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他的Sentinel接收到,并用于更新其他Sentinel对发送信息Sentinel的认知,和被用于更新其他Sentinel对被监视服务器的认知。
sentinels
字典sentinels
字典保存了除Sentinel本身之外,所有同样监视这个主服务器的其他Sentinel资料:
ip:port
当一个Sentinel接收到其他Sentinel发来的信息时(称发送信息的Sentinel为源Sentinel,接收信息的Sentinel为目标Sentinel),目标Sentinel会从信息中分析出以下信息:
根据这些主服务器参数,目标Sentinel会在自己的Sentinel状态的 masters
字典中查找相应的主服务器实例结构,然后根据提出的Sentinel参数,检查主服务器实例结构的 sentinels
字典中,源 Sentinel的实例结构是否存在:
sentinels
字典里面当Sentinel通过频道信息发现了一个新的Sentinel时,它不仅会为新的Sentinel在 sentinels
字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接。新的Sentinel同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:SentinelA有连向SentinelB的命令连接,SentinelB也有连向SentinelA的命令连接。
注:Sentinel之间不会创建订阅连接
默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送 PING
命令,并通过实例返回的 PING
命令回复来判断实例是否在线。
两种实例对 PING
指令的回复情况:
+PONG
、 -LOADING
、-MASTERDOWN
三种其中一种Sentinel配置文件中的 down-after-millseconds
选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在 down-after-millseconds
毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的 flags
属性中打开 SRI_S_DOWN
标识,用于表示这个实例已经进入主观下线状态。
注:
1. 主观下线时长选项,即 down-after-down
的值,不仅会被Sentinel用于判断主服务器的主观下线状态,还会被用于判断主服务器属下的所有从服务器,以及所有同样监视这个主服务器的其他Sentinel的主观下线状态。
2. 多个Sentinel设置的主观下线时长可能不同,对于监视同一个主服务器的多个Sentinel来说,这些Sentinel设置的 down-after-milliseconds
选项的值可能不同,因此,当一个Sentinel将主服务器判断为主观下线时,其它Sentinel可能任然会认为主服务器处于在线状态。
当Sentinel将一个主服务器判断为主观下线之后,为确定这个服务器是否真的下线,它会向同样监视这个主服务器的其它Sentinel进行询问,当接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器进行故障转移操作。
SENTINEL is-master-down-by-addr
命令Sentinel会发送下面的命令询问其它Sentinel是否同意主服务器下线:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
下表是参数的意义:
参数 | 意义 |
---|---|
ip |
被Sentinel判断 为主观下线的主服务器ip |
port |
被Sentinel判断 为主观下线的主服务器端口号 |
current_epoch |
Sentinel当前的配置纪元,用于选举领头Sentinel |
runid |
可以是 * 符号或者Sentinel的运行ID,* 表示命令仅仅用于检测主服务器的客观下线状态,而Sentinel的运行ID则用于选举领头Sentinel |
SENTINEL is-master-down-by-addr
命令当一个Sentinel(目标Sentinel)接收到另外一个Sentinel(源Sentinel)发来的 SENTINEL is-master-by-addr
命令时,目标Sentinel会分析并取出命令请求中包含的各个参数,并根据其中的IP和port,判断主服务器是否已经下线,然后向源Sentinel返回一个包含三个参数的 Multi Bulk
回复作为这个命令的回复。
这三个参数分别是:
返回目标Sentinel对主服务器的检查结果,1表示主服务器已下线,0表示主服务器未下线
可以是 *
符号或者目标Sentinel的局部领头Sentinel的运行ID,*
表示命令仅仅用于检测主服务器的下线状态,而局部领头Sentinel的运行ID则用于选举领头Sentinel
目标Sentinel的局部领头Sentinel的配置纪元,用于选举领头Sentinel。仅在 leader_runid
值不为 *
时有效,如果其值为 *
,这个参数总为0
SENTINEL is-master-down-by-addr
命令的回复根据其他Sentinel发回的 SENTINEL is-master-down-by-addr
回复,Sentinel将统计同意主服务器下线的数量,当这个值达到配置指定的判断客观下线所需的数量时(即 quorum
属性的值),Sentinel会将主服务器实例结构中 flags
属性的 SRI_O_DOWN
标识打开,标识主服务器已经进入客观下线状态。
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器进行故障转移操作。
至于如何选举领头Sentinel的规则和方法:
SENTINEL is-master-down-by-addr + SentinelA 的runid
,这就代表A想成为B的局部领头在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器进行故障转移操作:
在已下线的主服务器属下的所有从服务器中,挑选一个从服务器作为主服务器
挑选一个状态良好、数据完整的从服务器,然后发送 SLAVEOF no one
命令,然后将这个从服务器转换成主服务器
让已下线的主服务器的所有从服务器改为复制新的主服务器
将已下线主服务器设置为新的主服务器的从服务器,这个旧的主服务器重新上线时,就会成为新的主服务器的从服务器。