redis-哨兵
为了提高redis服务的高可用性,redis提供了一个哨兵模式。在该模式下,哨兵会做以下三件事:
- 监控master节点和slave节点的运行情况;
- 当某个节点出现异常时,通知告警当前情况;
- 当master节点发生故障时,哨兵会从slave节点中挑选一个节点成为新的master,并通知其他的slave节点将服务器配置改为新的master,当客户端尝试连接原来的master时,会向客户端返回新的master地址。当运行哨兵模式的时候,都是多个哨兵一起运行,否则哨兵只能提供监控&告警的功能,故障修复功能则永远不会触发。
启动
哨兵是一种运行在redis-server下的特殊模式,可以通过redis-server /path/to/sentinel.conf --sentinel来启动,也可以直接通过redis-sentinel /path/to/sentinel.conf来启动。无论哪种形式启动,都必须指定哨兵的配置文件。
运行原理
获取拓扑结构
哨兵启动的时候,会指定需要监控的master节点信息,通过向master节点发送info命令,获取master下的所有slave节点信息。同时订阅master节点下的__sentinel__:hello频道来获取监控该master的其他哨兵。因为哨兵在监控一个master时,都会定时的向该频道发送自己的信息。如下图所示,左边是发现slave,右边是发现其他sentinel
监控
哨兵向自己发现的拓扑结构中的所有节点,定时发送INFO,PING命令检测节点的存活情况。对于PING命令,PONG正常响应、LOADING启动中、MASTERDOWN节点还没有连接上master都算是有效回复。对于INFO命令,除了获取拓扑结构外,也会根据返回的角色,来判断一个节点角色的变化。如果一个节点原本是slave,后面被人为的改成master,且原master是状态良好的话,哨兵会向该节点发送slaveof ip port命令,恢复原有的拓扑结构。正常运行情况如下所示:
当一个节点最近一次的响应时间超过设置后,则该节点被当前哨兵标记成主观下线。
当一个节点被多个哨兵标记成主观下线后,改节点会被当前哨兵标记成客观下线,哨兵通过彼此间的交流得知他人的结果。主观下线只适用于master节点,哨兵节点和slave节点不存在该状态,如下图所示:
报警
当哨兵发现各种异常后,会触发配置的脚本且向频道发布消息,部分频道如下:
- +slave:哨兵发现的新的slave节点
- +sentinel: 哨兵发现新的哨兵节点
- +sdown:一个节点被标记成主观下线
故障修复
当一个master节点被哨兵标记成客观下线后,哨兵之间会采用Raft算法选举出一个Leader。该Leader会进行以下操作:
- 从该master节点的所有slave节点中选出一个,发送slaveof no one命令,使其成为新的master节点
- 通知其他哨兵,更新对应的配置
- 向其他的slave发送slaveof ip port命令,更换master
- 故障修复完成,恢复正常
选举Leader的过程如下:
- 哨兵发现节点变成客观下线,把自己的纪元+1,然后准备选举自己成为这个纪元内的leader
- 向其他哨兵同步信息的时候,带上自己的运行id,期望他人投自己一票
- 其他哨兵接收到选举信息后,如果发现消息里的纪元比自己的要大,那么就以消息里的为准,来更新自己的纪元,并为该消息里的哨兵投上自己的一票,然后返回自己的投票结果
- 哨兵每隔一段时间,都会基于询问的结果,看看那个哨兵获得的投票最多,如果最多的票数大于等于两个数(1.总的哨兵节点/2+1 2.设置的最低票数),则会当选Leader
操作如下图所示:
配置文件
示例:
| 1 | sentinel monitor mymaster 127.0.0.1 6379 2 | 
所有和哨兵相关的配置都是以sentinel开始,第一行是最主要的配置,表示监控一个名为mymaster的节点,后面是节点的ip为127.0.0.1、端口6397以及该节点发生故障时,需要至少2个哨兵达成一致才可以。其余的配置都是针对mymaster的节点进一步参数配置,详细如下:
- down-after-milliseconds表示哨兵认为节点下线所需要的毫秒
- failover-timeout表示故障修复流程的超时时间
- parallel-syncs表示故障修复期间,最多有多少个slave节点可以同步到新的master
源码分析
哨兵模式的运行主要入口是定时任务sentinelTimer,上一级是serverCron,运行频率和serverCron一样。
数据结构
| 1 | //ip&端口 | 
redisAeReadEvent
处理读事件
| 1 | static void redisAeReadEvent(aeEventLoop *el, int fd, void *privdata, int mask) { | 
redisAeWriteEvent
写事件
| 1 | static void redisAeWriteEvent(aeEventLoop *el, int fd, void *privdata, int mask) { | 
redisAeAddRead
增加读事件处理
| 1 | static void redisAeAddRead(void *privdata) { | 
redisAeDelRead
删除读处理
| 1 | static void redisAeDelRead(void *privdata) { | 
redisAeAddWrite
添加写事件处理
| 1 | static void redisAeAddWrite(void *privdata) { | 
redisAeDelWrite
删除写处理
| 1 | static void redisAeDelWrite(void *privdata) { | 
redisAeCleanup
删除写处理&读处理
| 1 | static void redisAeCleanup(void *privdata) { | 
redisAeAttach
把异步的相应信息添加到主事件循环中
| 1 | static int redisAeAttach(aeEventLoop *loop, redisAsyncContext *ac) { | 
哨兵模式下接受的命令
哨兵模式是一种特殊的redis模式,只能响应部分命令
| 1 | struct redisCommand sentinelcmds[] = { | 
initSentinelConfig
初始化哨兵的配置
| 1 | void initSentinelConfig(void) { | 
initSentinel
初始化哨兵
| 1 | void initSentinel(void) { | 
sentinelIsRunning
检查哨兵模式下的必要条件,创建运行id,更新配置到文件
| 1 | void sentinelIsRunning(void) { | 
createSentinelAddr
创建哨兵地址结构体,根据host和端口
| 1 | sentinelAddr *createSentinelAddr(char *hostname, int port) { | 
dupSentinelAddr
复制一个地址结构体
| 1 | sentinelAddr *dupSentinelAddr(sentinelAddr *src) { | 
releaseSentinelAddr
释放地址结构体
| 1 | void releaseSentinelAddr(sentinelAddr *sa) { | 
sentinelAddrIsEqual
判断两个地址结构体是否一样
| 1 | int sentinelAddrIsEqual(sentinelAddr *a, sentinelAddr *b) { | 
sentinelEvent
产生一个哨兵事件
level表示日志等级,只有LL_WARNING级别的才会触发脚本通知
fmt以‘%@’开始,表示输出ri节点的信息,格式为 
| 1 | void sentinelEvent(int level, char *type, sentinelRedisInstance *ri, | 
sentinelGenerateInitialMonitorEvents
产生+monitor事件,在初始化的时候针对监控的每一个master触发
| 1 | void sentinelGenerateInitialMonitorEvents(void) { | 
脚本
sentinelReleaseScriptJob
释放脚本作业
| 1 | void sentinelReleaseScriptJob(sentinelScriptJob *sj) { | 
sentinelScheduleScriptExecution
| 1 | 
 | 
sentinelGetScriptListNodeByPid
根据进程id,获取对应的任务
| 1 | listNode *sentinelGetScriptListNodeByPid(pid_t pid) { | 
sentinelRunPendingScripts
运行脚本
| 1 | void sentinelRunPendingScripts(void) { | 
sentinelScriptRetryDelay
根据失败次数获取下一次执行任务的延迟时间,每失败一次时间久翻倍
| 1 | mstime_t sentinelScriptRetryDelay(int retry_num) { | 
sentinelCollectTerminatedScripts
根据脚本进程的退出情况,处理后续
| 1 | void sentinelCollectTerminatedScripts(void) { | 
sentinelKillTimedoutScripts
干掉运行超时的脚本
| 1 | void sentinelKillTimedoutScripts(void) { | 
sentinelPendingScriptsCommand
响应sentinel pending-script命令,获取任务队列中的脚本信息
| 1 | void sentinelPendingScriptsCommand(client *c) { | 
sentinelCallClientReconfScript
slave节点更新配置时,触发的脚本
脚本的参数为
| 1 | void sentinelCallClientReconfScript(sentinelRedisInstance *master, int role, char *state, sentinelAddr *from, sentinelAddr *to) { | 
连接相关
哨兵和master,slave,其他哨兵的连接,都是使用instanceLink结构体存储,分为普通命令连接和发布订阅连接两种,都是异步的流程
createInstanceLink
创建连接
| 1 | instanceLink *createInstanceLink(void) { | 
instanceLinkCloseConnection
关闭特定的连接,普通命令连接或者发布订阅连接
| 1 | void instanceLinkCloseConnection(instanceLink *link, redisAsyncContext *c) { | 
releaseInstanceLink
释放连接,连接都是
| 1 | instanceLink *releaseInstanceLink(instanceLink *link, sentinelRedisInstance *ri) | 
sentinelTryConnectionSharing
尝试共享连接,当一个哨兵监听多个master时,每个master的sentinels里,都有一个sentinelRedisInstance哨兵节点,那么这些哨兵的节点是可以共用一个的
| 1 | int sentinelTryConnectionSharing(sentinelRedisInstance *ri) { | 
sentinelUpdateSentinelAddressInAllMasters
更新所有master中的某一个哨兵的地址信息,使用ri中的地址替换
| 1 | int sentinelUpdateSentinelAddressInAllMasters(sentinelRedisInstance *ri) { | 
instanceLinkConnectionError
连接出错
| 1 | void instanceLinkConnectionError(const redisAsyncContext *c) { | 
sentinelLinkEstablishedCallback
确定连接结果的回调,只处理失败的情况
| 1 | void sentinelLinkEstablishedCallback(const redisAsyncContext *c, int status) { | 
sentinelDisconnectCallback
断开连接的回调
| 1 | void sentinelDisconnectCallback(const redisAsyncContext *c, int status) { | 
哨兵管理节点实例
每一个master、slave、其他的哨兵都是在该对象下管理
createSentinelRedisInstance
创建节点实例
name 节点实例的名字,对于master,是在配置文件中指定的;对于salve,是ip:port;对于其他哨兵,是起runid
flags 节点实例类型,master、slave、哨兵
hostname 实例主机名
port 实例端口
quorum master节点发生故障时,需要多少个哨兵达成一致才可以
master 创建slave、哨兵节点实例的时候,对应的master信息,创建master时,为NULL
| 1 | sentinelRedisInstance *createSentinelRedisInstance(char *name, int flags, char *hostname, int port, int quorum, sentinelRedisInstance *master) { | 
releaseSentinelRedisInstance
释放节点实例
| 1 | void releaseSentinelRedisInstance(sentinelRedisInstance *ri) { | 
sentinelRedisInstanceLookupSlave
根据ip和端口在master节点中查找对应的slave节点实例
| 1 | sentinelRedisInstance *sentinelRedisInstanceLookupSlave( | 
sentinelRedisInstanceTypeStr
返回实例对应的类型
| 1 | const char *sentinelRedisInstanceTypeStr(sentinelRedisInstance *ri) { | 
removeMatchingSentinelFromMaster
从master的监听哨兵中移除一个哨兵,根据runid
| 1 | int removeMatchingSentinelFromMaster(sentinelRedisInstance *master, char *runid) { | 
getSentinelRedisInstanceByAddrAndRunID
在节点字典里,根绝ip,port,runid查找节点,ip和runid必须有一个非空
| 1 | sentinelRedisInstance *getSentinelRedisInstanceByAddrAndRunID(dict *instances, char *ip, int port, char *runid) { | 
sentinelGetMasterByName
根据名称获取master节点实例
| 1 | sentinelRedisInstance *sentinelGetMasterByName(char *name) { | 
sentinelAddFlagsToDictOfRedisInstances
向一堆节点添加标志位
| 1 | void sentinelAddFlagsToDictOfRedisInstances(dict *instances, int flags) { | 
sentinelDelFlagsToDictOfRedisInstances
向一堆节点删除标志位
| 1 | void sentinelDelFlagsToDictOfRedisInstances(dict *instances, int flags) { | 
sentinelResetMaster
重置master节点信息
| 1 | 
 | 
sentinelResetMastersByPattern
正则匹配,重置master
| 1 | int sentinelResetMastersByPattern(char *pattern, int flags) { | 
sentinelResetMasterAndChangeAddress
重置master并改变ip和port
| 1 | int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) { | 
sentinelRedisInstanceNoDownFor
判断节点在指定的超时时间内,是否有下线转态,非0表示没有
| 1 | int sentinelRedisInstanceNoDownFor(sentinelRedisInstance *ri, mstime_t ms) { | 
sentinelGetCurrentMasterAddress
获取当前节点master的地址
| 1 | sentinelAddr *sentinelGetCurrentMasterAddress(sentinelRedisInstance *master) { | 
sentinelPropagateDownAfterPeriod
更新master下的所有slave、哨兵节点的 变为下线所需要的时间
| 1 | void sentinelPropagateDownAfterPeriod(sentinelRedisInstance *master) { | 
sentinelGetInstanceTypeString
获取节点类型
| 1 | char *sentinelGetInstanceTypeString(sentinelRedisInstance *ri) { | 
sentinelInstanceMapCommand
获取重命名的命令,当我们从哨兵节点向master发送命令的时候,可能需要采用别名;因为master方面可能为了安全考虑,已经更换了conf,saleof的名字
| 1 | char *sentinelInstanceMapCommand(sentinelRedisInstance *ri, char *command) { | 
配置相关
sentinelHandleConfiguration
处理配置文件中相关的配置
| 1 | char *sentinelHandleConfiguration(char **argv, int argc) { | 
rewriteConfigSentinelOption
重写哨兵配置
| 1 | void rewriteConfigSentinelOption(struct rewriteConfigState *state) { | 
sentinelFlushConfig
把哨兵模式下的设置参数写入到文件中
| 1 | void sentinelFlushConfig(void) { | 
hiredis
hiredis是一个Redis官方的开发库,封装了各种连接和遵守redis协议的操作
sentinelSendAuthIfNeeded
发送密码验证,如果设置了的话,auth命令
| 1 | void sentinelSendAuthIfNeeded(sentinelRedisInstance *ri, redisAsyncContext *c) { | 
sentinelSetClientName
设置当前连接的名字,client命令
| 1 | void sentinelSetClientName(sentinelRedisInstance *ri, redisAsyncContext *c, char *type) { | 
sentinelReconnectInstance
重新连接实例
| 1 | void sentinelReconnectInstance(sentinelRedisInstance *ri) { | 
sentinelMasterLooksSane
判断master是否正常
| 1 | int sentinelMasterLooksSane(sentinelRedisInstance *master) { | 
sentinelRefreshInstanceInfo
根据info命令返回信息,更新节点信息
| 1 | void sentinelRefreshInstanceInfo(sentinelRedisInstance *ri, const char *info) { | 
sentinelInfoReplyCallback
info命令的回调
| 1 | void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { | 
sentinelDiscardReplyCallback
忽略内容的回调
| 1 | void sentinelDiscardReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { | 
sentinelPingReplyCallback
ping命令的回调
| 1 | void sentinelPingReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { | 
sentinelPublishReplyCallback
发布消息的回调
| 1 | void sentinelPublishReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { | 
sentinelProcessHelloMessage
处理hello频道的消息
| 1 | void sentinelProcessHelloMessage(char *hello, int hello_len) { | 
sentinelReceiveHelloMessages
收到hello频道消息的回调
| 1 | void sentinelReceiveHelloMessages(redisAsyncContext *c, void *reply, void *privdata) { | 
sentinelSendHello
向hello频道发送自身的状况
| 1 | int sentinelSendHello(sentinelRedisInstance *ri) { | 
sentinelForceHelloUpdateDictOfRedisInstances
强制更新每一个节点的上一次发布消息时间,为了下一次扫描的时候一定会发送hello消息
| 1 | void sentinelForceHelloUpdateDictOfRedisInstances(dict *instances) { | 
sentinelForceHelloUpdateForMaster
强制更新master及其下面的哨兵、从节点的发布时间
| 1 | int sentinelForceHelloUpdateForMaster(sentinelRedisInstance *master) { | 
sentinelSendPing
发送ping命令
| 1 | int sentinelSendPing(sentinelRedisInstance *ri) { | 
sentinelSendPeriodicCommands
哨兵的定时处理任务
| 1 | void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) { | 
响应命令
sentinelFailoverStateStr
获取故障修复状态的描述
| 1 | const char *sentinelFailoverStateStr(int state) { | 
addReplySentinelRedisInstance
添加单个节点的各种信息
| 1 | void addReplySentinelRedisInstance(client *c, sentinelRedisInstance *ri) { | 
addReplyDictOfRedisInstances
批量添加节点的信息
| 1 | void addReplyDictOfRedisInstances(client *c, dict *instances) { | 
sentinelGetMasterByNameOrReplyError
根据名字获取master节点信息
| 1 | sentinelRedisInstance *sentinelGetMasterByNameOrReplyError(client *c, | 
sentinelIsQuorumReachable
哨兵的数量是否支持选举,要求状态良好的哨兵数量满足两个条件:1.达到设置的最低数量 2.在总哨兵数量的比例要>50%
usableptr 返回状态良好哨兵数量
| 1 | 
 | 
sentinelCommand
响应sentinel命令,运行中动态改变哨兵配置
| 1 | void sentinelCommand(client *c) { | 
sentinelInfoCommand
获取哨兵信息,包括server、client、cpu、stats等信息
| 1 | 
 | 
sentinelRoleCommand
响应role命令,返回sentinel以及监听的所有的master名字
| 1 | void sentinelRoleCommand(client *c) { | 
sentinelSetCommand
响应sentinel set命令,更新监听master的哨兵配置
| 1 | void sentinelSetCommand(client *c) { | 
sentinelPublishCommand
响应订阅命令,只有hello频道才有用
| 1 | void sentinelPublishCommand(client *c) { | 
判断校验
sentinelCheckSubjectivelyDown
判断节点是否为主观下线
| 1 | void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) { | 
sentinelCheckObjectivelyDown
检查是否客观下线,
| 1 | void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) { | 
sentinelReceiveIsMasterDownReply
处理询问其它哨兵master是否挂掉的回调,SENTINEL is-master-down-by-addr
| 1 | void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) { | 
sentinelAskMasterStateToOtherSentinels
向其他所有哨兵询问master是否挂了,为了尽快的得到回复,选举出leader然后开始故障修复
| 1 | 
 | 
故障修复
sentinelSimFailureCrash
故障修复失败模式执行时的崩溃
| 1 | void sentinelSimFailureCrash(void) { | 
sentinelVoteLeader
在某个纪元下投指定的leader一票,返回现在的leader和leader所在的纪元
| 1 | char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) { | 
sentinelLeaderIncr
对某个leader+1计数
| 1 | struct sentinelLeader { | 
sentinelGetLeader
在某个纪元下,获取leader
| 1 | char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) { | 
sentinelSendSlaveOf
向节点发送slaveof命令,更改其master指向,如果host为NULL,表示执行slaveof no one。发送该命令,永远都会跟随一个rewrite config命令,让节点更新配置到磁盘上
| 1 | int sentinelSendSlaveOf(sentinelRedisInstance *ri, char *host, int port) { | 
sentinelStartFailover
开始故障修复
| 1 | void sentinelStartFailover(sentinelRedisInstance *master) { | 
sentinelStartFailoverIfNeeded
如果条件满足的话,开始故障修复
| 1 | int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) { | 
compareSlavesForPromotion
挑选成为新master的salve快排比较方法
| 1 | int compareSlavesForPromotion(const void *a, const void *b) { | 
sentinelSelectSlave
从一堆salve中选出新的master,能进入预选的slave需要满足以下条件
- 情况正常,没有被判定为主观下线或者客观下线
- 连接正常
- 距离上一次有效ping的时间,要小于5倍的ping频率(1s)
| 1 | sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) { | 
故障修复各个节点处理
sentinelFailoverWaitStart
等待开始
| 1 | void sentinelFailoverWaitStart(sentinelRedisInstance *ri) { | 
sentinelFailoverSelectSlave
从slaves中确定新的master
| 1 | void sentinelFailoverSelectSlave(sentinelRedisInstance *ri) { | 
sentinelFailoverSendSlaveOfNoOne
向选中的slave节点发送slaveof no one命令,使其成为master
| 1 | void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) { | 
sentinelFailoverWaitPromotion
等待选中的salve晋升
| 1 | void sentinelFailoverWaitPromotion(sentinelRedisInstance *ri) { | 
sentinelFailoverDetectEnd
故障修复的收尾
| 1 | void sentinelFailoverDetectEnd(sentinelRedisInstance *master) { | 
sentinelFailoverReconfNextSlave
分批更新salve的master
| 1 | void sentinelFailoverReconfNextSlave(sentinelRedisInstance *master) { | 
sentinelFailoverSwitchToPromotedSlave
切换master节点信息
| 1 | void sentinelFailoverSwitchToPromotedSlave(sentinelRedisInstance *master) { | 
sentinelFailoverStateMachine
根据故障修复进度执行对应的操作
| 1 | void sentinelFailoverStateMachine(sentinelRedisInstance *ri) { | 
sentinelAbortFailover
故障修复失败处理
| 1 | void sentinelAbortFailover(sentinelRedisInstance *ri) { | 
定时任务
sentinelHandleRedisInstance
每一个节点的处理
| 1 | void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { | 
sentinelHandleDictOfRedisInstances
处理节点集合
| 1 | void sentinelHandleDictOfRedisInstances(dict *instances) { | 
sentinelCheckTiltCondition
检查tilt模式
| 1 | void sentinelCheckTiltCondition(void) { | 
sentinelTimer
定时任务入口
| 1 | void sentinelTimer(void) { |