Redis设计与实现3.2:Sentinel
Sentinel哨兵
这是《Redis设计与实现》系列的文章,系列导航:Redis设计与实现笔记
哨兵:监视、通知、自动故障恢复
启动与初始化
Sentinel 的本质只是一个运行在特殊模式下的 Redis 服务器,所以启动 Sentinel 的步骤如下:
-
初始化一个普通的 Redis 服务器,不过也有一些不同:
-
将一部分 Redis 服务器使用的代码替换成 Sentinel 专用代码
举两个例子:
-
服务器端口由
redis.h/REDIS_SERVERPORT
修改为sentinel.c/REDIS_SENTINELPORT
-
服务器的命令表替换为
sentinel.c/sentinelcmds
// 服务器在 sentinel 模式下可执行的命令 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} };
-
-
初始化 Sentinel 状态
可以看一下这个状态的定义:
/* Main state. */ /* Sentinel 的状态结构 */ struct sentinelState { // 当前纪元 uint64_t current_epoch; /* Current epoch. */ // 保存了所有被这个 sentinel 监视的主服务器 // 字典的键是主服务器的名字 // 字典的值则是一个指向 sentinelRedisInstance 结构的指针 dict *masters; // 是否进入了 TILT 模式? int tilt; /* Are we in TILT mode? */ // 目前正在执行的脚本的数量 int running_scripts; /* Number of scripts in execution right now. */ // 进入 TILT 模式的时间 mstime_t tilt_start_time; /* When TITL started. */ // 最后一次执行时间处理器的时间 mstime_t previous_time; /* Last time we ran the time handler. */ // 一个 FIFO 队列,包含了所有需要执行的用户脚本 list *scripts_queue; /* Queue of user scripts to execute. */ } sentinel;
-
初始化 Sentinel 状态的 masters 属性
dict *masters;
是一个字典结构,键是被监视主服务器的名称,值是主服务器对应的sentinel.c/sentinelReidsInstance
结构这个初始化是根据被载入的 Sentinel 配置文件来进行的
-
创建网络连接
Sentinel 将成为主服务器的客户端,它可以向主服务器发送命令,并从命令回复中获取相关的信息。会创建两个连向主服务器的异步网络连接:
- 一个是命令连接,专门用于向主服务器发送命令,并接收命令回复
- 一个是订阅连接,专用用于订阅主服务器的
__sentinel__:hello
频道(订阅的好处是可以防止消息丢失)
与服务器进行通信
获取主服务器信息
如上图所示:
- Sentinel 默认会以每10秒一次的频率发送 INFO 命令,获取主节点的信息
- 主节点会返回自身和其从节点的信息
- Sentinel 接收到信息后,更新自己的 masters 字典,如果有新节点,则创建之
获取从服务器信息
当 Sentinel 发现主服务器有新的从节点时,会创建到从节点的命令连接和订阅链接:
同样的,以10秒一次的频率发送 INFO 命令并获取返回信息:
并更新自己保存的信息。
发送频道信息
默认情况下,Sentinel会以每两秒一次的频率,通过命令向所有被监视的主服务器和从服务器发送命令:
PUBLISH __sentinel__:hello "xxx"
这条命令向服务器的 __sentinel__
频道发送了一条消息,在上面我用"xxx"表示出来了,其具体组成有:
即两部分:
- 自己的信息
- 主服务器的信息
接收频道消息
前面提到了,Sentinel 会向服务器的频道发送信息:
PUBLISH __sentinel__:hello "xxx"
另一方面,Sentinel 还会订阅所有被监视服务器的频道:
SUBSCRIBE __sentinel__:hello
对于监视同一个服务器的多个 Sentinel 来说,这些消息会被用于更新其他 Sentinel 对发送信息的 Sentinel 的认知,也会被用于更新其他 Sentinel 对被监视服务器的认知。
而更新的具体数据是:sentinelState 结构体的 dict *masters;
变量(上文提到过)指向的 sentinelRedisInstance
的 sentinels
字典变量(这个变量保存了所有监视这个服务器的 Sentinel)
- 键位Sentinel的IP和端口
- 值指向sentinel实例
而具体的更新流程是:
这样做的一个好处是,可以自动发现其他 Sentinel,并形成相互连接的网络,而无需手动配置。
Sentinel 之间只会创建命令链接,而不会创建订阅链接。
因为之所以和服务器需要创建订阅链接就是用来发现未知的新的 Sentinel 的。
服务器意外状态
检测主观下线状态
Sentinel 会以每秒一次的频率向所有与他建立了命令简介的实例(包括主、从、Sentinel服务器)发送 PING 命令,并通过返回信息判断实例的状态。
实例对 PING 的回复有两种:
- 有效回复:
+PING
、-LOADING
、-MASTERDOWN
- 无效回复:其他内容或超时
如果一个实例在 down-after-milliseconds
配置的时间内没有返回有效回复,就会被标记为主观下线状态
检测客观下线状态
Sentinel 也要问问别的监控目标的 Sentinel 的意见,才好决定是否是真的下线了。
is-master-down-by-addr
有几个参数,包含了:
- ip、port:被审判的主机的ip和端口号
- current_epoch:当前的配置纪元,用以选举领头 Sentinel 进行故障转移
- runid:
*
表示判断客观下线- 如果是 Sentinel 的运行 ID 则用来选举领头
multi bulk
是 Sentinel 的返回值(为什么叫这个名字?文档是这么叫的),包含了三个值:
- down_state:是否下线
- leader_runid:
*
表示仅仅用以检测服务器的下线状态- 如果是领头 Sentinel 的 ID 则说明用于选举领头 Sentinel
- leader_epoch:
- 如果leader_runid为
*
,则为0 - 否则为配置纪元
- 如果leader_runid为
你应该看出来了,上面的两条命令有两种作用:
- 判断是否下线
- 选举领头 Sentinel
选举领头 Sentinel
当一个主服务器被判断为客观下线后,监视这个服务器的各个 Sentinel 会进行协商,选举一个领头的 Sentinel 并进行故障转移。
我的理解:
这里只有中间的 Sentinel 确定了客观下线这一事实,其他的 Sentinel 未必认同,但是即便如此,只要有一个 Sentinel 认定了客观下线的情况,其他 Sentinel 也会配合进行选举、故障转移。
选举的策略是:
- 所有人都有机会当选
- 发现主观下线的会向其他选手拉选票
- 所有人都是给第一个要求投票的人
- 超过一半选票的人当选
如果在给定时限中没有选出leader,则在一段时间后再次进行选举,直到选出leader。
这么一种做法有没有可能在很长的一段时间内都发生选举失败的情况呢?
这个可能要之后学习一下Raft算法的领头选举算法。
故障转移
领头 leader 将对已下线的主服务进行故障转移操作:
- 选一个新的主服务器
- 让前任主服务器的所有从服务器跟着现任服务器
- 将前任设置为现任的从服务器
如何选新的服务器:
- 筛选排除:
- 下线的、断线状态的
- 最近5秒内都没有回复过leader的INFO命令的服务器
- 与前任主服务器断开超过
down-after-milliseconds * 10
的服务器- 优先选择:
- 优先级较高
- 复制偏移量较大
- ID最小