redis 高可用 Sentinel 详解

写在前面

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 的结构体如下

c 复制代码
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会创建两个连接主服务器的异步网络连接:

  1. 命令连接,这个连接专门用于向主服务器发送命令,并接受命令回复。
  2. 订阅连接,这个连接专门用于订阅主服务器的 __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宣告自己的存在。

c 复制代码
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就会就会通过订阅连接,向服务器发送以下命令:

c 复制代码
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节点之后的故障转移会做以下几件事情:

  1. 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转化成主服务器
  2. 让已下线的主服务器属下的所有从服务器改成复制新的主服务器。
  3. 将已下线主服务器设置为新的从服务器

新的主服务器是如何挑选出来的呢?首先leader sentinel节点会将已下线主服务器的所有从服务器保存到一个列表,根据一些规则进行过滤:

  1. 删除已下线或者状态不正常的从服务器,保证列表中剩余的从服务器是正常的。
  2. 删除所有最近5秒内没有回复leader sentinel节点的INFO命令的从服务器,保证列表中的从服务器都是最新通信成功的
  3. 删除与已下线的主服务器连接断开超过 down-after-milliseconds * 10 毫秒的从服务器 ,保证剩余的服务器保存的数据都是比较新

down-after-milliseconds:实例失去联系的时间,而删除断开这个时间的10倍,是为了能保证剩余的从服务器没有过早的与主服务器断开连接。

  1. 会根据从服务器的优先级进行排序,选择最高优先级的从服务器,如果相同优先级,则选择偏移量最大服务器。因为偏移量大意味着数据最新。

易主

选择完主服务器之后,就开始改变从服务器的复制对象了。这一动作可以通过向服务器发送SLAVEOF的命令来实现。

本文我们详细介绍了redis集群中sentinel的数据结构,sentinel与主从服务器的连接,信息传递,以及主从服务器发生故障时的处理方式。

那么问题来了?如果sentinel 集群中某一个sentinel节点挂了会发送什么事情呢?

相关推荐
数据智能老司机1 小时前
CockroachDB权威指南——SQL调优
数据库·分布式·架构
数据智能老司机1 小时前
CockroachDB权威指南——应用设计与实现
数据库·分布式·架构
数据智能老司机1 小时前
CockroachDB权威指南——CockroachDB 模式设计
数据库·分布式·架构
数据智能老司机20 小时前
CockroachDB权威指南——CockroachDB SQL
数据库·分布式·架构
数据智能老司机21 小时前
CockroachDB权威指南——开始使用
数据库·分布式·架构
松果猿21 小时前
空间数据库学习(二)—— PostgreSQL数据库的备份转储和导入恢复
数据库
Kagol21 小时前
macOS 和 Windows 操作系统下如何安装和启动 MySQL / Redis 数据库
redis·后端·mysql
无名之逆21 小时前
Rust 开发提效神器:lombok-macros 宏库
服务器·开发语言·前端·数据库·后端·python·rust
s91236010121 小时前
rust 同时处理多个异步任务
java·数据库·rust
数据智能老司机21 小时前
CockroachDB权威指南——CockroachDB 架构
数据库·分布式·架构