Redis Sentinel介绍

165 阅读11分钟

Sentinel介绍

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

了解原理之前先问几个问题

  • Sentinel是怎么知道master和slave的,内部是如何维护的?
  • Sentinel与Sentinel又是如何知道的,内部是如何维护的?
  • Sentinel是怎么知道master下线的?
  • master下线之后由谁来进行故障转移?
  • 具体的故障转移的步骤又是怎样的?

启动并初始化Sentinel

启动一个Sentinel可以使用下面任一命令

redis-sentinel /path/to/your/sentinel.conf
redis-server /path/to/your/sentinel.conf --sentinel

当一个Sentinel启动时,它需要执行以下步骤(主要讲后面三个步骤了解几个结构体):

1. 初始化服务器。

2. 将普通Redis服务器使用的代码替换成Sentinel专用代码。

3. 初始化Sentinel状态。

这一步服务器会初始化一个sentinel.c/sentinelState结构,这个结构保存了服务器中所有Sentinel功能相关的状态

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;
4. 根据给定的配置文件,初始化Sentinel的监视主服务器列表

现在需要初始化SentinelState中的masters属性,masters是一个dict,key为master的名字,value是被监视主服务器对应的sentinel.c/sentinelRedisInstance结构

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;
    // ...
    // 其他监控相同主节点的Sentinel
    dict *sentinels;
} sentinelRedisInstance;

其中的sentinelAddr,即实例的地址

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

举个例子,用户在启动Sentinel的时候,指定了包含以下内容的文件: image.png

加载配置文件之后sentinelState的masters字段将会对应两个sentinelRedisInstance

image.png
5. 创建连向主服务器的网络连接。

这个时候sentinelState里面已经保存了masters的信息,最后一步会创建连向master的网络连接,对于每个被Sentinel监视的master来说,Sentinel会创建两个连向master的异步网络连接

  • 一个是命令连接,这个连接专门用于向master发送命令,并接收命令回复。
  • 另一个是订阅连接,这个连接专门用于订阅主服务器的__sentinel__:hello频道

获取主服务器信息

Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
举个例子,主服务器master有三个从服务器slave0、slave1 和slave2,并且一个Sentinel正在连接主服务器,那么Sentinel将持续地向主服务器发送INFO命令,并获得类似于以下内容的回复:

image.png

还记得初始化时候的sentinelRedisInstance吗,它不仅可以用作master,同样也可以用作slave,sentinel接收到INFO命令之后,会更新sentinelState的masters字段,也就是说sentinel现在不仅有master的信息,同样,也有slave的信息,master以及包含的slave的结构体如下:

image.png

获取从服务器信息

虽然能够通过给master发送INFO命令接收到slave大致的信息,但是还需要更详细的信息,同时还需要向slave也创建命令连接订阅连接
创建完命令连接之后,同样的,Sentinel默认每十秒一次的频率,通过命令连接向slave发送INFO命令,并获得类似于以下的回复:

image.png

Sentinel会根据INFO命令回复对从服务器的实例结构进行更新

到这里可以解决第一个问题了,Sentinel是怎么知道master和slave的,内部是如何维护的? 其实本质上还是通过配置文件知道master的信息,Sentinel启动的时候初始化了sentinelState,里面的masters字段维护了所有主服务器,后面会向master建立命令连接订阅连接,通过INFO命令可以知道master的详细信息,以及slave的信息,从而可以向slave建立命令连接订阅连接,也会发送INFO命令获取slave的信息


说这么多好像还没有用到订阅连接,别急,继续往下看

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

在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的master和slave发送以下格式的命令
PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
PUBLISH就是向__sentinel__:hello频道中发送消息,所有订阅了该频道的服务器都会收到消息,那到底发的是什么消息呢?

image.png

image.png 不难理解,s开头的都是Sentinel相关的,m开头的都是master相关的,至于epoch跟故障转移相关,后面再讲

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

还记得之前Sentinel已经和所有的master以及slave建立了命令连接以及订阅连接吗?建立订阅连接之后Sentinel就会通过订阅连接向服务器发送SUBSCRIBE __sentinel__:hello命令,即订阅__sentinel__:hello频道,这也就是说Sentinel既通过命令连接向服务器的__sentinel__:hello频道发送消息,又通过订阅连接向服务器的__sentinel__:hello频道接受消息,那接收的消息有什么用呢?如何解析呢?

举个例子:假设现在有Sentinel1Sentinel2Sentinel3三个Sentinel在监视同一个服务器,那么当Sentinel1向服务器的__sentinel__:hello频道发送消息的时候,所有订阅了__sentinel__:hello频道的Sentinel都会收到这条消息(包括自己)

收到消息的Sentinel,如果发现消息是自己发送的就忽略,如果是其他Sentinel发送的会做两件事

  • 更新Sentinel字典

即更新sentinelState维护的master的sentinels字段 image.png

  • 创建连向其他Sentinel的命令连接

当Sentinel通过订阅频道发现一个新的Sentinel时,不仅会为新的Sentinel在Sentinel字典中创建相应的实例结构,还会创建一个连向新的Sentinel的命令连接

到这里就能回答文章开头的第二个问题了,Sentinel与Sentinel又是如何知道的,内部是如何维护的?
答案是Sentinel通过命令连接向服务器的__sentinel__:hello频道发送sentinel和master相关的信息,而其他Sentinel通过订阅连接收到消息,会即更新sentinelState维护的master的sentinels字段,同时创建一个连向新的Sentinel的命令连接

说了这么多,Sentinel与master,slave,以及其他Sentinel的连接到底有什么用呢?
其实是用来检测服务器的存活状态,并且选出领头Sentinel对已下线的主服务器执行故障转移

检测主观下线状态

所谓检测主观下线状态,就是Sentinel判断某个主服务器是否下线了
在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括Master、Slave、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线,那多长时间没有回复算下线呢?

举个例子,Sentinel设置了以下配置:

image.png 那么50000毫秒不仅会成为Sentinel判断master进入主观下线的标准,还会成为 Sentinel判断master属下所有slave,以及所有同样监视master的其他Sentinel进入主观下线的标准,检测到下线之后会将sentinelRedisInstanceflags字段的SRI_S_DOWN标识打开

检测客观下线状态

当Sentinel将一个master判断为主观下线后,为了确认这个服务器是否真的下线,它会询问其他监视该master的Sentinel,看它们是否也认为该master下线了

举个例子:Sentinel1使用SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>命令询问其他Sentinel,各个字段的意义如下,至于current_epoch和runid先忽略

image.png

Sentinel2收到命令的会检查master是否下线,然后回复给Sentinel1如下命令
SENTINEL is-master-down-by-addr <down_state> <leader_runid> <leader_epoch>
参数意义如下,leader_runidleader_epoch先忽略 image.png 比如说,Sentinel2也同意master下线,就会回复SENTINEL is-master-down-by-addr 1 * 0

Sentinel1收到回复之后会将同意数量统计,当这一个数量达到配置指定判断客观下线所需数量的时候,Sentienl1会将master实例结构的flags属性的SRI_O_DOWN标识打开

image.png

到这里就很容易知道了,Sentinel是怎么知道master下线的?其实是通过PING命令主观判断,而是否客观下线是通过和其他Sentinel确认得到的

选取领头Sentinel

当一个主服务器被判断为客观下线的时候,监视这个下线服务器的各个Sentinel会进行协商,选出一个领头Sentinel,并由领头Sentinel进行故障转移操作
选取领头Sentinel的规则和方法:

image.png

image.png

举个例子:假设现在有三个Sentinel正在监视同一个主服务器,并且这三个Sentinel之前已经通过SENTINEL is-master-down-by-addr命令确认master进入了客观下线状态,那么为了选出领头Sentinel,三个Sentinel再次向其他Sentinel发送SENTINEL is-master-down-by-addr命令,这次会带上自己的运行ID和epoch
例如:
SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 e955b4c85598ef5b5f055bc 7ebfd5e828dbed4fa
如果接收到这个命令的Sentinel还没有设置局部领头Sentinel的话,它就会将运行ID为e955b4c85598ef5b5f055bc 7ebfd5e828dbed4fa的Sentinel设置为自己的局部领头Sentinel,并返回SENTINEL is-master-down-by-addr 1 e955b4c85598ef5b5f055bc 7ebfd5e828dbed4fa 0,根据这一回复,统计出有多少个Sentinel将自己设置了局部领头Sentinel,最终领头Sentinel就可以对master执行故障转移操作了

master下线之后由谁来进行故障转移?到这里我们解决了第四个问题,所有确认master为客观下线状态的Sentinel会选出一个领头Sentinel执行故障转移

故障转移

故障转移包含三个步骤

1、选出新的主服务器

从已下线master的slave列表里面选出一个状态良好,数据完整的slave,然后向这个从服务器发送SLAVEOF no one命令,将这个slave转换为master

2、修改从服务器的复制目标

对其他slave发送SLAVEOF <new_master_ip> <new_master_port>命令,将其他slave复制新的master

3、将旧的主服务器变为从服务器

因为旧的master已经下线了,所以这种设置是保存在旧的master对应的实例结构里面的,当server1重新上线时,Sentinel会向它发送SLAVEOF <new_master_ip> <new_master_port>命令,让他成为新的master的slave

参考《Redis设计与实现》