哨兵(Sentinel)是redis高可用性的解决方案,由一个或多个哨兵实例组成的哨兵系统,可以监视任意多个主服务器,以及这些主服务器属下的从服务器。
当被监视的主服务器下线时,根据某些规则挑选一个从服务器,作为新的主服务器。接着,其他从服务器会向新的主服务器发送复制指令,并且完成复制。同时,哨兵会监视下线的原主服务器,在它重新上线后,将它也置为从服务器。
目录
Sentinel启动与初始化
与服务器通信
故障检测与处理
总结
1、启动命令
redis-sentinel /path/to/sentinel/config/sentinel.conf
或 redis-server/path/to/sentinel/config/sentinel.conf --sentinel
启动一个哨兵可以使用这两个命令中的一个,效果完全相同,当一个哨兵启动时,需要完成以下步骤:
2、初始化服务器
Sentinel 本质上只是一个运行在特殊模式下的 Redis 服务器,所以启动 Sentinel 的第一步,就是初始化一个普通的 Redis 服务器, 具体的步骤可见Redis客户端/服务器。
不过, 因为 Sentinel 执行的工作和普通 Redis 服务器执行的工作不同, 所以 Sentinel 的初始化过程和普通 Redis 服务器的初始化过程并不完全相同。比如,普通服务器在初始化时会通过载入 RDB 文件或者 AOF 文件来还原数据库状态,但是因为 Sentinel 并不使用数据库,所以初始化 Sentinel 时就不会载入 RDB 文件或者 AOF 文件。
下表为 Redis 服务器在 Sentinel 模式下运行时, 服务器各个主要功能的使用情况:
功能 | 使用情况 |
---|---|
数据库和键值对方面的命令, 比如 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 要执行的所有操作。 |
3、使用 Sentinel 专用代码
启动 Sentinel 的第二个步骤就是将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。
REDIS_SERVERPORT
常量的值作为服务器端口:#define REDIS_SERVERPORT 6379 ;而 Sentinel 则使用 REDIS_SENTINEL_PORT
常量的值作为服务器端口:#define REDIS_SENTINEL_PORT 26379redis.c/redisCommandTable
作为服务器的命令表,而 Sentinel 则使用 sentinel.c/sentinelcmds
作为服务器的命令表sentinelcmds
命令表也解释了为什么在 Sentinel 模式下, Redis 服务器不能执行诸如 SET 、 DBSIZE 、 EVAL 等等这些命令 —— 因为服务器根本没有在命令表中载入这些命令: PING 、 SENTINEL 、 INFO 、 SUBSCRIBE 、 UNSUBSCRIBE 、
PSUBSCRIBE 和 PUNSUBSCRIBE 这七个命令就是客户端可以对 Sentinel 执行的全部命令了。
4、初始化 Sentinel 状态
在应用了 Sentinel 的专用代码之后, 服务器会初始化一个 sentinel.c/sentinelState
结构(后面简称“Sentinel 状态”),这个结构保存了服务器中所有和 Sentinel 功能有关的状态 (服务器的一般状态仍然由 redis.h/redisServer
结构保存):
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;
5、Sentinel 监视初始化
Sentinel 状态中的 masters
字典记录了所有被 Sentinel 监视的主服务器的相关信息, 其中:
sentinelRedisInstance
结构。每个 sentinelRedisInstance
结构(后面简称“实例结构”)代表一个被 Sentinel 监视的 Redis 服务器实例(instance),这个实例可以是主服务器、从服务器、或者另外一个 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-milliseconds 选项设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
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
属性是一个指向 sentinelAddr
结构的指针, 这个结构保存着实例的 IP 地址和端口号:
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
对 Sentinel 状态的初始化将引发对 masters
字典的初始化,而masters
字典的初始化是根据被载入的Sentinel配置文件来进行的。
# 载入master1 configure
#####################
sentinel monitor master1 127.0.0.1 6379 2
sentinel down-after-milliseconds master1 30000
sentinel parallel-syncs master1 1
sentinel failover-timeout master1 900000
# 载入master2 configure
#####################
sentinel monitor master2 127.0.0.1 12345 5
sentinel down-after-milliseconds master2 50000
sentinel parallel-syncs master2 5
sentinel failover-timeout master2 450000
那么 Sentinel 将为主服务器 master1
和主服务器 master2
创建如图所示的实例结构,而这两个实例结构又会被保存到 Sentinel 状态的 masters
字典中。
6、连向主服务器的网络连接
初始化 Sentinel 的最后一步是创建连向被监视主服务器的网络连接: Sentinel 将成为主服务器的客户端, 它可以向主服务器发送命令, 并从命令回复中获取相关的信息。
对于每个被 Sentinel 监视的主服务器来说, Sentinel 会创建两个连向主服务器的异步网络连接:
__sentinel__:hello
频道。为什么有两个连接?
在 Redis 目前的发布与订阅功能中,被发送的信息都不会保存在 Redis 服务器里面,如果在信息发送时,想要接收信息的客户端不在线或者断线,那么这个客户端就会丢失这条信息。
因此,为了不丢失
__sentinel__:hello
频道的任何信息,Sentinel 必须专门用一个订阅连接来接收该频道的信息。而另一方面, 除了订阅频道之外,Sentinel 还又必须向主服务器发送命令,以此来与主服务器进行通讯,所以Sentinel 还必须向主服务器创建命令连接。
并且因为 Sentinel 需要与多个实例创建多个网络连接, 所以 Sentinel 使用的是异步连接。
1、获取主服务器信息
sentinel 默认会每10秒一次,给主服务器发送info命令,并且通过分析info命令,来判断当前主服务器的信息。通过分析info命令,sentinel会得到两方面的信息:
因此,sentinel无需建立和从服务器的连接,也可以知道从服务器的情况。sentinel只需要将info获得的返回结果,分析并更新到sentinel的sentinelState结构体的相应属性即可。另外,sentinel在更新结构体时,还会分析每一个从服务器是否存在,如果是现有的则更新结构体,如果不是现有的则新增一个结构体。
从上述可知,主从服务器sentinelRedisInstance结构的主要区别:
2、获取从服务器信息
当 sentinel 发现有新的从服务器,除了会为其创建实例结构,还会与其建立连接,连接也是包括订阅连接和命令连接。且默认下,每10秒也会给从服务器发送info命令。
根据info命令,可以获取从服务器的以下主要信息:从服务器运行ID、从服务器角色、主服务器ip与端口号、主从服务器连接状态、从服务器优先级、从服务器的复制偏移量。
根据这些信息,Sentinel 会对从服务器的实例结构 sentinelRedisInstance 进行更新。
3、与主从服务器进行通信
默认情况下,sentinel会每两秒一次,通过命令连接向被其监听的所有主服务器和从服务器发送以下格式的命令:
publish __sentinel__:hello “,,,,,,,”
这条命令向 __sentinel__:hello频道发送了一条信息,信息的内容中:
sentinel会和每个其监听的服务器建立发布订阅连接,对于每个与Sentinel连接的服务器,Sentinel即通过命令连接向服务器的__sentinel__:hello 频道发送信息,又通过订阅连接从服务器的__sentinel__:hello 频道接受信息,Sentinel会一直监听__sentinel__:hello 频道,直到与服务器连接断开为止。
对于监视同一服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他的Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息的Sentinel的认知,也会被用于更新其他Sentinel对被监视服务器的认知。
sentinel接收到信息将与sentinel自己的运行id进行比对,如果一致则表示信息是自己发送的,sentinel将丢弃不处理信息;如果不一致表示是其他sentinel发送的命令,则会进行比对并更新相应内容(因为发布和订阅都在同一频道,所以自己会收到自己在此频道发布的信息)。
4、更新sentinels字典
sentinel为主服务器建立的实例结构中的 sentinels 字典除了保存自身信息,还会保存其他所有同样监视这个主服务器的sentinel的信息,字典的键是sentinel的ip:port,值是对应的 sentinel实例结构。
当一个 Sentinel 接收到其他的 Sentinel 发来的信息时(发送信息的为源Sentinel,接受信息的为目标Sentinel),目标Sentinel会从信息中分析并提取出以下信息:
根据信息中的主服务器参数,目标Sentinel会在自己的Sentinel状态的 masters 字典中查找相应的主服务器实例结构,然后根据提取出的 Sentinel 参数,检查主服务器实例结构的 sentinels 字典中源Sentinel的实例结构是否存在。
由上可以看出,一个Sentinel可以通过分析接收到的频道信息获知其他Sentinel的存在,并通过发送频道信息来让其他Sentinel知道自己的存在,所以用户在使用Sentinel的时候并不需要提供各个Sentinel的地址信息,监视一个主服务器的各个Sentinel能自动互相发现对方的存在。
5、创建连向其他sentinel的连接
当sentinel发现其他sentinel,不仅会记录其结构信息,还会建立一个命令连接,而新的sentinel也会向该sentinel建立命令连接。最终监视主服务器的多个sentinel将形成网络。
另外,sentinel不会和其他sentinel建立订阅连接,因为不需要订阅相关信息。sentinel之间发送命令和订阅信息,只需要互相用命令连接发送即可。
1、检测主观下线状态
默认情况下,sentinel会每秒1次,向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel)发送ping命令,通过实例返回回复判断实例是否在线。
有效的回复包括pong、loading和mastdown,无效的回复包括超时的回复以及前三个之外的回复。
在sentinel的配置文件中,down_after_milliseconds指定了sentinel判断主观下线的方式,主服务器连续没给出正确的回复超过这个设定的时间,则表示该主服务器被这个sentinel认定为主观下线。
此时,sentinel会在自身记录主服务器的flags加上一个标签,使主服务器此时的flags标签变为SRI_MASTER | SRI_S_DOWN。
配置主观下线的down_after_milliseconds,不仅sentinel会对该主服务器进行这样的判断,也同样会将该参数带到该主服务器下的全部从服务器。
另外,不同的sentinel对同一个主服务器,设定的主观下线的值可以不同。每个sentinel会按照自身的配置文件,来判断检测的主服务器是否主观下线。
主观下线状态可理解为当前的Sentinel主观的认为此服务器已经下线,但不代表这个服务器真的下线,是否真的下线还要看其他也同样监视这个服务器的Sentinel是不是也同样主观认为此服务器已是下线状态,当超过一定数量的监视者Sentinel都主观认为此服务器处于下线状态了,那么此时才会真的认为此服务器是已经下线的,也就是客观下线状态,然后进行故障转移。
2、检测客观下线状态
发送检测主观下线命令
当sentinel认定该主服务器主观下线,会同时给其他几个监测此主服务器的sentinel发送命令,确认在其他sentinel处是否也认为该主服务器主观下线。命令格式为:
sentinel is-master-down-by-addr
其中,runid 如果是 * ,表示仅仅检测该主服务器是否主观下线,如果是用sentinel的运行id,则表示选举领头sentinel。
接收命令回复
当目标Sentinel收到源Sentinel的 is-master-down-by-addr命令后,目标sentinel会返回包含三个参数的multi bulk回复,如下:
3、选举领头Sentinel
根据其他Sentinel的is-master-down-by-addr命令回复,当前Sentinel将统计其他同意主服务器已下线的Sentinel的数量,当这一数量达到配置指定的判断客观下线所需数量时(配置文件中设定的quorum的值),则sentinel将此主服务器的实例结构的flags属性的 SRI_MASTER|SRI_S_DOWN|SRI_O_DOWN 打开,认为该主服务器客观下线。
不同的sentinel可以设置不同的客观下线数量,因此可能部分sentinel认为主服务器客观下线,另一部分认为还没客观下线。
当一个主服务器被判定为客观下线,监视该主服务器的各个sentinel,会协商选举一个领头sentinel,对下线主服务器进行故障转移工作。具体方式如下:
根据命令请求的先后顺序不同,可能会有某个Sentinel的sentinelis-master-down-by-addr命令比其他Sentinel都更快到达,从而在这次领头选举中胜出,然后领头sentinel将对主服务器进行故障转移。
需要注意的是,Sentinel发送的命令还是有可能会同时到达的,导致没法选取超过半数的领头,所以Redis这里采取的是其改进的raft分布式一致性算法,若没有选举出领头,则会让所以参与选举的候选Sentinel都随机的睡眠一段时间,先苏醒的Sentinel会第一时间发送选举的命令,所以最终还是会选出领头的Sentinel。
4、故障转移
选举领头后,领头sentinel将进行故障转移,工作包括:
(1)选出新主服务器
sentinel会将下线的主服务器的从服务器列表中,选出一个从服务器,选择规则如下:
选出主服务器后,sentinel给其发送slaveof no one命令,让其变成主服务器。并且,会每秒一次的频率发送info命令给新主服务器(正常是10秒一次发送info),直到回复中role从slave变成master,表示升级成功。
(2)修改其他从服务器的复制目标
即给其余的从服务器,发送slaveof
(3)将旧主服务器变成从服务器
由于其已经下线,无法直接发送slaveof命令,sentinel会将其保存在实例结构里面,监控并待其重新上线,再发送命令,让其变为新的主服务器的从服务器。
__sentinel__:hello
频道发送消息来向其他 Sentinel 宣告自己的存在。__sentinel__:hello
频道中接收其他 Sentinel 发来的信息,并根据这些信息为其他 Sentinel 创建相应的实例结构,以及命令连接。
参考文章:
《Redis设计与实现》