前言:
说到哨兵之前,可以思考一个问题。为什么有哨兵机制?这一点其实可以从主从复制的优缺点作为考虑。主从复制其实有一个致命的弱点就是它是非高可用的,比如说当主节点出现故障时,那么此时我的写入就会出现问题,切换需要重新配,就无法做到高可用。此时其实就可以用到哨兵进制,哨兵自动发现故障,并通过选举算法重新选举新主完成转移和通知实现高可用。
参考资料:
https://redis.io/topics/sentinel
一篇看着太长分为两篇:
《集群之哨兵sentinel初识》和《集群之哨兵sentinel故障转移》。
(一)理解哨兵模式
基础结构
1.哨兵本身是监听者身份,没有存储功能。在整体体系当中,一个sentinel或者一个群sentinels与redis主从体系是有监听和被监听者关系。
2.哨兵有如下一些功能: 监听、通知、自动故障转移、配置提供程序。
1)监听:哨兵会不断检查主服务和从服务是否正常运行。
2)通知:哨兵可以通过API通知到系统管理员或者其他程序,其中一个受监听redis实例出问题。
3)自动转移故障:如果主服务不正常,则哨兵可以启动故障转移,在该从服务升级为主服务。将其该主服务下其他从服务重新配置主服务,并通知使用Redis服务器的应用要使用新地址。
4) 配置提供程序: Sentinel充当客户端服务发现的授权来源, 客户端连接到Sentinels,以询问负责给定服务的当前Redis主服务器的地址。如果发生故障转移,Sentinels将报告新地址。
3.哨兵存在三种交互模式: 哨兵与主服务、哨兵与从服务、哨兵与其他哨兵。
4.哨兵体现中有三个角色: master、slave、sentinel。
(二)配置哨兵模
1.1 sentinel.conf配置说明
# 哨兵sentinel监控的redis主节点的 ip port
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
# 这个数字越小,完成failover所需的时间就越长,
# 但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
# 可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
# 1. 同一个sentinel对同一个master两次failover之间的间隔时间。
# 2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那里同步数据时。
# 3.当想要取消一个正在进行的failover所需要的时间。
# 4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,slaves依然会被正确配置为指向master,
# 但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知相关人员。
# 对于脚本的运行结果有以下规则:
# 若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
# 若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
# 如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
# 一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
# 通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),将会去调用这个脚本,
# 这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信息。调用该脚本时,将传给脚本两个参数,
# 一个是事件的类型,一个是事件的描述。 如果sentinel.conf配置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个
# 路径,并且是可执行的,否则sentinel无法正常启动成功。
# 通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
master-name 节点名称,名称只能是[A-z 0-9]以及.-_内的字符。
quorum 当这些quorum个数sentinel哨兵认为master主节点失联,那么这时客观上认为主节点失联了 。
1.2 启动哨兵模式
如果使用的是redis-sentinel可执行文件(或者您具有指向该redis-server可执行文件的名称的符号链接),则可以使用以下命令行运行Sentinel:
redis-sentinel /path/to/sentinel.conf
否则,您可以直接使用redis-server在Sentinel模式下启动的可执行文件:
redis-server /path/to/sentinel.conf --sentinel
server.c源码中可以体现两种加载模式:
int checkForSentinelMode(int argc, char **argv) {
int j;
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1;
return 0;
}
(三)哨兵源码解析
2.1 初始化哨兵
1) server.c 哨兵初始化
int main(int argc, char **argv) {
//...省略
server.sentinel_mode = checkForSentinelMode(argc,argv); //判断是否为哨兵模式
//...省略
if (server.sentinel_mode) {
initSentinelConfig(); //加载端口
initSentinel(); //初始化哨兵模式,包括命令调用和各种数据结构。
}
//...省略
if (!server.sentinel_mode) { //普通模式
//...省略
} else {
/*
检测哨兵模式是否正常配置
*/
sentinelIsRunning();
}
}
initSentinelConfig函数中为加载哨兵端口,默认端口为26379。initSentinel函数中初始化哨兵模式,包括命令调用和各种数据结构。
initSentinel函数如下:
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},
{"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0}
};
void initSentinel(void) {
unsigned int j;
/* 初始化哨兵命令 */
dictEmpty(server.commands,NULL);
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
int retval;
struct redisCommand *cmd = sentinelcmds+j;
retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
}
/* 初始化各种数据结构. */
sentinel.current_epoch = 0;
sentinel.masters = dictCreate(&instancesDictType,NULL);
sentinel.tilt = 0;
sentinel.tilt_start_time = 0;
sentinel.previous_time = mstime();
sentinel.running_scripts = 0;
sentinel.scripts_queue = listCreate();
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
memset(sentinel.myid,0,sizeof(sentinel.myid));
}
redisCommand结构体数组为哨兵模式支持命令集。哨兵有单独的命令集,只支持ping、sentinel、subscribe、unsubscribe、psubscribe、publish、info、role、client、shutdown命令。
2) server.c 哨兵任务
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//...省略
run_with_period(100) {
if (server.sentinel_mode) sentinelTimer(); //哨兵相关模式下运行任务
}
//...省略
}
serverCron为initServer函数中的注册时间回调。serverCron中调用sentinelTimer方法中执行哨兵模式中的任务。
包括执行定期操作比如PING、分析主服务和从服务的INFO命令、故障转移等等。
2.2 定期操作
周期执行命令函数sentinelSendPeriodicCommands
void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
mstime_t now = mstime();
mstime_t info_period, ping_period;
int retval;
//。。。省略
/* 如果这是一个处于O_DOWN状态的主服务器的从服务器,我们开始发送
它每秒钟都有信息,而不是通常的哨兵信息期周期 */
if ((ri->flags & SRI_SLAVE) &&
((ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS)) ||
(ri->master_link_down_time != 0)))
{
info_period = 1000; //1s
} else {
info_period = SENTINEL_INFO_PERIOD; //10s
}
/* ping通过 down-after-milliseconds 参数可以配置,默认1秒*/
ping_period = ri->down_after_period;
if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD;
/* Send INFO to masters and slaves, not sentinels. */
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
retval = redisAsyncCommand(ri->link->cc,
sentinelInfoReplyCallback, ri, "%s",
sentinelInstanceMapCommand(ri,"INFO"));
if (retval == C_OK) ri->link->pending_commands++;
}
/* 发送ping */
if ((now - ri->link->last_pong_time) > ping_period &&
(now - ri->link->last_ping_time) > ping_period/2) {
sentinelSendPing(ri);
}
/* PUBLISH 发送hello,SENTINEL_PUBLISH_PERIOD为2000则为2s */
if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
sentinelSendHello(ri);
}
}
正常情况下每10秒会获取一次info主和从到。每1秒发送ping命令,ping命令down-after-milliseconds可以配置,每2秒广播一次hello msg。
总结:
(1)哨兵没有存储功能。
(2)哨兵服务支持命令: ping、sentinel、subscribe、unsubscribe、psubscribe、publish、info、role、client、shutdown
(3)哨兵提供功能: 监听、通知、自动故障转移、配置提供程序。
(4)哨兵有两种启动模式:配置或者命令行参数。
(5)哨兵体现中有三个角色: master、slave、sentinel。
(6)周期函数主从非O_DOWN状态下每10s发送一次info命令到主从。
(7)ping命令默认是1s发送一次,可以通过down-after-milliseconds配置。
(8)每2秒广播一次hello msg。