11.redis设计与实现学习笔记-Sentinel(哨兵)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jun8148/article/details/82979023

15. Sentinel

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

在这里插入图片描述

在这里插入图片描述

补充下:在替换了新的主服务器之后,如果之前下线的主服务器上线了,就会被降为新的主服务器的从服务器。

15.1. 启动并初始化Sentinel

命令:

$ redis-sentinel /path/to/your/sentinel.conf
或者
$ redis-sentinel /path/to/your/sentinel.conf --sentinel

这两个命令都能启动Sentinel,效果都是一样的。

Sentinel启动后,会有五个步骤:

15.1.1. 初始化服务器

Sentinel的本质是一个运行在特殊模式下的Redis服务器,因此启动时必须对其进行初始化,但是由于Sentinel与普通的服务器不同,它的初始化需要执行的操作也不同。


下表是Sentinel 模式下Redis服务器的主要功能的使用情况

功能 使用情况
数据库和键值对方面的命令,比如SET、DEL、FLUSHDB 不使用
事务命令,比如MULTIWATCH 不使用
脚本命令,比如EVAL 不使用
RDB持久化命令,比如SAVEBGSAVE 不使用
AOF持久化命令,比如BGREWRITEAOF 不使用
复制命令,比如SLAVEOF Sentinel内部可以使用,但是客户端不能使用
发布和订阅命令,比如PUBLISHSUBSCRIBE SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE四个命令在Sentinel内部和客户端都可以使用,但是PUBLISH命令只能在Sentinel内部使用
文件事件处理器(负责发送命令请求、处理命令回复) Sentinel内部使用,但关联的文件事件处理器和普通Redis服务器不同
时间事件处理器(负责serverCron函数) Sentinel内部使用,时间事件的处理器仍然是serverCron函数,serverCron函数会调用sentinel.c/sentinelTimer函数,后者包含了Sentinel要执行的操作

15.1.2. 使用Sentinel专用代码

启动Sentinel的第二步,就是将普通Redis服务器使用的代码替换成Sentinel专用的代码。

比如 普通Redis服务器使用 redis.h/REDIS_SERVERPORT常量作为服务端口(#define REDIS_SERVERPORT 6379),使用 redis.h/redisCommandTable 作为服务器的命令表。

而Sentinel使用 reids.h/REDIS_SENTINEL_PORT 常量作为服务器端口,默认26379,使用 redis.h/sentinelcmds 作为服务器的命令表且其中的INFO 命令使用Sentinel模式下的专用实现 sentinel.c/sentinelInfoCommand 函数,而不是普通Redis服务器的 redis.h/infoCommand 函数。

在这我把sentinel命令表展示出来吧,普通redis服务器的命令表百度了解吧:

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},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};

15.1.3. 初始化Sentinel状态

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

typedef 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;

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

接下来要做的是将sentinel状态的 masters 属性进行初始化,上面已经说过了,masters 里面保存的是所有被监视的主服务器的信息以及其键值对应保存的是什么内容。

我们先介绍一下 sentinelRedisInstance 结构(简称“实例结构”),这个结构代表着一个被Sentinel监视的Redis服务器实例,可以是主服务器、从服务器或者另外一个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-millseconds 选项设定的值
    // 实例无响应多少毫秒之后才会被判断为主观下线
    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;

其中的 addr 属性是一个指向 sentinel.c/sentinelAddr 结构的指针,这个结构保存实例的IP地址和端口号:

typedef struct sentinelAddr{
    char *p;
    int port;
}sentinelAddr;

对Sentinel 状态的初始化将引发对 masters 字典的初始化,而 masters 字典的初始化是根据被载入的Sentinel配置文件来进行的。

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

这是最后一步啦,这一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息。

每个被Sentinel监视的主服务器,Sentinel会创建两个连向主服务器的异步网络连接:

  • 命令连接,用于向主服务器发送命令,并接收命令回复
  • 订阅连接,用于订阅主服务器的 __sentinel__:hello 频道

15.2. 获取主服务器信息

Sentinel 默认每十秒一次,通过命令连接向被监视的主服务器发送 INFO 命令,并通过分析 INFO 命令回复来获取主服务器当前信息。两方面信息:

  • 关于服务器本身的信息

    包括 run_id 域记录的服务器运行ID,以及 role 域记录的服务器角色

  • 关于主服务器属下的所有从服务器信息

    每个从服务器都由一个“slave”字符串开头的行记录,每行的 ip= 域记录了从服务器的IP地址, port= 域记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。

根据 run_id 域和 role 域的信息,Sentinel将对主服务器的实例结构进行更新。而主服务器返回的从服务器信息,将会被用于更新主服务器实例结构的 slaves 字典(记录了属下从服务器的名单)。

Sentinel 分析 INFO 命令中包含的从服务器信息时,会检查从服务器对应的实例结构是否已经存在于 slaves 字典: 如果存在,就对从服务器的实例结构进行更新,如果不存在(表明这个从服务器是新发现的从服务器),Sentinel会在 slaves 字典中为这个从服务器创建一个新的实例结构。

注:

  • 主服务器实例结构的 flags 值为 SRI_MASTER,从服务器是 SRI_SLAVE
  • 主服务器实例结构的 name 由用户使用Sentinel配置文件设置,从服务器的name 是由Sentinel根据服务器ip+port自动设置的

15.3. 获取从服务器信息

当Sentinel发现主服务器有新的服务器出现时,除了会为这个新从服务器创建相应的实例结构之外,还会创建连接到从服务器的命令连接和订阅连接。

创建了命令连接之后,每10秒一次向从服务器发送 INFO 命令,并根据回复分析以下信息:

  • 从服务器的运行ID run_id
  • 从服务器的角色 role
  • 主服务器的ip地址 master_host 以及主服务器的端口号 master_port
  • 主从服务器的连接状态 master_link_status
  • 从服务器的优先级 slave_priority
  • 从服务器的复制偏移量 slave_repl_offset

根据这些信息,Sentinel会对从服务器的实例结构进行更新。

15.4. 向主服务器和从服务器发送信息

每两秒一次,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:

PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,s_epoch>,
						<m_name>,<m_ip>,<m_port>,<m_epoch>"

这条命令就表示向服务器的 __sentinel__:hello 频道发送一条信息,信息由一下部分组成:

  • s_ 开头的参数记录Sentinel本身的信息
  • m_ 开头的参数则是主服务器的信息,当然如果监视的是从服务器,这个信息表示的就是从服务器的信息

下表是 该命令中的相关参数

参数 意义
s_ip Sentinel的ip地址
s_port Sentinel的端口号
s_runid Sentinel的运行ID
s_epoch Sentinel当前的配置纪元
m_ip 主服务器的ip地址
m_port 主服务器的端口号
m_runid 主服务器的运行ID
m_epoch 主服务器当前的配置纪元

15.5. 接收来自主服务器和从服务器的频道信息

在建立起订阅连接之后,Sentinel会通过这个连接,向服务器发送以下命令:

SUBSCRIBE __sentinel__:hello

Sentinel对 __sentinel__:hello 这个定于会一直持续到Sentinel与服务器的连接断开之后。

可以通过这个频道发送和接收信息。

在这里插入图片描述

对于监视同一服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他的Sentinel接收到,并用于更新其他Sentinel对发送信息Sentinel的认知,和被用于更新其他Sentinel对被监视服务器的认知。

15.5.1. 更新 sentinels 字典

sentinels 字典保存了除Sentinel本身之外,所有同样监视这个主服务器的其他Sentinel资料:

  • 键 是其中一个Sentinel的名字,格式:ip:port
  • 值 是对应Sentinel的实例结构

当一个Sentinel接收到其他Sentinel发来的信息时(称发送信息的Sentinel为源Sentinel,接收信息的Sentinel为目标Sentinel),目标Sentinel会从信息中分析出以下信息:

  • 与Sentinel相关的参数:源Sentinel的IP、port、run_id、配置纪元
  • 与主服务器相关参数:源Sentinel 正在监视的主服务器的名字、IP、port、配置纪元

根据这些主服务器参数,目标Sentinel会在自己的Sentinel状态的 masters 字典中查找相应的主服务器实例结构,然后根据提出的Sentinel参数,检查主服务器实例结构的 sentinels 字典中,源 Sentinel的实例结构是否存在:

  • 存在,就对源Sentinel的实例结构进行更新
  • 不存在,说明源Sentinel是才开始监视主服务器的新Sentinel,目标Sentinel会为源Sentinel创建一个新的实例结构,并将这个结构添加到 sentinels 字典里面

15.5.2. 创建连向其他Sentinel的命令连接

当Sentinel通过频道信息发现了一个新的Sentinel时,它不仅会为新的Sentinel在 sentinels 字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接。新的Sentinel同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:SentinelA有连向SentinelB的命令连接,SentinelB也有连向SentinelA的命令连接。

在这里插入图片描述

注:Sentinel之间不会创建订阅连接

15.6. 检测主观下线状态

默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。

两种实例对 PING 指令的回复情况:

  • 有效回复:实例返回 +PONG-LOADING-MASTERDOWN 三种其中一种
  • 无效回复,除了上面三种之外的其它回复,或者在指定时限内没有返回任何回复

Sentinel配置文件中的 down-after-millseconds 选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在 down-after-millseconds 毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的 flags 属性中打开 SRI_S_DOWN 标识,用于表示这个实例已经进入主观下线状态。

注:

1. 主观下线时长选项,即 down-after-down 的值,不仅会被Sentinel用于判断主服务器的主观下线状态,还会被用于判断主服务器属下的所有从服务器,以及所有同样监视这个主服务器的其他Sentinel的主观下线状态。

2. 多个Sentinel设置的主观下线时长可能不同,对于监视同一个主服务器的多个Sentinel来说,这些Sentinel设置的 down-after-milliseconds 选项的值可能不同,因此,当一个Sentinel将主服务器判断为主观下线时,其它Sentinel可能任然会认为主服务器处于在线状态。

15.7. 检测客观下线状态

当Sentinel将一个主服务器判断为主观下线之后,为确定这个服务器是否真的下线,它会向同样监视这个主服务器的其它Sentinel进行询问,当接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器进行故障转移操作。

15.7.1. 发送 SENTINEL is-master-down-by-addr 命令

Sentinel会发送下面的命令询问其它Sentinel是否同意主服务器下线:

SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

下表是参数的意义:

参数 意义
ip 被Sentinel判断 为主观下线的主服务器ip
port 被Sentinel判断 为主观下线的主服务器端口号
current_epoch Sentinel当前的配置纪元,用于选举领头Sentinel
runid 可以是 *符号或者Sentinel的运行ID,* 表示命令仅仅用于检测主服务器的客观下线状态,而Sentinel的运行ID则用于选举领头Sentinel

15.7.2. 接收 SENTINEL is-master-down-by-addr 命令

当一个Sentinel(目标Sentinel)接收到另外一个Sentinel(源Sentinel)发来的 SENTINEL is-master-by-addr 命令时,目标Sentinel会分析并取出命令请求中包含的各个参数,并根据其中的IP和port,判断主服务器是否已经下线,然后向源Sentinel返回一个包含三个参数的 Multi Bulk 回复作为这个命令的回复。

这三个参数分别是:

  1. <down_state>

    返回目标Sentinel对主服务器的检查结果,1表示主服务器已下线,0表示主服务器未下线

  2. <leader_runid>

    可以是 * 符号或者目标Sentinel的局部领头Sentinel的运行ID,*表示命令仅仅用于检测主服务器的下线状态,而局部领头Sentinel的运行ID则用于选举领头Sentinel

  3. <leader_epoch

    目标Sentinel的局部领头Sentinel的配置纪元,用于选举领头Sentinel。仅在 leader_runid 值不为 * 时有效,如果其值为 * ,这个参数总为0

15.7.3. 接收 SENTINEL is-master-down-by-addr 命令的回复

根据其他Sentinel发回的 SENTINEL is-master-down-by-addr 回复,Sentinel将统计同意主服务器下线的数量,当这个值达到配置指定的判断客观下线所需的数量时(即 quorum 属性的值),Sentinel会将主服务器实例结构中 flags 属性的 SRI_O_DOWN 标识打开,标识主服务器已经进入客观下线状态。

15.8. 选举领头Sentinel

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器进行故障转移操作。

至于如何选举领头Sentinel的规则和方法:

  • 每个Sentinel都有成为领头的机会,无论选举成功与否,其配置纪元都会进行自增。
  • 局部领头:当一个Sentinel A向另一个Sentinel B发送请求 SENTINEL is-master-down-by-addr + SentinelA 的runid ,这就代表A想成为B的局部领头
  • 这种局部领头规则就是先到先得,最早向目标Sentinel发送这个命令的比如成为目标Sentinel的局部领头Sentinel,后面接收到的所有设置要求都会被目标Sentinel拒绝,当它的票数超过半数时,它就会成为领头Sentinel,然后对下线的主服务器执行故障转移操作。

15.9. 故障转移

在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器进行故障转移操作:

  1. 在已下线的主服务器属下的所有从服务器中,挑选一个从服务器作为主服务器

    挑选一个状态良好、数据完整的从服务器,然后发送 SLAVEOF no one 命令,然后将这个从服务器转换成主服务器

  2. 让已下线的主服务器的所有从服务器改为复制新的主服务器

  3. 将已下线主服务器设置为新的主服务器的从服务器,这个旧的主服务器重新上线时,就会成为新的主服务器的从服务器。

猜你喜欢

转载自blog.csdn.net/jun8148/article/details/82979023