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) { |