redis-哨兵
一、基本概念和架构
1.1 基本概念
哨兵,Redis sentinel,在主从复制的基础上实现故障恢复的自动化。其核心功能是主节点(master)的自动故障转移。
主要功能:
- 监控(Monitor):哨兵不断检查主节点和从节点是否正常工作。
- 自动故障转移(Automatic failover):主节点不正常时,哨兵启动自动故障转移,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点从这个新的主节点复制数据。
- 配置提供者(Configuration provider):客户端可以通过哨兵来获取主节点地址。
- 通知(Notification):哨兵可以把故障转移结果通知给客户端。
1.2 架构
它由两部分组成,哨兵节点和数据节点:
- 哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。
- 数据节点:主节点和从节点都是数据节点。
redis sentinel是一个分布式的架构,其中包含若干个sentinel节点,对于节点的故障判断是由多个独立的sentinel节点共同完成,这样可以有效防止误判。多个sentinel节点使得即使个别sentinel节点不可用,整个sentinel节点集合依然是健壮的。
二、部署演示
整体部署在一台机器上(192.168.118.129 ),通过不同的端口号来区分主从节点以及哨兵节点。
2.1 部署主从节点
部署一台主节点(端口号为6379),2台从节点(端口号分别为6380和6381)。
把6380配置为6379的slave。
把6381配置为6379的slave。
查看master节点的复制信息
2.2 部署哨兵节点
配置三个哨兵节点,和主从在同一台机器上(192.168.118.129),用三个不同的端口号来区分(分别为26379,26380,26381)。
哨兵节点是特殊的redis节点,有特殊的配置,针对每一个哨兵分别建立一个配置文件。
以26379哨兵为例,建立sentinel26379.conf配置文件,基本配置如下:
sentinel monitor mymaster 193.168.129 6379 2
这句是哨兵节点的核心配置,代表这个哨兵监控193.168.129: 6379这个master节点,且命名为mymaster。2代表至少有两个哨兵同意才能认定主节点产生故障并且进行故障转移。
按照上述步骤配置sentinel26380.conf和sentinel26381.conf。至此,三个哨兵节点配置完成。
接下来启动哨兵节点。有两种方法,两个方法等效
-
src/redis-sentinel sentinel26379.conf
-
src/redis-server sentinel26379.conf --sentinel
一次启动三个哨兵,启动后可以查看。
启动后哨兵节点会发现主节点,以及主节点对应的salve节点,同时哨兵节点会发现彼此。哨兵节点启动后最终的结构如下。
2.3 故障转移演示
演示主节点down机后哨兵进行自动故障转移的case。目前主节点是6379节点。
杀掉主节点的进程
迁移
主节点已经被迁移成了6380节点。
转移后配置文件也被自动做了修改。
三、客户端连接(配置提供者)
哨兵作为配置的提供者,客户端可以直接基于哨兵进行连接,而无需直到具体主节点的ip,这样实现了和主节点的解耦,当主节点down机后,哨兵重新选主,对客户端透明。
3.1 代码
public static void main(String[] args)
{
String masterName="mymaster";// 主节点名字
Set<String> sentinels=new HashSet<>();//哨兵集合
sentinels.add("192.168.118.129:26379");
sentinels.add("192.168.118.129:26380");
sentinels.add("192.168.118.129:26381");
JedisSentinelPool jedisSentinelPool=new JedisSentinelPool(masterName,sentinels);
Jedis jedis= jedisSentinelPool.getResource();
jedis.set("name","cnblogs-new");
System.out.println(jedis.get("name"));
jedisSentinelPool.close();
}
执行结果:
[main] INFO redis.clients.jedis.JedisSentinelPool - Trying to find master from available Sentinels...
[main] INFO redis.clients.jedis.JedisSentinelPool - Redis master running at 192.168.118.129:6380, starting Sentinel listeners...
[main] INFO redis.clients.jedis.JedisSentinelPool - Created JedisPool to master at 192.168.118.129:6380
cnblogs-new
可以看到通过哨兵找到了master节点。
3.2 原理
核心实现在JedisSentinelPool
的构造函数中
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
log.info("Trying to find master from available Sentinels...");
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
log.debug("Connecting to Sentinel {}", hap);
jedis = new Jedis(hap.getHost(), hap.getPort(), sentinelConnectionTimeout, sentinelSoTimeout);
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName); //查找master
master = toHostAndPort(masterAddr);
return master;
}
}
通过代码可以发现:
-
遍历哨兵节点
-
连接哨兵节点
-
通过哨兵节点获取master节点
sentinelGetMasterAddrByName
。改方法内部调用了redis的命令来实现sentinel get-master-addr-by-name mymaster
上述代码省略了一部分,订阅。
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
为每一个sentinel节点独立启动一个线程,利用redis的发布订阅功能,每个线程订阅sentinel节点上切换master的相关频道+switch-master
public void onMessage(String channel, String message) {
String[] switchMasterMsg = message.split(" ");
if (masterName.equals(switchMasterMsg[0])) {
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
log.debug("Ignoring message on +switch-master for master name {}, our master name is {}",switchMasterMsg[0], masterName);
}
}
发现当前master发生了switch,使用initPool重新初始化连接池。
四、实现原理
4.1 三个定时任务
通过监控来判断节点是否不可达,sentinel通过三个定时任务来完成后对各个节点的监控和发现。
Sentinel节点定时执行info命令来获取拓扑信息
每隔10秒执行
- 通过向master 执行info可以拿到从节点的信息(这就解释了为什么sentinel没有显示配置从节点,但是可以监发现从节点)。
- 从节点加入,可以自动感知。
- 节点不可达或者故障转移后,可以通过info实时拿到拓扑信息。
Sentinel节点发布和订阅 _sentinel_hello频道
每隔两秒执行
每隔2秒,每隔sentinel节点会向master的_sentinel_hello 频道发送自身的节点信息,以及自己对master状态的判断,同时每个sentinel也会订阅改频道,最 终实现了sentinel节点之间的互相发现以及彼此交换自己对主节点状态的判断,作为客观下线的依据。
Sentinel节点向其余节点发送ping命令
每隔1秒执行
实现对每个节点的监控,这个定时任务是节点失败判定的重要依据。
4.2 主观下线和客观下线
一个参数: down-after-milliseconds
# sentinel down-after-milliseconds <master-name> <milliseconds>
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
主观下线
上一节里面的ping操作,如果超过 down-after-milliseconds
没有得到回复,sentinel会把改节点做失败判断,叫做主观下线。就是当前sentinel一家认为这个节点是失败的,有可能有误判。
客观下线
当sentinel对master做了主观下线后会通过命令: sentinel is-master-down-by-addr
来询问其他sentinel 节点对主节点是否下线的判断。当超过
总结举例
master相当于一个嫌疑人,在审判,法律规定必须要3(quorum)个陪审员都认为有罪,才会判断为有罪。当任何一个陪审员认为嫌疑人有罪的时候,就问其他陪审员的意见,如果最终汇总下来发现有3个陪审员认为有罪,则最终才会判断master有罪,通过多个确认来减少误判的可能。
客观下线是针对master节点的,slave节点只需要主观下线即可。
4.3 领导者Sentinel节点选举
当最终对主节点进行客观下线后,并不是马上进行故障转移,而是进入领导者sentinel节点的选举,因为最终的故障转移由一个sentinel节点来完成,这个节点就是领导者sentinel节点。
领导者选取使用的算法是raft算法,(参考2),是一个共识算法,大致思路如下。
- 每个sentinel节点都有资格成为领导者,当它确认主节点主观下线后,向其他sentinel节点发送
is-master-down-by-addr
,要求将自己设置为领导者,当然给自己投了一票 - 其他sentinel收到命令后,如果没有同意过其他sentinel
is-master-down-by-addr
命令,则同意该请求(只投一次同意票)。 - 如果sentinel节点发现自己的票数已经大于或者等于max(quorum, num(sentinels)/2+1),那么它将会成为领导者sentinel节点
- 如果本次没有选举出领导者,将进入下一次选举。
S1最先完成了客观下线。
- 向S2请求,S2第一次收到,恢复YES
- 向S3请求,S3第一次收到,恢复YES
S1得到两票,满足条件,成为leader。
节点 | 发出同意的节点 | 接受同意的节点 |
---|---|---|
S1 | S2,S3 | |
S2 | S1 | |
S3 | S1 |
4.4 故障转移
重新选取master
当领导者sentinel选举完成后,由该sentinel节点完成故障转移。整体流程如下。
-
过滤不健康节点是指主观下线,断线的salve
-
先比较replica_priority, 值越小,优先级越高
-
如果priority相同,则选择复制偏移量最大的节点(最接近原来master的节点,数据最全)
-
如果偏移量相同,则选择runid最小的节点
设置过程
- 领导者sentinel通过执行replicaof no one命令让选举出来的最新节点成为最新的master
- 领导者sentinel向其他从节点发送 replicaof 命令,让其成为最新master的从节点
- 同时将已经下线的原主节点设置为新master的从节点,等其上线后自动成为新的主节点的从节点。