Redis哨兵-Sentinel
- 概述
- 1.启动并初始化Sentinel
- 1.1 初始化服务器
- 1.2 使用Sentinel专用代码
- 1.3 初始化Sentinel状态
- 1.4 初始化 Sentinel状态 的`masters`属性
- 1.5 创建连向主服务器的网络连接
- 为什么有两个连接?
- 2.获取主服务器信息
- 3.获取从服务器信息
- 4.向主服务器和从服务器发送信息
- 5.接收来自主服务器和从服务器的频道信息
- 5.1 更新sentinels字典
- 5.2 创建 连向 其他Sentinel 的 命令连接
- Sentinel之间 是不会创建 订阅连接
- 6.检测主观下线状态
- 主观下线时长选项的作用范围
- 多个Sentinel设置的主观下线时长可能不同
- 7.检测客观下线状态
- 7.1 发送`SENTINEL is-master-down-by-addr`命令
- 7.2 接收`SENTINEL is-master-down-by-addr`命令
- 7.3 接收`SENTINEL is-master-down-by-addr`命令的回复
- 客观下线状态的判断条件
- 不同Sentinel判断客观下线的条件可能不同
- 8.选举领头Sentinel(重要!!!)
- 9.故障转移(重要!!!)
- 9.1 选出新的主服务器
- 9.2 修改从服务器的复制目标
- 9.3 将旧的主服务器变为从服务器
概述
Sentinel(哨岗、哨兵)是Redis的高可用性(high availability)解决方案
由 一个或多个Sentinel实例(instance) 组成的 Sentinel系统(system)可以监视 任意多个 主服务器,以及 这些主服务器属下的 所有从服务器,并在 被监视的主服务器 进入下线状态时,自动将 下线主服务器 属下的 某个从服务器 升级为 新的主服务器,然后 由新的主服务器 代替 已下线的主服务器 继续处理 命令请求
这里补三张图
1.启动并初始化Sentinel
启动一个Sentinel可以使用命令:
$ redis-sentinel /path/to/your/sentinel.conf
或者命令:
$ redis-server /path/to/your/sentinel.conf --sentinel
这两个命令的效果完全相同
当一个Sentinel启动时,它需要执行以下步骤:
- 初始化服务器
- 将普通Redis服务器使用的代码替换成Sentinel专用代码
- 初始化Sentinel状态
- 根据给定的配置文件,初始化Sentinel的监视主服务器列表
- 创建连向主服务器的网络连接。
1.1 初始化服务器
Sentinel本质上只是一个运行在特殊模式下的Redis服务器,所以启动Sentinel的第一步,就是初始化一个普通的Redis服务器
Sentinel执行的工作和普通Redis服务器执行的工作不同,所以Sentinel的初始化过程和普通Redis服务器的初始化过程并不完全相同
普通服务器在初始化时会通过载入RDB文件或者AOF文件来还原数据库状态
但是因为Sentinel并不使用数据库,所以初始化Sentinel时就不会载入RDB文件或者AOF文件
下图展示了Redis服务器在Sentinel模式下运行时,服务器各个主要功能的使用情况
1.2 使用Sentinel专用代码
启动Sentinel的第二个步骤 就是将 一部分普通Redis服务器使用的代码 替换成 Sentinel专用代码
比如说,普通Redis服务器使用redis.h/REDIS_SERVERPORT
常量的值作为服务器端口:
#define REDIS_SERVERPORT 6379
而Sentinel则使用sentinel.c/REDIS_SENTINEL_PORT
常量的值作为服务器端口:
#define REDIS_SENTINEL_PORT 26379
除此之外,普通Redis服务器使用redis.c/redisCommandTable
作为服务器的命令表:
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}
}
而Sentinel则使用sentinel.c/sentinelcmds
作为服务器的命令表
并且其中的INFO
命令会使用Sentinel模式下的专用实现sentinel.c/sentinelInfoCommand
函数,而不是普通Redis服务器使用的实现redis.c/infoCommand
函数
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}
};
sentinelcmds
命令表也解释了为什么在Sentinel模式下,Redis服务器不能执行诸如SET、DBSIZE、EVAL等等这些命令,因为服务器根本没有在命令表中载入这些命令
PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE这七个命令就是客户端可以对Sentinel执行的全部命令了
1.3 初始化Sentinel状态
在应用了Sentinel的专用代码之后,接下来,服务器会初始化一个sentinel.c/sentinelState
结构(简称“Sentinel状态”)
这个结构 保存了 服务器中 所有 和Sentinel功能有关的 状态
服务器的一般状态仍然由redis.h/redisServer
结构保存
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;
1.4 初始化 Sentinel状态 的masters
属性
Sentinel状态(sentinelState) 中的 masters
字典 记录了 所有被Sentinel监视的主服务器 的 相关信息,其中:
- 字典的键 是 被监视主服务器 的 名字
- 字典的值 是 被监视主服务器 对应的
sentinel.c/sentinelRedisInstance
结构
小结:
可以把 Sentinel状态(sentinelState)看作是一个类, masters
字典是类sentinelState的一个字段
这个字段的类型是一个map,key是被 监视的 主服务器的名字,value是这个主服务器的sentinelRedisInstance类型的对象(sentinelRedisInstance也可以看作是一个类)
下面还会提到sentinelRedisInstance类中有一个属性addr,属性addr的类型是sentinelAddr类
每个sentinelRedisInstance
结构(简称“实例结构”)代表一个 被Sentinel监视的 Redis服务器实例(instance)
这个实例可以是主服务器、从服务器,或者另外一个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-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;
sentinelRedisInstance.addr
属性 是一个 指向 sentinel.c/sentinelAddr
结构的指针
这个结构 保存着 实例的IP地址和端口号:
typedef struct sentinelAddr {
char *ip;
int port;
} sentinelAddr;
对 Sentinel状态(sentinelState) 的 初始化 将引发 对 masters
字典 的 初始化
而 masters
字典 的 初始化 是根据 被载入的 Sentinel配置文件 来进行的
#####################
# 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
按照上面这个配置,会是下面的情况
1.5 创建连向主服务器的网络连接
初始化Sentinel的最后一步 是 创建 连向 被监视主服务器 的 网络连接
Sentinel 将成为 主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息
对于每个被Sentinel监视的主服务器来说,Sentinel 会创建 两个 连向主服务器 的 异步网络连接:
- 一个是命令连接,这个连接 专门用于 向主服务器发送命令,并接收命令回复
- 一个是订阅连接,这个连接 专门用于 订阅主服务器的
__sentinel__:hello
频道
为什么有两个连接?
在Redis目前的发布与订阅功能中,被发送的信息 都不会保存在Redis服务器里面
如果在信息发送时,如果 接收信息的客户端 不在线或者断线,那么这个客户端就会丢失这条信息
因此,为了不丢失__sentinel__:hello
频道的任何信息,Sentinel必须专门用一个订阅连接来接收该频道的信息
另一方面,除了订阅频道之外,Sentinel还必须向主服务器发送命令,以此来与主服务器进行通信,所以Sentinel还必须向主服务器 创建 命令连接
因为Sentinel需要与多个实例创建多个网络连接,所以Sentinel使用的是异步连接
2.获取主服务器信息
Sentinel默认会以每十秒一次的频率,通过 命令连接 向 被监视的主服务器发送INFO
命令,并通过分析INFO
命令的回复 来获取 主服务器的当前信息
通过分析主服务器返回的INFO
命令回复,Sentinel可以获取以下两方面的信息:
- 关于主服务器本身的信息,包括
run_id
域 记录的 服务器运行ID,以及role
域 记录的 服务器角色 - 关于主服务器属下 所有从服务器的信息,每个从服务器都由一个"slave"字符串开头的行记录,每行的
ip=
域记录了从服务器的IP地址,而port=
域则记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器
根据run_id
域和role
域记录的信息,Sentinel 将对 主服务器 的 实例结构(sentinelRedisInstance)进行更新
例如,主服务器重启之后,它的 运行ID 就会和 实例结构之前保存的运行ID 不同,Sentinel检测到这一情况之后,就会对实例结构的运行ID进行更新
至于主服务器返回的从服务器信息,则会被用于 更新 主服务器实例结构 的 slaves
字典,这个字典 记录了 主服务器属下从服务器的名单
-
slaves
字典的键 是由 Sentinel 自动设置的 从服务器名字,格式为ip:port,对于IP地址为127.0.0.1,端口号为11111的从服务器来说,Sentinel为它设置的名字就是127.0.0.1:11111 -
slaves
字典的值 是 从服务器对应的实例结构(sentinelRedisInstance):比如说,如果键是127.0.0.1:11111,那么这个键的值就是IP地址为127.0.0.1,端口号为11111 的 从服务器 的 实例结构
Sentinel在分析INFO
命令中 包含的 从服务器信息时,会检查 从服务器 对应的 实例结构 是否已经存在于 slaves
字典
- 如果从服务器对应的实例结构已经存在,那么Sentinel对从服务器的实例结构进行更新
- 如果从服务器对应的实例结构不存在,那么说明这个从服务器是新发现的从服务器,Sentinel会在slaves字典中为这个从服务器新创建一个实例结构
注意对比 图中 主服务器实例结构 和 从服务器实例结构 之间的区别:
- 主服务器实例结构的
flags
属性的值为SRI_MASTER
,而从服务器实例结构的flags
属性的值为SRI_SLAVE
- 主服务器实例结构的
name
属性的值 是 用户 使用Sentinel配置文件设置的,而从服务器实例结构的name
属性的值 则是 Sentinel根据 从服务器的IP地址和端口号自动设置的
3.获取从服务器信息
当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构之外,Sentinel 还会创建 连接到 从服务器 的 命令连接 和 订阅连接
在创建命令连接之后,Sentinel在默认情况下,会以每十秒一次的频率 通过 命令连接 向从服务器 发送INFO
命令,并获得类似于以下内容的回复:
# Server
...
run_id:32be0699dd27b410f7c90dada3a6fab17f97899f
...
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
slave_repl_offset:11887
slave_priority:100
# Other sections
...
根据INFO
命令的回复,Sentinel会提取出以下信息:
- 从服务器的运行ID run_id
- 从服务器的角色role
- 主服务器的IP地址master_host,以及主服务器的端口号master_port
- 主从服务器的连接状态master_link_status
- 从服务器的优先级slave_priority
- 从服务器的复制偏移量slave_repl_offset
根据这些信息,Sentinel会对从服务器的实例结构进行更新,如下所示:
4.向主服务器和从服务器发送信息
在默认情况下,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正在监视的是从服务器,那么这些参数记录的就是 从服务器 正在复制 的 主服务器的信息
5.接收来自主服务器和从服务器的频道信息
当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令:
SUBSCRIBE __sentinel__:hello
Sentinel对__sentinel__:hello
频道的订阅 会一直持续到 Sentinel 与 服务器 的 连接 断开为止
也就是说,对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的__sentinel__:hello
频道发送信息,又通过订阅连接从服务器的__sentinel__:hello
频道接收信息
对于 监视同一个服务器 的 多个Sentinel来说,一个Sentinel发送的信息 会被 其他Sentinel接收到,这些信息 会被用于 更新其他Sentinel 对 发送信息Sentinel的认知,也会被用于 更新 其他Sentinel 对 被监视服务器的认知
当一个Sentinel从__sentinel__:hello
频道收到一条信息时,Sentinel会对这条信息进行分析,提取出信息中的Sentinel IP地址、Sentinel端口号、Sentinel运行ID等八个参数,并进行以下检查
- 如果 信息中 记录的 Sentinel运行ID 和 接收信息的Sentinel的运行ID 相同,那么说明这条信息是Sentinel自己发送的,Sentinel将丢弃这条信息,不做进一步处理
- 相反地,如果 信息中 记录的 Sentinel运行ID 和 接收信息的Sentinel的运行ID不相同,那么说明这条信息 是 监视同一个服务器的 其他Sentinel 发来的,接收信息的Sentinel 将根据 信息中的各个参数,对 相应主服务器的实例结构(sentinelRedisInstance) 进行更新
5.1 更新sentinels字典
Sentinel 为(给) 主服务器 创建的 实例结构(sentinelRedisInstance)中的 sentinels
字典 保存了 除Sentinel本身之外,所有 同样监视 这个主服务器 的 其他Sentinel的资料:
-
sentinels
字典 的 键 是 其中一个Sentinel的名字,格式为ip:port,比如对于IP地址为127.0.0.1,端口号为26379的Sentinel来说,这个Sentinel在sentinels
字典中的键就是"127.0.0.1:26379" -
sentinels
字典 的 值 是 键 所对应Sentinel的实例结构,比如对于键"127.0.0.1:26379"来说,这个键在sentinels
字典中的值就是IP为127.0.0.1,端口号为26379的Sentinel的实例结构
当 一个Sentinel 接收到 其他Sentinel 发来的信息时(称 发送信息的Sentinel 为 源Sentinel,接收信息的Sentinel 为 目标Sentinel),目标Sentinel 会从信息中 分析并提取出 以下两方面参数:
- 与Sentinel有关的参数:源Sentinel的IP地址、端口号、运行ID和配置纪元
- 与主服务器有关的参数:源Sentinel 正在监视的 主服务器的名字、IP地址、端口号和配置纪元
根据信息中提取出的主服务器参数,目标Sentinel 会在 自己的 Sentinel状态(sentinelState) 的 masters
字典中 查找 相应 的 主服务器实例结构(sentinelRedisInstance),然后根据提取出的Sentinel参数,检查 主服务器实例结构的sentinels
字典中,源Sentinel的实例结构是否存在
- 如果源Sentinel的实例结构已经存在,那么对源Sentinel的实例结构进行更新
- 如果源Sentinel的实例结构不存在,那么说明 源Sentinel 是 刚刚开始监视 主服务器 的 新Sentinel,目标Sentinel 会为 源Sentinel 创建一个 新的实例结构,并将这个结构添加到
sentinels
字典里面
5.2 创建 连向 其他Sentinel 的 命令连接
当Sentinel 通过频道信息 发现一个 新的Sentinel时,它不仅会为 新Sentinel在sentinels
字典中 创建 相应的实例结构,还会创建一个 连向 新Sentinel的命令连接
而 新Sentinel 也同样会创建 连向 这个Sentinel 的 命令连接,最终 监视同一主服务器的多个Sentinel 将形成 相互连接的网络
Sentinel A 有连向 Sentinel B 的 命令连接,而 Sentinel B 也有连向 Sentinel A 的 命令连接
使用命令连接相连的各个Sentinel 可以通过 向其他Sentinel发送命令请求 来进行 信息交换
Sentinel之间 是不会创建 订阅连接
Sentinel在连接主服务器或者从服务器时,会同时创建命令连接和订阅连接,但是在连接其他Sentinel时,却只会创建命令连接,而不创建订阅连接
这是因为Sentinel 需要通过 接收主服务器或者从服务器 发来的 频道信息 来发现 未知的新Sentinel,所以才需要建立订阅连接
而 相互已知的Sentinel 只要 使用 命令连接 来进行通信 就足够了
6.检测主观下线状态
在默认情况下,Sentinel会以每秒一次的频率 向 所有与它 创建了 命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING
命令,并通过实例返回的 PING
命令回复 来判断 实例是否在线
实例对PING命令的回复可以分为以下两种情况:
- 有效回复:实例返回+PONG、-LOADING、-MASTERDOWN三种回复的其中一种
- 无效回复:实例返回除+PONG、-LOADING、-MASTERDOWN三种回复之外的其他回复,或者在指定时限内没有返回任何回复
Sentinel配置文件中的down-after-milliseconds
选项 指定了 Sentinel 判断实例 进入主观下线所需的时间长度
如果一个实例在down-after-milliseconds
毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的flags
属性中打开SRI_S_DOWN
标识,以此来表示这个实例已经进入主观下线状态
主观下线时长选项的作用范围
用户设置的down-after-milliseconds
选项的值,不仅会被Sentinel 用来判断 主服务器 的 主观下线状态,还会被用于 判断 主服务器属下 的 所有从服务器,以及 所有同样监视这个主服务器 的 其他Sentinel的主观下线状态
多个Sentinel设置的主观下线时长可能不同
down-after-milliseconds
选项另一个需要注意的地方是,对于监视同一个主服务器的多个Sentinel来说,这些Sentinel所设置的down-after-milliseconds
选项的值也可能不同
因此,当一个Sentinel将主服务器判断为主观下线时,其他Sentinel可能仍然会认为主服务器处于在线状态
7.检测客观下线状态
当Sentinel 将一个主服务器 判断为 主观下线之后,为了确认 这个主服务器 是否 真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)
当Sentinel从其他Sentinel那里 接收到 足够数量的 已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作
7.1 发送SENTINEL is-master-down-by-addr
命令
Sentinel使用:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
命令 询问 其他Sentinel 是否同意 主服务器已下线
7.2 接收SENTINEL is-master-down-by-addr
命令
当一个Sentinel(目标Sentinel) 接收到 另一个Sentinel(源Sentinel) 发来的 SENTINEL is-master-down-by
命令时,目标Sentinel 会分析 并取出 命令请求中 包含的各个参数,并根据其中的主服务器IP和端口号,检查 主服务器 是否已下线,然后 向源Sentinel 返回一条 包含三个参数的Multi Bulk
回复 作为 SENTINEL is-master-down-by
命令的回复:
<down_state>
<leader_runid>
<leader_epoch>
7.3 接收SENTINEL is-master-down-by-addr
命令的回复
根据其他Sentinel发回的SENTINEL is-master-down-by-addr
命令回复,Sentinel 将统计 其他Sentinel同意主服务器已下线的数量,当这一数量 达到 配置指定的判断客观下线所需的数量时,Sentinel会将主服务器实例结构flags
属性的SRI_O_DOWN
标识打开,表示主服务器已经进入客观下线状态
客观下线状态的判断条件
当认为主服务器已经进入下线状态的Sentinel的数量,超过Sentinel配置中设置的quorum
参数的值,那么该Sentinel就会认为主服务器已经进入客观下线状态
不同Sentinel判断客观下线的条件可能不同
对于监视同一个主服务器的多个Sentinel来说,它们将主服务器标判断为客观下线的条件可能也不同
当一个Sentinel将主服务器判断为客观下线时,其他Sentinel可能并不是那么认为的
8.选举领头Sentinel(重要!!!)
当一个 主服务器 被判断为 客观下线时,监视这个 下线主服务器 的 各个Sentinel会进行协商,选举出一个领头Sentinel,并由 领头Sentinel 对 下线主服务器 执行 故障转移操作
以下是Redis选举领头Sentinel的规则和方法:
- 所有在线的Sentinel 都有 被选为 领头Sentinel的资格,换句话说,监视同一个主服务器的多个在线Sentinel中的任意一个都有可能成为领头Sentinel
- 每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元(configuration epoch)的值都会自增一次。配置纪元实际上就是一个计数器,并没有什么特别的
- 在一个配置纪元里面,所有Sentinel 都有一次 将 某个Sentinel 设置为 局部领头Sentinel 的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改
- 每个 发现 主服务器 进入 客观下线的Sentinel 都会要求 其他Sentinel 将自己 设置为 局部领头Sentinel
- 当一个Sentinel(源Sentinel) 向 另一个Sentinel(目标Sentinel)发送
SENTINEL is-master-down-by-addr
命令,并且命令中的runid
参数不是*
符号 而是 源Sentinel的运行ID时,这表示 源Sentinel 要求 目标Sentinel 将 前者(源Sentinel) 设置为 后者的(目标Sentinel) 局部领头Sentinel - Sentinel设置 局部领头Sentinel 的 规则是 先到先得:最先向 目标Sentinel 发送设置要求 的 源Sentinel 将成为 目标Sentinel 的 局部领头Sentinel,而之后接收到的所有设置要求都会被目标Sentinel拒绝
- 目标Sentinel 在接收到
SENTINEL is-master-down-by-addr
命令之后,将向 源Sentinel 返回一条命令回复,回复中的leader_runid
参数和leader_epoch
参数分别记录了 目标Sentinel 的 局部领头Sentinel 的 运行ID和配置纪元 - 源Sentinel 在 接收到 目标Sentinel 返回的 命令回复之后,会检查回复中
leader_epoch
参数的值 和 自己的配置纪元是否相同,如果相同的话,那么 源Sentinel 继续取出 回复中的leader_runid
参数,如果leader_runid
参数的值 和 源Sentinel的运行ID一致,那么表示 目标Sentinel 将 源Sentinel 设置成了 局部领头Sentinel - 如果有某个Sentinel 被 半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel
举个例子,在一个由10个Sentinel组成的Sentinel系统里面,只要有大于等于10/2+1=6个Sentinel将某个Sentinel设置为局部领头Sentinel,那么被设置的那个Sentinel就会成为领头Sentinel - 因为领头Sentinel的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部领头Sentinel,所以在一个配置纪元里面,只会出现一个领头Sentinel
- 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止
9.故障转移(重要!!!)
在 选举产生出 领头Sentinel之后,领头Sentinel 将对 已下线的主服务器 执行 故障转移操作,该操作包含以下三个步骤:
- 在 已下线主服务器 属下的 所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器
- 让 已下线主服务器 属下的 所有从服务器 改为 复制 新的主服务器
- 将 已下线主服务器 设置为 新的主服务器 的 从服务器,当这个 旧的主服务器 重新上线时,它就会成为 新的主服务器 的 从服务器
9.1 选出新的主服务器
故障转移操作第一步要做的就是在已下线主服务器属下的所有从服务器中,挑选出一个状态良好、数据完整的从服务器,然后向这个从服务器发送SLAVEOF no one
命令,将这个从服务器转换为主服务器
新的主服务器是怎样挑选出来的
领头Sentinel 会将 已下线主服务器 的 所有从服务器 保存到 一个列表里面,然后按照以下规则,一项一项地对列表进行过滤:
- 删除列表中 所有处于 下线或者断线状态 的 从服务器,这可以保证列表中剩余的从服务器都是正常在线的
- 删除列表中 所有 最近五秒内 没有回复过 领头Sentinel 的
INFO
命令的从服务器,这可以保证 列表中 剩余的从服务器 都是 最近成功进行过通信的 - 删除 所有 与 已下线主服务器 连接断开 超过
down-after-milliseconds*10
毫秒的从服务器:down-after-milliseconds
选项指定了判断主服务器下线所需的时间,而删除断开时长超过down-after-milliseconds*10
毫秒的从服务器,则可以保证 列表中 剩余的从服务器 都没有 过早地 与 主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的
之后,领头Sentinel将根据从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器
如果有多个具有相同最高优先级的从服务器,那么领头Sentinel将按照从服务器的复制偏移量,对具有相同最高优先级的所有从服务器进行排序,并选出其中偏移量最大的从服务器(复制偏移量最大的从服务器就是保存着最新数据的从服务器)
最后,如果有多个优先级最高、复制偏移量最大的从服务器,那么领头Sentinel将按照运行ID对这些从服务器进行排序,并选出其中运行ID最小的从服务器
以上流程如下所示:
在发送SLAVEOF no one
命令之后,领头Sentinel会以每秒一 次的频率(平时是每十秒一次),向被升级 的 从服务器 发送 INFO
命令,并观察命令回复中的角色(role
)信息,当被升级服务器的role从原来的slave变为master时,领头Sentinel就知道被选中的从服务器已经顺利升级为主服务器了,如下所示:
9.2 修改从服务器的复制目标
当新的主服务器出现之后,领头Sentinel下一步要做的就是,让 已下线主服务器 属下的 所有从服务器 去复制 新的主服务器,这一动作 可以通过 向 从服务器发送SLAVEOF
命令来实现
9.3 将旧的主服务器变为从服务器
故障转移操作最后要做的是,将 已下线的主服务器 设置为 新的主服务器 的 从服务器
因为旧的主服务器已经下线,所以这种设置 是保存在 server1 对应的 实例结构里面的,当server1重新上线时,Sentinel就会向它发送SLAVEOF
命令,让它成为server2的从服务器