Redis设计与实现-哨兵

哨兵模式

如有侵权,请联系~

如有错误,也欢迎批评指正~

本篇文章大部分是来自学习《Redis设计与实现》的笔记

哨兵(Sentinel)是Redis的高可用解决方案:由一个或者多个哨兵实例组成的哨兵系统,可以监视任意多个主服务器以及这些主服务器所属下的所有从服务器,并且在被监视的主服务器下线时自动的将从某个服务器升级为新的主服务器,然后将新的主服务器替换已经下线的主服务器继续处理命令请求。

1、启动并初始化sentinel

启动一个Sentinel可以使用如下命令:

c 复制代码
redis-sentinel /path/your/sentinel.conf
redis-server /path/your/sentinel.conf --sentinel

启动一个sentinel服务器,需要执行如下步骤:

  1. 初始化服务器
  2. 将普通的Redis服务器使用的代码代替为Sentinel专用代码
  3. 初始化Sentinel状态
  4. 根据给定的配置文件,初始化Sentinel的监视主服务器列表
  5. 创建与主服务器的网络连接

1.1 初始化服务器

因为Sentinel哨兵实例本质上就是一个运行在特殊模式下的Redis服务器,所以这一步初始化服务器就是初始化一个redis实例,参考Redis设计与实现-底层实现

但是毕竟两者工作的方式不一样,所以初始化过程还是有所不一样,例如哨兵初始化是不需要RDB文件或者AOF文件还原数据库。

1.2 使用Sentinel代码

将普通redis使用的代码替换为sentinel哨兵使用的代码。例如,redis的服务端端口号为6379,哨兵使用26379.

c 复制代码
define REDIS_SERVERPORT 6379
define REDIS_SENTINEL_PORT 26379

哨兵的命令表也和redis服务器的不同,哨兵对客户端提供的命令PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE,不对外提供set get等redis的基础命令。

1.3 初始化sentinel状态

接下来会初始化哨兵的状态sentinelState结构,SentinelState 是 Redis Sentinel 内部用来表示全局状态的数据结构。它包含了所有监控的主节点、从节点、哨兵实例以及配置信息。

c 复制代码
typedef struct sentinelState {
    uint64_t current_epoch;       // 当前的纪元(epoch),用于故障转移选举
    dict *masters;                // 主节点字典,key 为被监控主节点名称,value 为 sentinelRedisInstance
    int tilt;                     // 是否处于倾斜模式(Tilt Mode)
    int running_scripts;          // 当前正在运行的脚本数量
    mstime_t tilt_start_time;     // 倾斜模式开始的时间
    mstime_t previous_time;       // 上一次时间戳,用于检测时间跳跃
    list *scripts_queue;          // 脚本队列,用于执行用户定义的脚本
} sentinelState;

1.4 初始化sentinel状态的master属性

sentinel状态会保存所有监控的主服务器的信息master,而这个master的key就是主服务器的名字,value就是sentinelRedisInstance。这一步就是初始化master字典。为什么只保存主服务器信息?因为从服务器是动态的,sentinel会通过主服务器进行获取。

c 复制代码
typedef struct sentinelRedisInstance {
    int flags;                    // 实例标志位(如主节点、从节点、哨兵等)
    char *name;                   // 实例名称(仅主节点有名称)
    redisAsyncContext *cc;        // 与该实例的命令连接
    redisAsyncContext *pc;        // 与该实例的发布/订阅连接
    mstime_t last_pub_time;       // 上一次发送 PUBLISH 消息的时间
    mstime_t last_hello_time;     // 上一次收到 HELLO 消息的时间
    mstime_t last_master_down_time; // 上一次检测到主节点下线的时间
    mstime_t down_after_period;   // 判定实例下线的时间阈值
    mstime_t info_refresh;        // INFO 命令刷新的时间间隔
    dict *renamed_commands;       // 重命名的 Redis 命令映射
    int parallel_syncs;           // 故障转移后允许同时同步的从节点数量
    int quorum;                   // 判定主节点不可用所需的最小哨兵数量
    list *sentinels;              // 监控该主节点的所有哨兵实例
    list *slaves;                 // 主节点的所有从节点
    struct sentinelRedisInstance *master; // 如果是从节点,指向其主节点
    ...
} sentinelRedisInstance;

1.5 创建连向主服务器的网络连接

哨兵sentinel会向主服务器建立网络连接,针对于一个主服务器需要建立两个网络连接:

  • 用于命令回复的网络连接,主要用来向主服务器发送命令请求,接受命令回复
  • 用于订阅功能的网络连接,用来订阅主服务器的_sentinel_:hello频道

2、获取主服务器信息

sentinel哨兵会每10秒向监控的主服务器发送INFO命令,并通过主服务器返回的INFO命令回复进行分析更新相应主服务器的sentinelRedisInstance。INFO命令主服务器返回的结果如下:

c 复制代码
# server部分
redis_version:6.2.6                  # Redis 版本号
redis_mode:standalone                # Redis 模式 (standalone 或 cluster)
os:Linux 5.4.0-42-generic x86_64     # 操作系统信息
arch_bits:64                         # 架构位数 (32 或 64)
process_id:12345                     # Redis 进程 ID
run_id:abcdef1234567890abcdef1234567890abcdef1234567890abcdef12 # 运行 ID
tcp_port:6379                        # Redis 服务端口
uptime_in_seconds:123456             # Redis 运行时间(秒)
uptime_in_days:1                     # Redis 运行时间(天)
hz:10                                # Redis 内部定时器频率

# client部分
connected_clients:10                 # 当前连接的客户端数量
client_recent_max_input_buffer:2     # 最大输入缓冲区大小
client_recent_max_output_buffer:0    # 最大输出缓冲区大小
blocked_clients:0                    # 被阻塞的客户端数量

# replication部分
role:master                          # 角色:主节点
connected_slaves:2                   # 已连接的从节点数量
slave0:ip=192.168.1.2,port=6379,state=online,offset=12345678,lag=0
slave1:ip=192.168.1.3,port=6379,state=online,offset=12345678,lag=1
master_replid:abcdef1234567890abcdef1234567890abcdef1234567890abcdef12
master_replid2:00000000000000000000000000000000000000000000000000000000
master_repl_offset:12345678          # 主节点的复制偏移量
second_repl_offset:-1                # 第二个复制 ID 的偏移量
repl_backlog_active:1                # 是否启用了复制积压缓冲区
repl_backlog_size:1048576            # 复制积压缓冲区大小
repl_backlog_first_byte_offset:12345 # 积压缓冲区的第一个字节偏移量
repl_backlog_histlen:12345           # 积压缓冲区的历史长度

....

通过分析上述命令回复会更新对应主服务器的sentinelRedisInstance,这个命令回复也会返回从服务器的信息。如果有新增的从服务器就会为从服务器创建新的sentinelRedisInstance,添加到主服务的sentinelRedisInstance.slaves属性中。

3、获取从服务器的信息

当有新的从服务器出现的时候,sentinel会在该主服务下的slaves中添加一个sentinelRedisInstance实例,并且和从服务器创建命令连接和订阅连接。

命令创建完成之后,sentinel默认10秒通过命令连接向从服务器发一次info命令,并且返回:

c 复制代码
# Replication
role:slave                   # 角色为从节点
master_host:127.0.0.1        # 主节点IP
master_port:6379             # 主节点端口
master_link_status:up        # 与主节点的连接状态(up/down)
master_last_io_seconds_ago:1 # 上次与主节点通信的秒数(反映复制延迟)
master_sync_in_progress:0    # 是否正在全量同步(0=否,1=是)
slave_repl_offset:123456     # 当前复制偏移量(用于评估数据同步进度)
slave_priority:100           # 从节点优先级(故障转移时影响选举)
slave_read_only:1            # 是否只读模式(默认1)
# Server
redis_version:6.2.6         # Redis版本
os:Linux 5.4.0-80-generic   # 运行的操作系统
uptime_in_seconds:86400     # 运行时间(秒)
# Clients
connected_clients:3         # 当前客户端连接数

根据从服务器返回的数据,更新sentinelRedisInstance实例。

4、向主从服务器发送信息

sentinel每两秒就会通过订阅连接向主从服务器发送信息。当 Sentinel 在 sentinel:hello 频道上发布消息时,消息内容通常包含以下信息:

c 复制代码
publish __sentinel__:hello <sentinel-ip>,<sentinel-port>,<runid>,<current_epoch>,<master-name>,<master-ip>,<master-port>,<master-config-epoch>
  • sentinel-ip 和 sentinel-port:当前 Sentinel 的地址。
  • runid:当前 Sentinel 的运行 ID。
  • current_epoch:当前的配置纪元(用于协调故障转移)。
  • master-name:被监控的主节点名称。
  • master-ip 和 master-port:主节点的地址。
  • master-config-epoch:主节点的配置纪元。

5、接受主从服务器的频道信息

有关频道的订阅关系,针对于一个主从服务Redis集群会分配单独的订阅频道,即不同redis主服务器之间的频道是相互独立的。针对于同一个频道,不仅所有的哨兵和redis主从服务器都订阅这个频道,而且他们也都可以向这个频道发布消息。

所以通过这个频道 redis服务器可以向监听自己的所有sentinel发送消息。

通过上图可以知道,一个哨兵sentinel发送消息之后除了redis服务器可以接收到数据之外,其他监听这个redis主服务的sentinel也会收到消息。

一个sentinel发送消息之后,sentinel的处理:

  • 如果发现接收到的sentinel runId和自己的一样,说明是自己发送的,则丢弃
  • 如果发现不一样,则根据发送的消息解析出sentinel和主服务器的信息。通过主服务器的信息在自己的sentinelServer的master字典中找到这个主服务器的sentinelRedisInstance实例,然后查看该实例下sentinels【监听这个主服务器的其他sentinel】是否存在。如果存在则更新数据;否则创建实例,并且与这个sentinel彼此创建命令连接。

sentinel和redis服务器的命令连接和订阅关系。【本图没有画各个sentinel与redis服务器的命令连接】(sentinel之间只有命令连接,没有订阅)

6、检测主观下线状态

默认情况下,sentinel会每秒都会向与其建立命令连接的所有实例【主服务器、从服务器以及与其相连的sentinel】发送ping命令,并根据实例返回的命令回复是否有限【有效回复+PONG、- LOADING、- MASTERDOWN】进行判断是否在线。

sentinel会根据配置文件中down-after-milliseconds参数判断该实例是否为主观下线。即一个实例连续down-after-milliseconds毫秒内一直无效回复,则标记为主观下线,在该实例的sentinelRedisInstance的flags属性设置为sri_s_down标识。

注意每个sentinel配置文件中的down-after-milliseconds值可能不同,有的可能为1000,有的可能为5000。所以,一个实例下线并不一定所有的sentinel都同时认为下线。

当一个 Sentinel 实例判断另一个 Sentinel 实例为主观下线后,它会执行以下操作:

  • 记录主观下线状态:当前 Sentinel 会将目标 Sentinel 标记为 主观下线,并记录其状态。
    这一状态仅存在于当前 Sentinel 的视角中,并不会立即影响系统的整体行为。
  • 通知其他 Sentinel 实例:通知其他 Sentinel 实例自己对目标 Sentinel 的判断结果。其他 Sentinel 实例可以接收到这些消息,但它们会根据自己的健康检查结果独立判断目标 Sentinel 是否也处于主观下线状态。
  • 不触发客观下线

7、检查客观下线状态

当sentinel判断一个主服务器为主观下线之后,为了进一步判断是否真的下线,会问其他sentinel哨兵是否下线。当超过quorum个sentinel认为该主服务器下线,则就会将这个redis主服务器标记为客观下线。【从服务器不需要进行判断客观下线,影响比较小】

SENTINEL IS-MASTER-DOWN-BY-ADDR 是 Redis Sentinel 提供的一个命令,用于检查某个主节点是否被标记为下线(主观下线或客观下线),并参与领导者选举的协商过程。sentinel通过命令连接进行发送的,不是通过hello频道发送的。

SENTINEL IS-MASTER-DOWN-BY-ADDR命令:

c 复制代码
SENTINEL IS-MASTER-DOWN-BY-ADDR <master-ip> <master-port> <current-epoch> <runid>

参数说明:

  • master-ip和 master-port:指定要检查的主节点的 IP 地址和端口号。
  • current-epoch:当前的配置纪元(configuration epoch)。配置纪元是一个递增的数字,用于协调多个 Sentinel 实例之间的状态变更和领导者选举。
  • runid:如果该参数为 *,表示当前 Sentinel 只是询问其他 Sentinel 是否认为主节点已下线。
    如果该参数为具体的运行 ID(如当前 Sentinel 的 runid),表示当前 Sentinel 希望竞选成为领导者。

目标 Sentinel 返回包含三个参数的响应:

  • down_state:
    • 1 表示目标 Sentinel 认为主节点已下线;
    • 0 表示认为主节点在线。
  • leader_runid:
    • 若为 *,表示回复仅用于主节点状态检测;
    • 若为具体 runid,表示目标 Sentinel 同意该 runid 对应的 Sentinel 成为故障转移的领导者。
  • leader_epoch: 目标 Sentinel 的局部领头配置纪元(仅当 leader_runid 不为 * 时有效)

sentinel会根据该命令的回复判断,是否超过quorum个sentinel回复了一下线。客观下线则会将该redis主服务器实例的sentinelRedisInstance的flags属性设置为sri_o_down标识。

同样,不同的sentinel可能配置文件不一样,从而会出现quorum大小不一样,可能出现有的sentinel认为主服务器已经下线,有的认为没有客观下线。

8、选择领头的sentinel

只要判断主机已经客观下线之后,监听这个主机的各个sentinel进行协商,选择一个领头的sentinel,由这个领头的sentinel负责故障转移。

  • 源哨兵期望成为领头的sentinel,所以源sentinel向目标sentinel发送sentinel is-master-down-by-addr命令,并且runid为源sentinel的runid。
  • 目标sentinel会根据先到先得,如果没有在这个记元投递给其他sentinel,就会投递给这个sentinel,回复的leader_runid和leader_epoch分别为源sentinel的runid和目标sentinel的epoch。否则就会拒绝
  • 源sentinel接收到命令回复之后,会判断epoch和自己的epoch是否一样以及回复的runid是否等于自己的epoch。
  • 如果某个sentinel超过半数以上,则会成为领头sentinel执行故障转移。如果都没有满足这一条件的,则过一段时间再进行选举。每次选举epoch都会加1,无论是否成功。

9、故障转移

在选择了领头sentinel之后,由领头sentinel负责故障转移。进行故障转移主要步骤为:从那些可用的从服务器中选择主服务器、将其他从服务器复制新的主服务器、将已经下线的主服务器也作为新主服务器的从服务器。

9.1 选择新主服务器

领头sentinel从所有从服务器中选择状态良好、数据完整的从服务器作为新主服务器,并向这个从服务器发送slave no one命令,将这服务器设置为主服务器。选择主服务的步骤大致如下:

  1. 从所有的从服务器中删除处于下线的服务器
  2. 从所有的从服务器中删除5秒内没有回复info命令的服务器
  3. 然后从剩余的从服务器根据【 优先级 > 复制偏移量大 > 运行runId最小】的原则选择作为从服务器作为主服务器

9.2 修改从服务器复制目标

将其他从服务器复制新的主服务器,领头sentinel会向其他的从服务器发送slave <新主服务器ip> <新主服务器port>。

9.3 将旧的主服务器转换为从服务器

接下来就是将旧的主服务器转换为从服务器,当旧的主服务器重新启动之后,sentinel会向它发送slave <新主服务器ip> <新主服务器port>命令。

相关推荐
石去皿6 分钟前
解决 LRU 缓存中的“堆使用后释放”问题
缓存
小Tomkk39 分钟前
mysql 最长连续登录天数解析
数据库·mysql
快来卷java1 小时前
深入剖析雪花算法:分布式ID生成的核心方案
java·数据库·redis·分布式·算法·缓存·dreamweaver
tpoog1 小时前
[MySQL]数据类型
android·开发语言·数据库·mysql·算法·adb·贪心算法
云上艺旅1 小时前
K8S学习之基础六十四:helm常用命令
学习·云原生·容器·kubernetes
时光追逐者1 小时前
学习如何设计大规模系统,为系统设计面试做准备!
学习·面试·职场和发展·系统设计
爱吃喵的鲤鱼2 小时前
MySQL增删改查(CRUD)操作详解与实战指南
数据库·mysql
淬渊阁2 小时前
汇编学习之《指针寄存器&大小端学习》
汇编·学习
淬渊阁2 小时前
汇编学习之《段寄存器》
汇编·学习
虾球xz2 小时前
游戏引擎学习第193天
c++·学习·游戏引擎