Redis Sentinel
文章目录
1. 基本概念
1.1 背景:主从复制的问题
Redis 的主从复制模式可以将主节点的数据改变同步给从节点,这样从节点就可以起到两个用:
- 第一,作为主节点的一个备份,一旦主节点出了故障不可达的情况,从节点可以作为后备“顶” 上来,并且保证数据尽量不丢失(主从复制是最终一致性)。
- 第二,从节点可以扩展主节点的读能力,一旦主节点不能支撑住大并发量的读操作,从节点可以在一定程度上帮助主节点分担读压力。
主从复制存在以下问题:
- (1)一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预。
- (2)主节点的写能力受到单机的限制。
- (3)主节点的存储能力受到单机的限制。
问题(1)是Redis的高可用问题,可用Redis Sentinel解决;问题(2)、(3)Redis的分布式问题,可用Redis的集群解决。
1.2 Redis Sentinel 的高可用性
名词 | 逻辑结构 | 物理结构 |
---|---|---|
主节点(master) | Redis 主服务/数据库 | 一个独立的Redis进程 |
从节点(slave) | Redis 从服务/数据库 | 一个独立的Redis进程 |
Redis 数据节点 | 主节点和从节点 | 主节点和从节点的进程 |
Sentinel 节点集合 | 若干Sentinel 节点的抽象组合 | 若干Sentine 节点进程 |
Redis Sentinel | Redis高可用实现方案 | Sentinel 节点集合和Redis 数据节点进程 |
Redis Sentinel 是一个分布式架构(这里的分布式是指:Redis 数据节点、Sentinel 节点集合、客户端分布在多个物理节点的架构),其中包含若干个 Sentinel 节点和 Redis 数据点,每个 Sentinel 节点会对数据节点和其余 Sentinel 节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他 Sentinel 节点进行“协商”,当 大多数 Sentinel 节点都认为主节点不可达时,它们会选举出一个 Sentinel 节点来完成自动 故障转移的工作,同时会将这个变化实时通知给 Redis 应用方。整个过程完全是自动的,不需要人工来介入,所以这套方案很有效地解决了 Redis 的高可用问题。
Redis Sentinel 具有以下几个功能:
- 监控(Monitoring):Sentinel 节点会定期检测 Redis 数据节点(包括master和slave)、其余 Sentinel 节点是否可达。
- 通知(Notification):Sentinel 节点会将故障转移的结果通知给应用方。
- 自动故障迁移(Automatic failover):当master不能正常工作时,Sentinel会实现从节点晋升为主节点并维护后续正确的主从关系。—If a master is not working as expected, Sentinel can start a failover process where a slave is promoted to master, the other additional slaves are reconfigured to use the new master, and the applications using the Redis server informed about the new address to use when connecting.
- 配置提供者(Configuration provider.):在 Redis Sentinel 结构中,客户端 在初始化的时候连接的是 Sentinel 节点集合,从中获取主节点信息。—Sentinel acts as a source of authority for clients service discovery: clients connect to Sentinels in order to ask for the address of the current Redis master responsible for a given service. If a failover occurs, Sentinels will report the new address.
2. Redis Sentinel的部署
下面将以 3 个 Sentinel 节点、1 个主节点、2 个从节点组成一个 Redis Sentinel 进行 说明,并且使用了简单的加密,拓扑结构如下图所示。
2.1 部署 Redis 的master和slave节点
启动master
- 配置
redis-6379.conf 文件
port 6379
daemonize yes
logfile "/data/lincoln/redis-stable/mydata/6379.log"
dbfilename "dump-6379.rdb"
dir "/data/lincoln/redis-stable/mydata"
#密码
requirepass "My-secret-pass"
masterauth "My-secret-pass"
- 启动master
$ ./redis-server ../mydata/redis-6379.conf
- 验证是否启动
$ ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> AUTH My-secret-pass
OK
127.0.0.1:6379> PING
PONG
启动slave
- 配置
两个从节点的配置是完全一样的,和主节点的配置不一样的是添加了 slaveof 配置。
port 6380
daemonize yes
logfile "/data/lincoln/redis-stable/mydata/6380.log"
dbfilename "dump-6380.rdb"
dir "/data/lincoln/redis-stable/mydata"
requirepass "My-secret-pass"
masterauth "My-secret-pass"
#增加了slaveof配置,其余和主节点一样
slaveof 127.0.0.1 6379
- 启动节点
$ ./redis-server ../mydata/redis-6380.conf
$ ./redis-server ../mydata/redis-6381.conf
- 验证是否启动
$ ./redis-cli-h 127.0.0.1 -p 6380 #也是需要AUTH验证密码的,以下省略
127.0.0.1:6380> PING
PONG
$ ./redis-cli-h 127.0.0.1 -p 6381
127.0.0.1:6381> PING
PONG
确认主从关系
连接主节点查看:
$ ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=154,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=154,lag=0
...
连接从节点查看:
$ ./redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
...
此时的拓扑图如下所示:
2.2 部署Sentinel节点
3 个 Sentinel 节点的部署方法是完全一致的(端口不同)。
- 配置
port 26379
daemonize yes
dir "/data/lincoln/redis-stable/mydata"
logfile "/data/lincoln/redis-stable/mydata/26379.log"
#当前Sentinel节点监控 127.0.0.1:6379 这个主节点
#2代表判断主节点失败至少需要2个Sentinel节点节点同意
#mymaster是主节点的别名
sentinel monitor mymaster 127.0.0.1 6379 2
#每个Sentinel节点都要定期PING命令来判断Redis数据节点和其余Sentinel节点是否可达,如果超过30000毫秒且没有回复,则判定不可达
sentinel down-after-milliseconds mymaster 30000
#当Sentinel节点集合对主节点故障判定达成一致时,Sentinel领导者节点会做故障转移操作,选出新的主节点,原来的从节点会向新的主节点发起复制操作,限制每次向新的主节点发起复制操作的从节点个数为1
sentinel parallel-syncs mymaster 1
#故障转移超时时间为180000毫秒
sentinel failover-timeout mymaster 180000
#master的密码
sentinel auth-pass mymaster "My-secret-pass"
- 启动sentinel
#法1:
$ ./redis-sentinel ../mydata/sentinel-26379.conf
#法2:
$ ./redis-server ../mydata/sentinel-26379.conf --sentinel
#启动其余两个sentinel
$ ./redis-sentinel ../mydata/sentinel-26380.conf
$ ./redis-sentinel ../mydata/sentinel-26381.conf
- 验证是否启动:
$ ./redis-cli -h 127.0.0.1 -p 26379
127.0.0.1:26379> info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
2.3 Sentinel配置项说明
(1)sentinel monitor
配置如下:sentinel monitor <master-name> <ip> <port> <quorum>
- Sentinel 节点会定期监控主节点,所以从配置上必然也会有所体现,本配置说明 Sentinel 节点要监控的是一个名字叫做,ip 地址和端口为 的主节点。 代表要判定主节点最终不可达所需要的票数。
- 还与 Sentinel 节点的领导者选举有关,至少要有 max(quorum,num(sentinels)/2+1)个 Sentinel 节点参与选举,才能选出领导者 Sentinel,从而完成故障转移。
实际上 Sentinel 节点会对所有节点 进行监控,但是在 Sentinel 节点的配置中没有看到有关从节点和其余 Sentinel 节点的置, 那是因为 Sentinel 节点会从主节点中获取有关从节点以及其余 Sentinel 节点的相关信息。
(2)sentinel down-after-milliseconds
配置如下:sentinel down-after-milliseconds <master-name> <times>
- 每个 Sentinel 节点都要通过定期发送 ping 命令来判断 Redis 数据节点和其余 Sentinel 节点是否可达,如果超过了 down-after-milliseconds 配置的时间且没有有效的回复,则判定节点不可达.
- down-after-milliseconds 虽然以 为参数,但实际上对 Sentinel 节点、主节点、从节点的失败判定同时有效。
(3)sentinel parallel-syncs
配置如下:sentinel parallel-syncs <master-name> <nums>
- 当 Sentinel 节点集合对主节点故障判定达成一致时,Sentinel 领导者节点会做故障转移 操作,选出新的主节点,原来的从节点会向新的主节点发起复制操作,parallel-syncs 是 用来限制在一次故障转移之后,每次向新的主节点发起复制操作的从节点个数。
(4)sentinel failover-timeout
(5)sentinel auth-pass
配置如下:sentinel auth-pass <master-name> <password>
- 如果Sentinel监控的主节点配置了密码,可以通过sentinel auth-pass配置通过添加主节点的密码,防止Sentinel节点无法对主节点进行监控。
- 例如:sentinel auth-pass mymaster MySUPER–secret-0123passw0rd
(6)sentinel notification-script
配置如下:sentinel notification-script <master-name> <script-path>
- 在故障转移期间,当一些警告级别的Sentinel事件发生(指重要事件,如主观下线,客观下线等)时,会触发对应路径的脚本,向脚本发送相应的事件参数。
- 例如:sentinel notification-script mymaster /var/redis/notify.sh
(7)sentinel client-reconfig-script
配置如下:sentinel client-reconfig-script <master-name> <script-path>
- 在故障转移结束后,触发应对路径的脚本,并向脚本发送故障转移结果的参数。
- 例如:sentinel client-reconfig-script mymaster /var/redis/reconfig.sh。
3. Sentinel API
Sentinel命令sentinel支持的合法命令如下:
- PING :sentinel回复PONG.
- SENTINEL masters:显示被监控的所有master以及它们的状态.
- SENTINEL master :显示指定master的信息和状态;
- SENTINEL slaves :显示指定master的所有slave以及它们的状态;
- SENTINEL get-master-addr-by-name :返回指定master的ip和端口,如果正在进行failover或者failover已经完成,将会显示被提升为master的slave的ip和端口。
- SENTINEL reset :重置名字匹配该正则表达式的所有的master的状态信息,清楚其之前的状态信息,以及slaves信息。
- SENTINEL failover :强制sentinel执行failover,并且不需要得到其他sentinel的同意。但是failover后会将最新的配置发送给其他sentinel。
4. 客户端使用代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <vector>
#include "hiredis.h"
using namespace std;
const string passWord = "My-secret-pass";
struct IpPort
{
string ip;
int port;
};
//通过Sentinel获得master的ip和port
int getMasetAddr(const vector<IpPort> &addVec, struct timeval timeOut,string& masterIp, string& masterPort)
{
//建立和sentinel的连接
redisContext* context = NULL;
redisReply *reply = NULL;
for(int i = 0; i < addVec.size(); ++i)
{
printf("i[%d], ip[%s], port[%d]", i, addVec[i].ip.c_str(), addVec[i].port);
context = redisConnectWithTimeout(addVec[i].ip.c_str(), addVec[i].port, timeOut);
if (context == NULL || context->err)
{
printf("Connection error: can't allocate redis context,will find next");
redisFree(context);//断开连接并释放redisContext空间
continue;
}
//获取master的ip和port
reply = static_cast<redisReply*> ( redisCommand(context,"SENTINEL get-master-addr-by-name mymaster") );
if(reply->type != REDIS_REPLY_ARRAY || reply -> elements != 2)
{
printf("use sentinel to get-master-addr-by-name failure, will find next\n");
freeReplyObject(reply);
continue;
}
masterIp = reply -> element[0] -> str;
masterPort = reply -> element[1] -> str;
break;
}
if(masterIp.empty() || masterPort.empty())
return -1;
return 0;
}
int main(int argc, char **argv) {
unsigned int j;
redisContext *context;
redisReply *reply;
vector<IpPort> addrVec = { {"127.0.0.1",26379}, {"127.0.0.1",26380}, {"127.0.0.1",26381} };
string master_ip("");
string master_port("");
struct timeval timeout = { 1, 500000 }; // 1.5 seconds
if(getMasetAddr(addrVec, timeout, master_ip, master_port) != 0)
{
printf("get master name and port failed!");
exit(1);
}
printf("master ip [%s], port[%s]",master_ip.c_str(), master_port.c_str());
//建立master连接
context = redisConnectWithTimeout(master_ip.c_str(), strtoul(master_port.c_str(), NULL, 0), timeout);
if (context == NULL || context->err)
{
if (context)
{
printf("Connection error: %s\n", context->errstr);
redisFree(context);
}
else
{
printf("Connection error: can't allocate redis context\n");
}
exit(1);
}
//密码验证
reply = static_cast<redisReply*> ( redisCommand(context,"AUTH %s", passWord.c_str()) );
printf("redis AUTH result: %s\n",reply -> str);
if(NULL == reply || strcmp(reply->str, "OK") != 0 )
{
printf("redis AUTH failure");
freeReplyObject(reply);
return -1;
}
/* Set a key using binary safe API */
string binary_key = "bar";
string binary_str ("hell \0 world!!!",15);
reply = static_cast<redisReply*> ( redisCommand(context,"SET %s %b", binary_key.c_str(), binary_str.c_str(), binary_str.size() ) );
if(NULL == reply)
{
printf("command execute set key failure\n");
redisFree(context);
exit(1);
}
//返回执行结果为状态的命令。比如set命令的返回值的类型是REDIS_REPLY_STATUS,然后只有当返回信息是"OK"时,才表示该命令执行成功。
if( !(reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "OK") == 0))
{
printf("command execute set key failure\n");
freeReplyObject(reply);
redisFree(context);
exit(1);
}
printf("SET (binary API): %s , vaule size[%d]\n", reply->str, binary_str.size() );
freeReplyObject(reply);
//get binary key
reply = static_cast<redisReply*> ( redisCommand(context,"GET %s", binary_key.c_str()) );
if(reply->type != REDIS_REPLY_STRING)
{
printf("command execute get key [%s] failure\n", binary_key.c_str());
freeReplyObject(reply);
redisFree(context);
exit(1);
}
string binary_result(reply -> str, reply -> len);
cout<<"GET "<<binary_key << ":"<< binary_result <<"value size :"<< binary_result.size()<<endl;
freeReplyObject(reply);
/* Set a key */
//不推荐使用%s,此时binary_str在存储的时候遇到'\0'就已经发生截断了
reply = static_cast<redisReply*> ( redisCommand(context,"SET %s %s", "foo", binary_str.c_str()) );
if(NULL == reply)
{
printf("command execute set key failure\n");
redisFree(context);
exit(1);
}
if(!(reply->type == REDIS_REPLY_STATUS && strcmp(reply->str, "OK") == 0))
{
printf("command execute set key failure\n");
freeReplyObject(reply);
redisFree(context);
exit(1);
}
printf("SET: %s\n", reply->str);
freeReplyObject(reply);
//Get a key
reply = static_cast<redisReply*> ( redisCommand(context,"GET foo") );
if(reply->type != REDIS_REPLY_STRING)
{
printf("command execute get key foo failure\n");
freeReplyObject(reply);
redisFree(context);
exit(1);
}
string foo_result(reply -> str, reply -> len);
cout<<"GET foo :"<< foo_result <<"value size :"<< foo_result.size()<<endl;
freeReplyObject(reply);
/* Disconnects and frees the context */
redisFree(context);
return 0;
}
5. Redis Sentinel的基本实现原理
本节将介绍 Redis Sentinel 的基本实现原理,具体包含以下几个方面:Redis Sentinel 的三个定时任务、主观下线和客观下线、Sentinel 领导者选举、故障转移。
5.1 三个定时监控任务
Redis Sentinel 通过三个定时监控任务完成对各个节点发现和监控:
1)每隔 10 秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构。
这个定时任务的作用具体可以表现在三个方面:
- 通过向主节点执行 info 命令,获取从节点的信息,这也是为什么 Sentinel 节点不需要 显式配置监控从节点。
- 当有新的从节点加入时都可以立刻感知出来。
- 节点不可达或者故障转移后,可以通过 info 命令实时更新节点拓扑信息。
2) 每隔 2 秒,每个 Sentinel 节点会向 Redis 数据节点的__sentinel__:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点 的信息,同时每个Sentinel 节点也会订阅该频道,来了解其他 Sentinel 节点以及它们对主节点的判断,所以 这个定时任务可以完成以下两个工作:
- 发现新的 Sentinel 节点:通过订阅主节点的__sentinel__:hello 了解其他的 Sentinel 节点信息,如果是新加入的 Sentinel 节点,将该 Sentinel 节点信息保存 起来,并与该 Sentinel 节点创建连接。
- Sentinel 节点之间交换主节点的状态,作为客观下线以及领导者选举的依据。
3)每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点当前是否可达。
5.2 主观下线和客观下线
第三个定时任务,每个 Sentinel 节点会每隔 1 秒对主节点、从节点、其他 Sentinel 节点 发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds 没有进行有效回复,Sentinel 节点就会对该节点做失败判定,这个行为叫做主观下线(Subjectively Down,简称 SDOWN)。
当 Sentinel 主观下线的节点是主节点时,该 Sentinel 节点会通过 sentinel is-master-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断,当超过 个数,Sentinel 节点认为主节点确实有问题,这时该 Sentinel 节点会做出**客观下线(Objectively Down,简称 ODOWN)**的决定。
- sentinel is-master-down-by-addr 命令:
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
- 参数
- ip:主节点 IP。
- port:主节点端口。
- current_epoch:当前配置纪元。
- runid:此参数有两种类型,不同类型决定了此 API 作用的不同。
- 当 runid 等于“*” 时,作用是 Sentinel 节点直接交换对主节点下线的判定。
- 当 runid 等于当前 Sentinel 节点的 runid 时,作用是当前 Sentinel 节点希望目标 Sentinel 节点同意自己成为领导者的请求。
- 返回结果参数:
- down_state:目标 Sentinel 节点对于主节点的下线判断,1 是下线,0 是在线。
- leader_runid:当 leader_runid 等于“*” 时,代表返回结果是用来做主节点是否 不可达,当 leader_runid 等于具体的 runid,代表目标节点同意 runid 成为领导者。
- leader_epoch:领导者纪元。
5.3 Sentinel 领导者选举
当 Sentinel 节点对于主节点已经做了客观下线,Sentinel 节点之间会做一个领导者选举的 工作,选出一个 Sentinel 节点作为 领导者进行故障转移的工作。Redis 使用了 Raft算法 实现领导者选举。
- 1)每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点主观下线时候,会向 其他 Sentinel 节点发送 sentinel is-master-down-by-addr 命令,要求将自己设置 为领导者。
- 2)收到命令的 Sentinel 节点,如果没有同意过其他 Sentinel 节点的 sentinel is- master-down-by-addr 命令,将同意该请求,否则拒绝。
- 3)如果该 Sentinel 节点发现自己的票数已经大于等于 max(quorum, num(sentinels/2+ 1),那么它将成为领导者。
- 4)如果此过程没有选举出领导者,将进入下一次选举。
5.4 故障转移
领导者选举出的 Sentinel 节点负责故障转移,具体步骤如下:
- 1)在从节点列表中选出一个节点作为新的主节点,选择方法如下:
- a)过滤:“不健康”(主观下线、断线)、5 秒内没有回复过 Sentinel 节点 ping 响应、与主节点失联超过 down-after-milliseconds*10 秒。
- b)选择 slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
- c)选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。
- d)选择 runid 最小的从节点。
- 2)Sentinel 领导者节点会对第一步选出来的从节点执行 slaveof no one 命令让其成为 主节点。
- 3)Sentinel 领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制 规则和 parallel-syncs 参数有关。
- 4)Sentinel 节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。
参考
- 官网:https://redis.io/topics/sentinel
- 《redis开发与运维》