写在前面
redis 在我们日常的业务开发中是十分常见的,而redis的可用性就必须要有很高的要求,那么 redis集群的高可用由有一个或者多个 Sentinel(哨兵)
实例组成的 哨兵系统来保证的。
哨兵
由一个或者多个 Sentinel 实例组成的 Sentinel 系统可以监控任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,
自动将下线主服务器属下的某个从服务器升级为新主服务器,然后有新的主服务器代替已下线的主服务器继续处理命令请求。
简介
Sentinel 本质上只是一个运行在特殊模式下的Redis服务器,但是 Sentinel 和 Redis的初始化和工作内容是不同的。Sentinel 不需要使用数据库,所以初始化的时候是不需要载入RDB文件或者AOF文件的。而Sentinel的工作内容如下:
功能 | 使用情况 |
---|---|
数据库键值对命令 SET、DEL、FLUSHDB | 不使用 |
事务命令 MULTI,WATCH | 不使用 |
脚本命令 EVAL | 不使用 |
RDB/AOF持久化命令 SAVE、BGSAVE、BGREWRITEAOF | 不使用 |
复制命令,SLAVEOF | Sentinel 内部使用,但是客户端不用 |
发布与订阅命令,比如 PUBLISH 和 SUBSCRIBE | SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUBSUBSCRIBE这四个命令在Sentinel内部和客户端都可以使用,但PUBLISH命令只能在Sentinel内部使用 |
文件事件处理器(负责发送命令和请求、处理命令回复) | Sentinel 内部使用,但关联的文件事件处理器和普通Redis处理器不同 |
事件处理器(负责执行serverCron 函数) | Sentinel 内部使用,时间事件的处理器仍然是ServerCron函数,ServerCron函数会调用sentinel.c/sentineTimer函数,后者包含了Sentinel要执行的所有操作 |
在为什么启动Sentinel的时候,会有这些限制呢?Sentinel 不都是 Redis 吗?
因为在Sentinel初始化的时候,加载的是
src/sentinel.c
文件的函数,而Redis加载的是src/redis.c
文件的函数,而这两个文件的初始化函数,限定了只能使用哪些命令
数据结构
Sentinel 的结构体如下
typedef struct sentinelRedisInstance {
int flags; // 标识,记录实例类型
char *name; // 该实例名字
char *runid; // 实例的运行id
uint64_t config_epoch; // 配置纪元,用于实现故障转移
sentinelAddr *addr; // 实例地址
mstime_t last_pub_time; // 上次我们通过 Pub/Sub 发送了 hello 的时间。
mstime_t last_hello_time; // 仅在设置 SRI_SENTINEL 时使用。上次我们发送hello的响应时间
mstime_t last_master_down_reply_time; // SENTINEL is-master-down command.命令的最新响应时间
// ...
mstime_t down_after_period; // 实例无响应多少毫秒之后才会被判断为主观下线
// ...
// Master 配置
unsigned int quorum;// 判断这个实例为客观下线锁需要支持的投票数量
// ...
// Slave 配置
int slave_priority; /* Slave 优先级 */
struct sentinelRedisInstance *master; /* Master instance if it's slave. */
char *slave_master_host; /* Master host as reported by INFO */
int slave_master_port; /* Master port as reported by INFO */
int slave_master_link_status; /* Master link status as reported by INFO */
unsigned long long slave_repl_offset; /* Slave 复制的偏移量. */
// ...
} sentinelRedisInstance;
创建链接
初始化Sentinel的最后一步是创建连向被监控主服务器的网络连接,Sentinel 将成为主服务器的客户端,可以向主服务器发送命令,并且从命令回复中获取相关的信息。
对于每个被Sentinel 监视的主服务器来说,Sentinel会创建两个连接主服务器的异步网络连接:
- 命令连接,这个连接专门用于向主服务器发送命令,并接受命令回复。
- 订阅连接,这个连接专门用于订阅主服务器的
__sentinel__:hello
频道。
为什么会有两个连接?
在Redis目前的发布与订阅功能中,被i发送的信息都不会保存在Redis服务器里面,如果在信息发送时,想要接受信息的客户端不在线或者断线,那么这个客户端就会丢失这条信息。 因此为了不丢失__sentinel__:hello
频道的任何信息,Sentinel必须专门用一个连接来接受该频道的信息。
除了订阅频道之外,Sentinel还必须向主服务器发送命令,以此来与主服务器进行通信,所以Sentinel还必须向主服务器创建命令连接。
获取信息 INFO
Sentinel 默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO命令
,并通过分析 INFO 命令的回复
来获取主服务器的当前信息。
一般INFO命令的回复有以下信息:
- 从服务器的运行ID、角色role、优先级slave_priority、复制偏移量
- 主服务器的IP地址 master_host 以及主服务器的端口号 master_port
- 主从服务器的连接状态master_link_status
获取到这些信息之后,就会更新存储到Sentinel的结构体中。
但是当主服务器处于下线状态,或者Sentinel正在对主服务器和从服务器进行故障转移操作时,Sentinel 向从服务器发送INFO命令的频率将会变成每秒一次。
发送命令
对于监视同一个主服务器和从服务器的多个Sentinel来说,他们会以每两秒一次的频率,通过被监视服务器的 __sentinel:hello__
频道发送消息来响应其他sentinel宣告自己的存在。
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
这条命令向服务器的__sentinel__:hello
频道发送一条信息,这些信息的组成如下:
- s_ 开头的是sentinel本身的信息。
- m_开头的是主服务器的信息。如果此sentinel监视的是主服务器,那么这个参数就是主服务器的参数,如果监视的是从服务器,那么就是这个从服务器正在所复制的主服务器。
接收命令
当sentinel与一个主服务器或者从服务器建立起订阅连接之后,sentinel就会就会通过订阅连接,向服务器发送以下命令:
SUBSCRIBE __sentinel__:hello
也就是说对于每一个sentinel连接的服务器,sentinel既通过命令连接到服务器的__sentinel__:hello
频道发送信息,又通过订阅连接服务器的__sentinel__:hello
频道接受消息。
那么对于监视同一个服务器的多个sentinel来说,一个sentinel发送的信息就会被其他sentinel接受到,因为是监听订阅了同一个服务器的__sentinel__:hello
频道,所sentinel就会感知到其他sentinel的存在。并sentinel将会更新其他的sentinel信息到自己的sentinel字典中。
sentinel 之间的通信
从上面我们知道每个Sentinel也会从__sentinel:hello__
频道中接收其他Sentinel发送来的信息,并根据这些信息为其他Sentinel创建实例结构和命令连接。
但是Sentinel 只会与主服务器和从服务器创建命令连接和订阅连接,Sentinel 和 Sentinel 之间则只创建命令连接。
为什么sentinel与sentinel之间不需要创建订阅连接呢?
首先我们要确定订阅连接是用来干嘛的,订阅连接是用来发现其他节点的,而sentinel已经通过主服务器或者从服务器的频道信息来发现未知的sentinel
,也就是说sentinel订阅了主/从服务器已经知道了其他的sentinel,就不需要再进行订阅连接其他的sentinel了,而相互已知的sentinel只需要使用命令连接来进行通信就够了。
主/客观下线
Sentinel 会以每秒一次的频率向实例,包括主服务器,从服务器,其他Sentinel发送 PING
命令,并根据实例对PING命令
的回复判断实例是否在线,当一个实例在指定的时长中连续向Sentinel发送无效回复时,Sentinel就会判断为主观下线。
Sentinel1 将向 Sentinel2、server、slave1、slave2发送ping命令。sentinel2也会进行同样的操作。那么一般会得到以下两种情况的回复
- 有效回复:返回 PONG、LOADING、MASTERDOWN 三个中的一个
- 无效回复:非有效回复的内容,或者是指定时间内没有返回任何的回复,而这个指定时间的字段为
down-after-milliseconds
。
当Sentinel讲一个主服务器判断为主观下线
,他会向同样监视这个主服务器的其他 Sentinel 进行询问,如果有足够多的结点判定这个主服务器为主观下线
,那么就状态改成客观下线
,某个节点的状态改成客观下线之后,监视这个节点的各个sentinel就会协商选取一个leader sentinel节点
,并且由领头的 leader sentinel 节点发起一次针对主服务器的故障转移。
这个选举的过程在这里就不过多介绍了。有点类似raft。后面有空再说明。
故障转移
在选举出leader sentinel节点之后的故障转移会做以下几件事情:
- 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转化成
主服务器
。 - 让已下线的主服务器属下的所有从服务器改成复制新的主服务器。
- 将已下线主服务器设置为
新的从服务器
新的主服务器是如何挑选出来的呢?首先leader sentinel节点会将已下线主服务器的所有从服务器保存到一个列表,根据一些规则进行过滤:
- 删除已下线或者状态不正常的从服务器,保证列表中剩余的从服务器是
正常
的。 - 删除所有最近5秒内没有回复leader sentinel节点的INFO命令的从服务器,保证列表中的从服务器都是
最新通信成功的
。 - 删除与已下线的主服务器连接断开超过 down-after-milliseconds * 10 毫秒的从服务器,保证剩余的服务器保存的
数据都是比较新
的
down-after-milliseconds:实例失去联系的时间,而删除断开这个时间的10倍,
是为了能保证剩余的从服务器没有过早的与主服务器断开连接。
- 会根据从服务器的优先级进行排序,选择最高优先级的从服务器,如果相同优先级,则选择偏移量最大服务器。因为偏移量大意味着数据最新。
易主
选择完主服务器之后,就开始改变从服务器的复制对象了。这一动作可以通过向服务器发送SLAVEOF
的命令来实现。
本文我们详细介绍了redis集群中sentinel的数据结构,sentinel与主从服务器的连接,信息传递,以及主从服务器发生故障时的处理方式。
那么问题来了?如果sentinel 集群中某一个sentinel节点挂了会发送什么事情呢?