引言
接上一篇,今天我们来聊一聊Redis的高可用的第二个解决方案:哨兵模式。
一、Redis哨兵模式
哨兵模式(sentinel)是Redis提供的高可用的解决方案之一。由一个或者多个sentinel示例组成的sentinel系统,可以监听任意数量的主服务器,以及这些服务器属下的所有从服务器,并在被监视的主服务进入下线状态时,自动从该主服务器属下的从服务器中选出新的主服务器,对外提供服务。
1.1 哨兵模式优缺点
- 优点:在主从复制的基础上实现了主节点的自动故障转移
- 缺点:部署麻烦、数据可能丢失、扩展麻烦、主从迁移很慢(大概在几十秒),实际项目中大部分用的是集群的解决方案
sentinel本质上是一个运行在特殊模式下的Redis服务器。不过,因为sentinel不存储业务数据,只用来监视数据节点的状态,所以它的代码和普通Redis服务器是不同的。比如:服务的端口和命令表的不同。
sentinel使用的端口及命令表
#define REDIS_SENTINEL_PORT 26379
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"fast @connection",0,NULL,0,0,0,0,0},
...
{"info",sentinelInfoCommand,-1,"random @dangerous",0,NULL,0,0,0,0,0},
...
};
二、如何使用哨兵模式
2.1 启动哨兵模式
使用命令启动sentinel
redis-sentinel /path/sentinel.conf
redis-server /path/sentinel.conf --sentinel
假设有如下配置文件:表示该sentinel监视了两个主服务器master1和master2 ,该sentinel会创建如下图的两个sentinelRedisInstance 实例结构,结构源码及各个字段的功能描述,我们会在源码分析部分详细展开,这里不做赘述:
sentinel monitor master1 127.0.0.1 6379 2
sentinel down-after-milliseconds master1 30000
sentinel parallel-sync master1 1
sentinel failover-timeout master1 900000
sentinel monitor master2 127.0.0.1 2345 5
sentinel down-after-milliseconds master2 50000
sentinel parallel-sync master2 5
sentinel failover-timeout master2 450000
2.2 哨兵节点获取被监听对象信息
在加载完配置文件,sentinel获取到要监视的主服务器,会为每个主服务器都创建两个异步的网络连接:命令连接和订阅连接。前者主要用来向主服务器发送命令和接收命令回复,后者用来专门订阅主服务的__sentinel__:hello 频道,这个频道的作用主要用来感知和通知其他sentinel或自身sentinel的存在。
Redis是如何区分一个实例是主服务器、从服务器和sentinel的? 主要看flags字段,SRI_MASTER、SRI_SLAVE、SRI_SENTINEL
2.2.1 获取主服务器信息及下属所有从服务器。
创建完网络连接,sentinel和主服务器就可以通信了。sentinel默认以每10秒一次的频率,通过命令连接想被监视的主服务器发送INFO命令,通过INFO命令的回复获取主服务器的当前信息。这些信息包括两方面:(1)主服务器自身的信息,如自己的run_id 和角色(主/从/sentinel);(2)该主服务器属下所有的从服务器的信息(ip、port 在线状态、复制偏移等);
对于获取的从服务器的信息会被用来更新所属主服务器示例结构的slaves字典,这个字典保存主服务器所有的从服务器信息,字典的key是sentinel自动设置的名字(格式为:ip+port),字典的值为从服务器对应的实例结构。sentinel在分析从服务器的信息时,如果存在,则更新该实例结构,如果从服务器的实例不存在,则说明该从服务器是新发现的,会在slaves字典中为这个从服务器创建新的实例,并且为该从服务器创建两个异步连接:命令连接和订阅连接。在命令连接创建完成后,sentinel默认会以10秒一次的频率向从服务器发送INFO命令,获取从服务器的信息,类似下面的回复:
run_id:32be0699dd27b410fc790da3a6fab17f97899f //从服务器的运行id
...
role:slave //从服务器的角色role
master_host:127.0.0.1 //主服务器的ip
master_port:6379 //主服务器的port
master_link_status:up //主从服务器的连接状态
slave_repl_offset:11887 //从服务器的复制偏移量
slave_priority:100 //从服务器的优先级
...
2.2.2 向主服务器和从服务器发送信息
sentinel默认会以每两秒一次的频率,通过命令连接向所有被监视的主、从服务器的__sentinel_:hello频道发送以下命令:
PUBLISH __sentinel:hello "<s_ip>,<s_port>,<s_run_id>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
命令说明: 所有以s_开头的参数记录的是sentinel本身的信息;以m_开头的参数表示的是主服务器的信息,如果sentinel监视的是主服务器,那么这些参数记录的就是主服务器的信息,如果监视的是从服务器,这些参数记录的是从服务器正在复制的主服务器的信息。
这个命令有什么用呢?当sentinel与一个主服务器或者从服务器建立起订阅连接后,sentinel就会通过订阅连接,向服务器发送:
SUBSCRIBE _sentinel:hello
也就是说监视了同一主服务器sentinel都订阅了该频道。当一个sentinel往该频道里发送消息是,所有的sentinel都能收到该条消息,包括自身。收到消息的sentinel会根据s_runid来判断是不是自己发出的消息,如果是自己发的就丢弃;如果是别的sentinel发的,更新相应的主服务器实例结构,具体来说就是:收到消息的sentinel会在自己的sentinel状态的masters字典中查找对应的主服务器实例,再在主服务的实例对应的sentinels字典中查找有没有发消息的sentinel的信息,如果有就更新sentinel的实例结构,如果没有说明该sentinel是刚开始监听主服务器的sentinel,为其创建sentinel结构添加到主服务器的sentinels字典中。该字典保存所有监视同一服务器的sentinel。
sentinel可以通过接收频道的信息来感知其他sentinel的存在,并通过发送频道信息让其他sentinel知道自己的存在。
当sentinel通过频道信息发现了一个新的sentinel时,不仅会在自己的sentinels字典中创建相应的实例,还会创建连向新sentinel的命令连接,而新的sentinel同样会创建连向这个sentinel的命令连接。sentinel之间通过命令连接进行接下来的一系列操作(主客观下线、选举等)。
三、sentinel工作流程
哨兵模式的工作流程可以分为五个步骤:主观下线、客观下线、哨兵选举、从库选主和故障转移
3.1 主观下线
主观下线:就是某个sentinel认为某个实例下线了,就是单纯的“我以为”,还要经过其他的sentinel的认同,才会进入“客观下线”,也就是大家都认为。
sentinel默认会以每秒一次的频率向所有与自己创建了命令连接的实例(主从、其他sentinel)发送PING命令,通过返回值来判断某个实例是否下线。在配置文件中有个参数down-after-milliseconds,这个参数指定sentinel判断一个实例主观下线的时间长度。如果一个实例在down-after-milliseconds毫秒内,连续想sentinel返回了无效回复,sentinel就会修改该实例对应的flags子弹,打开SRI_S_DOWN标识,以此标记这个实例进入主观下线状态。
回复结果:
- 有效回复: +PONG -LOADING -MASTERDOWN 三种;
- 无效回复:除有效回复外的所有回复
假设用户配置如下:
sentinel monitor master 127.0.0.1 6379 2
sentinel down-after-milliseconds master 50000
这50000毫秒不仅是sentinel判断master进入主观下线的标准,也会是sentinel判断master属下所有从服务器和所有监视该master的其他sentinel进入主观下线的标准。
注意:多个sentinel设置的主观下线时长可能不同,需要查看具体的配置文件。
3.2 客观下线
当一个sentinel认为一个主服务器为主观下线后,为了确保这个主服务器是不是真的下线了,需要向监视了这个主服务器的其他sentinel询问。当收到足够数量的下线信息后,sentinel会将主服务器判定为客观下线,设置flags的SRI_O_DOWN标识,并准备对主服务器进行故障转移操作。
通过发送 SENTINEL is-mater-down-by-addr 命令询问其他sentinel
SENTINEL is-mater-down-by-addr <ip> <port> <current_epoch> <run_id>
当认为主服务器已经下线状态的sentinel的数量(包括自己),超过sentinel配置中设置的quorum参数的值,那么该sentinel就任务主服务器已经进入客观下线。
// quorum 参数设置
sentinel monitor master 127.0.0.1 6379 2
不同的sentinel判断客观下线的条件也可能不同。
3.3 领头sentinel的选举
选举规则:
- 监视同一个主服务器的任意一个sentinel都有可能成为领头sentinel。
- 每次选举完领头sentinel后。不论选举是否成功,所有sentinel的配置纪元会自增1。配置纪元实际上就是个计数器,没什么特别的;
- 在一个配置纪元里,所有的sentinel都有一次能将某个sentinel设置为局部领头sentinel的机会,且机会只有一次,先到先得。
- 如果在给定时间内,没有一个sentinel被选为领头sentinel,那么各个sentinel会在一段时间后,再次进行选举。配置纪元就是选举的任期。
选举流程:
- 当一个sentinel(源sentinel)向另一个sentinel(目标sentinel)发送 SENTINEL is-master-down-by-addr 命令,并且命令中的参数不是*,而是自己的run_id,表示源sentinel要求目标sentinel将自己设置为它的局部领头sentinel。
- 目标sentinel在收到SENTINEL is-master-down-by-addr 命令后,回复中的leader_runid 参数和leader_epoch 参数分别记录自己的局部领头sentinel的运行ID和配置纪元;
- 源sentinel在收到消息后,检查leader_epoch 是不是和自己的配置纪元一致,如果一致,取出 leader_runid 与自己的 run_id 比较,如果一致,表示目标sentinel 同意选自己为局部领头sentinel,如果不一致,说明目标sentinel已经选别的sentinel为局部领头sentinel了
- 如果某个sentinel被半数以上的sentinel设置为局部领头sentinel,那么这个局部领头sentinel就称为领头sentinel。
3.4 从库选主和故障转移
在选举产生领头sentinel后,领头的sentinel将对已下线的主服务器执行故障转移操作,该操作分为三步:
- 在已下线的主服务器属下的从服务器里选一个服务器,将其转为主服务器(SLAVEOF no one)
- 让其他从服务器改复制新的主服务器
- 将已下线的主服务器设置为新的主服务器的从服务器,当旧的主服务器重新上线时就会称为新的主服务器的从服务器。
在发送SLAVEOF no one命令后,领头的sentinel会以每秒一次的频率发送INFO,检查角色(role)信息,直至变为master,就顺利将从服务器升级为主服务器了。
修改从服务器的复制目标 slaveof <new_master_ip> <new_master_port> 让从服务器复制新的额主服务器。
四、哨兵模式源码分析
sentinelState 定义:
struct sentinelState {
char myid[CONFIG_RUN_ID_SIZE+1];
uint64_t current_epoch;
dict *masters;
int tilt;
int running_scripts;
mstime_t tilt_start_time;
mstime_t previous_time;
list *scripts_queue;
char *announce_ip;
int announce_port;
unsigned long simfailure_flags;
int deny_scripts_reconfig;
char *sentinel_auth_pass;
char *sentinel_auth_user;
int resolve_hostnames;
int announce_hostnames;
} sentinel;
4.1 什么是TILT?
TITL:因为sentinel依赖本机时间驱动,如果系统时间出问题,或者因为进程阻塞导致的时间函数延迟调用。这时再去参与集群逻辑会出现不正确的决策。因此如果当前时间和上一次执行时间差为负值或者超过2s,该节点会进入TILT模式。
4.2 sentinelRedisInstance结构定义:
每一个sentinelRedisInstance表示一个被sentinel监视的Redis服务器实例。这个实例可以使主服务器、从服务器或者另一个sentinel实例。
typedef struct sentinelRedisInstance {
int flags;
char *name;
char *runid;
uint64_t config_epoch;
sentinelAddr *addr;
instanceLink *link;
mstime_t last_pub_time;
mstime_t last_hello_time;
mstime_t last_master_down_reply_time;
mstime_t s_down_since_time;
mstime_t o_down_since_time;
mstime_t down_after_period;
mstime_t info_refresh;
dict *renamed_commands;
int role_reported;
mstime_t role_reported_time;
mstime_t slave_conf_change_time;
dict *sentinels;
dict *slaves;
unsigned int quorum;
int parallel_syncs;
char *auth_pass;
char *auth_user;
mstime_t master_link_down_time;
int slave_priority;
int replica_announced;
mstime_t slave_reconf_sent_time;
struct sentinelRedisInstance *master;
char *slave_master_host;
int slave_master_port;
int slave_master_link_status;
unsigned long long slave_repl_offset;
char *leader;
uint64_t leader_epoch;
uint64_t failover_epoch;
int failover_state;
mstime_t failover_state_change_time;
mstime_t failover_start_time;
mstime_t failover_timeout;
mstime_t failover_delay_logged;
struct sentinelRedisInstance *promoted_slave;
char *notification_script;
char *client_reconfig_script;
sds info;
} sentinelRedisInstance;
|