Redis使用及源码剖析-20.Redis哨兵(Sentinel )-2021-2-4

2022-02-22 13:47:22 浏览数 (1)

文章目录

  • 前言
  • 一、Redis哨兵简介
  • 二、初始化哨兵节点
    • 1.初始化服务器
    • 2.使用哨兵专用代码
    • 3.初始化哨兵状态
    • 4.初始化 Sentinel 状态的 masters 属性
    • 5.创建连向主服务器的网络连接
  • 三、获取主服务器信息
  • 四、获取从服务器信息
  • 五、向主从服务器发送信息
  • 六、从主从服务器接收信息
    • 1.更新sentinels词典
    • 2.创建连向其他哨兵的命令连接
  • 七、检测主观下线状态
  • 八、检查客观下线状态
  • 九、选举领头哨兵
  • 十、故障转移

前言

哨兵(Sentinel )是Redis高可用性的解决方案,本文将对其进行简单介绍

一、Redis哨兵简介

Redis哨兵系统由一个或者多个哨兵实例组成,它可以监视任意多个主服务器以及主服务器对应的从服务器,当主服务器下线后,哨兵系统会执行故障转移操作,挑选一个从服务器作为新的主服务器,并在下线服务器上线后将其作为新主服务器的从服务器。示意图如下:

二、初始化哨兵节点

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

代码语言:javascript复制
$ redis-server /path/to/your/sentinel.conf --sentinel

执行的步骤如下: 1.初始化服务器。 2.将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。 3.初始化 Sentinel 状态。 4.根据给定的配置文件, 初始化 Sentinel 的监视主服务器列表。 5.创建连向主服务器的网络连接。 下面分别进行介绍

1.初始化服务器

首先, 因为 哨兵本质上只是一个运行在特殊模式下的 Redis 服务器, 所以启动 Sentinel 的第一步, 就是初始化一个普通的 Redis 服务器。不过, 因为 哨兵执行的工作和普通 Redis 服务器执行的工作不同, 所以 哨兵的初始化过程和普通 Redis 服务器的初始化过程并不完全相同。比如说, 普通服务器在初始化时会通过载入 RDB 文件或者 AOF 文件来还原数据库状态, 但是因为 Sentinel 并不使用数据库, 所以初始化 哨兵时就不会载入 RDB 文件或者 AOF 文件。

2.使用哨兵专用代码

启动 Sentinel 的第二个步骤就是将一部分普通 Redis 服务器使用的代码替换成 Sentinel 专用代码。比如普通 Redis 服务器使用6379常量的值作为服务器端口,而 Sentinel 则使用 26379常量的值作为服务器端口。除此之外, 普通 Redis 服务器使用 redis.c/redisCommandTable 作为服务器的命令表,而 Sentinel 则使用 sentinel.c/sentinelcmds 作为服务器的命令表,两者可执行的命令并不相同。两个命令表如下所示:

代码语言:javascript复制
struct redisCommand redisCommandTable[] = {
    {"get",getCommand,2,"r",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,noPreloadGetKeys,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wm",0,noPreloadGetKeys,1,1,1,0,0},
    // ...
    {"script",scriptCommand,-2,"ras",0,NULL,0,0,0,0,0},
    {"time",timeCommand,1,"rR",0,NULL,0,0,0,0,0},
    {"bitop",bitopCommand,-4,"wm",0,NULL,2,-1,1,0,0},
    {"bitcount",bitcountCommand,-2,"r",0,NULL,1,1,1,0,0}
}

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},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0}
};

3.初始化哨兵状态

在应用了 Sentinel 的专用代码之后, 接下来, 服务器会初始化一个 sentinel.c/sentinelState 结构(后面简称“Sentinel 状态”), 这个结构保存了服务器中所有和 Sentinel 功能有关的状态 (服务器的一般状态仍然由 redis.h/redisServer 结构保存):

代码语言:javascript复制
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;

4.初始化 Sentinel 状态的 masters 属性

Sentinel 状态中的 masters 字典记录了所有被 Sentinel 监视的主服务器的相关信息, 其中:字典的键是被监视主服务器的名字。而字典的值则是被监视主服务器对应的 sentinel.c/sentinelRedisInstance 结构。每个 sentinelRedisInstance 结构(后面简称“实例结构”)代表一个被 Sentinel 监视的 Redis 服务器实例(instance), 这个实例可以是主服务器、从服务器、或者另外一个 Sentinel 。sentinelRedisInstance 结构如下所示:

代码语言:javascript复制
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 <master-name> <IP> <port> <quorum> 选项中的 quorum 参数
    // 判断这个实例为客观下线(objectively down)所需的支持投票数量
    int quorum;

    // SENTINEL parallel-syncs <master-name> <number> 选项的值
    // 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs;

    // SENTINEL failover-timeout <master-name> <ms> 选项的值
    // 刷新故障迁移状态的最大时限
    mstime_t failover_timeout;

    // ...

} sentinelRedisInstance;
typedef struct sentinelAddr {

    char *ip;

    int port;

} sentinelAddr;

masters 字典的初始化是根据被载入的 Sentinel 配置文件来进行的,举个例子, 如果用户在启动 Sentinel 时, 指定了包含以下内容的配置文件:

代码语言:javascript复制
#####################
# 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

那么初始化的masters词典如下所示:

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

初始化 Sentinel 的最后一步是创建连向被监视主服务器的网络连接: Sentinel 将成为主服务器的客户端, 它可以向主服务器发送命令, 并从命令回复中获取相关的信息。对于每个被 Sentinel 监视的主服务器来说, Sentinel 会创建两个连向主服务器的异步网络连接:一个是命令连接, 这个连接专门用于向主服务器发送命令, 并接收命令回复。另一个是订阅连接, 这个连接专门用于订阅主服务器的 sentinel:hello 频道。创建连接的示意图如下所示:

三、获取主服务器信息

哨兵默认会以每10s一次的频率向被监视的主服务器发送info命令,主服务器会对命令进行回复,回复内容包括主服务器运行id以及主服务器下属的从服务器信息,如下所示:

哨兵分析回复内容,来更新masters词典中的主服务器的信息,如运行id,还会将获取的从服务器信息保存在主服务器对应的sentinelRedisInstance结构中,示意图如下所示:

四、获取从服务器信息

哨兵发现一个新的从服务器时,除了创建对应的实例结构以外,还会创建到从服务器的命令连接和订阅连接。示意图如下所示:

创建连接以后,哨兵也会每10s一次的频率向从服务器发送info命令,获得对应的信息来更新如下所示的从服务器结构:

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

哨兵会以每2秒一次的频率,通过命令连接向主从服务器发送信息,发送的信息为:向__sentinel__:hello频道发布消息,消息内容包括哨兵的ip、端口、运行id、主服务器的名称、ip、端口等信息,向从服务器发送的是它所属的主服务器的名称、ip、端口。

六、从主从服务器接收信息

当哨兵和主从服务器建立起订阅连接以后,就会通过订阅连接,向服务器发送订阅__sentinel__:hello频道的命令。这样主从服务器运行期间__sentinel__:hello频道收到一个哨兵发布的消息以后,就会推送给监视同一个服务器的其他哨兵。

1.更新sentinels词典

哨兵为主服务器创建的实例结构中的sentinels词典中保存了监视该服务器的所有哨兵节点信息,如下所示:

代码语言:javascript复制
typedef struct sentinelRedisInstance {
	// 其他同样监控这个主服务器的所有 sentinel
    dict *sentinels;    /* Other sentinels monitoring the same master. */
};

sentinels词典键是哨兵节点的ip:port,值是哨兵对应的sentinelRedisInstance 结构。这样当哨兵收到来自其他哨兵的消息以后,就会将消息中的主服务器信息和哨兵信息提取出来,在masters词典中查找主服务器对应的sentinelRedisInstance 结构,再根据消息的哨兵信息更新该结构的sentinels词典内容。如下所示:

2.创建连向其他哨兵的命令连接

当哨兵通过订阅频道获取到其他哨兵的信息时,还会根据获取的消息创建指向其他哨兵节点的命令连接。如下所示:

七、检测主观下线状态

哨兵会在每秒一次的频率向其他哨兵、监视的主从服务器发送ping命令,若在一段连续时间内均未收到有效回复,那么哨兵会修改该实例对应的实例结构,在flags属性中打开SRI_S_DOWN标志,表示认为这个实例已经进入了主观下线状态。

八、检查客观下线状态

当一个哨兵节点判断某个主服务器是主观下线以后,就会向监视这个主服务器的其他哨兵发送命令询问看它们是否认为该主服务器已经客观下线。收到其他哨兵的回复以后,若认为主观下线的数目达到判断客观下线的数量时,则将该主服务器实例结构flags属性的SRI_O_DOWN打开,表示服务器进入了客观下线状态。

九、选举领头哨兵

当一个主服务器被判断为主观下线时,监视这个主服务器的所有哨兵节点就会通过“投票”的方式,选出一个头部哨兵,由它负责故障转移操作。投票规则比较复杂,在此不多做介绍。

十、故障转移

当哨兵节点进行故障转移操作时会从所有的从服务器中选出在线、优先级最高、复制偏移量最大的从服务器作为新的主服务器,并通过slaveof命令修改其他从服务器的复制目标,使得它们从新的主服务器复制数据。并且还会修改旧的主服务器的实例结构,让它变为从服务器,这样旧的主服务器重新上线时就会变为从服务器工作。

0 人点赞