一、哨兵的作用和架构

1、哨兵的作用

在复制的基础上,哨兵实现了自动化的故障恢复。哨兵的核心功能是主节点的自动故障转移。下面是Redis官方文档对于哨兵功能的描述:

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

哨兵的缺陷:写操作无法负载均衡;存储能力受到单机的限制。

2、哨兵的架构

典型的哨兵架构图如下所示:

Redis系列(六) 高可用之哨兵 - 图1

它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。
  • 数据节点:主节点和从节点都是数据节点。

二、哨兵机制(sentinel)的原理

当主节点出现故障时,由redis sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。

1、定时任务

其实整个过程只需要一个哨兵节点来完成,首先使用Raft算法(下文有介绍)实现选举机制,选出一个哨兵节点来完成转移和通知哨兵有三个定时监控任务完成对各节点的发现和监控;定时任务如下:

任务1

每个哨兵节点每10秒会向主节点和从节点发送info命令获取最拓扑结构图。哨兵配置时只要配置对主节点的监控即可,通过向主节点发送info,获取从节点的信息,并当有新的从节点加入时可以马上感知到。
Redis系列(六) 高可用之哨兵 - 图2

任务2

通过发布订阅功能获取其他哨兵节点的信息。每个哨兵节点每隔2秒会向redis数据节点的指定频道上发送该哨兵节点对于主节点的判断以及当前哨兵节点的信息,同时每个哨兵节点也会订阅该频道,来了解其它哨兵节点的信息及对主节点的判断;
Redis系列(六) 高可用之哨兵 - 图3

任务3

每隔1秒每个哨兵会向主节点、从节点及其余哨兵节点发送一次ping命令做一次心跳检测,这个也是哨兵用来判断节点是否正常的重要依据。
Redis系列(六) 高可用之哨兵 - 图4

2、主观下线和客观下线

主观下线: 在心跳检测的定时任务中,当心跳检测时间超过down-after-milliseconds时,哨兵节点则认为该节点错误或下线,这叫主观下线;这可能会存在错误的判断。
Redis系列(六) 高可用之哨兵 - 图5
客观下线: 当主观下线的节点是主节点时,此时该哨兵3节点会通过指令sentinel is-masterdown-by-addr寻求其它哨兵节点对主节点的判断,当超过quorum(法定人数)个数,此时哨兵节点则认为该主节点确实有问题,这样就客观下线了,大部分哨兵节点都同意下线操作,也就说是客观下线。
Redis系列(六) 高可用之哨兵 - 图6

需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。

3、选举领导者哨兵节点

每个在线的哨兵节点都可以成为领导者,当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。选举使用的算法是Raft算法。

Raft算法

Raft算法的基本思路是先到先得:即在一轮选举中,哨兵A向B发送成为领导者的申请,如果B没有同意过其他哨兵,则会同意A成为领导者。一般来说,哨兵选择的过程很快,谁先完成客观下线,一般就能成为领导者。

选举过程

a、当哨兵节点确认(比如哨兵3)主节点下线时,会向其它哨兵发is-master-down-by-addr命令,征求判断并要求将自己设置为领导者,由领导者处理故障转移;
b、当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;
c、如果哨兵3发现自己在选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举…………

4、故障转移机制

a、当主节点出现故障,此时3个Sentinel节点共同选举一个节点(Sentinel3)为领导,负载处理主节点的故障转移。
Redis系列(六) 高可用之哨兵 - 图7
b、由Sentinel3领导者节点执行故障转移,过程和主从复制一样,但是自动执行。
Redis系列(六) 高可用之哨兵 - 图8
流程:

  1. 将slave-1脱离原从节点,升级主节点(选举新主节点下文会介绍)。
  2. 将从节点slave-2指向新的主节点。
  3. 通知客户端主节点已更换。
  4. 将原主节点(oldMaster)变成从节点,指向新的主节点。

c、故障转移后的redis sentinel的拓扑结构图
Redis系列(六) 高可用之哨兵 - 图9
d、在从节点中选择新的主节点

  1. 过滤掉不健康的(下线或断线),没有回复过哨兵ping响应的从节点。
  2. 选择salve-priority从节点优先级最高(redis.conf)。
  3. 选择复制偏移量最大,指复制最完整的从节点。
  4. 如果仍无法区分,则选择runid最小的从节点。

三、安装和部署

我们以3个Sentinel节点、2个从节点、1个主节点为例进行安装部署,所有这些节点都部署在一台机器上,使用端口号区分;节点的配置尽可能简化。
Redis系列(六) 高可用之哨兵 - 图10

1、部署主从节点

A主节点6379节点(/usr/local/redis/redis6379.conf):
修改 requirepass 12345678,注释掉#bind 127.0.0.1

B从节点redis6380.conf和redis6381.conf:
修改 requirepass 12345678 ,注释掉#bind 127.0.0.1,
加上masterauth 12345678 ,加上slaveof 127.0.0.1 6379

redis6379.conf

  1. port 6379
  2. daemonize yes
  3. requirepass 12345678
  4. #bind 127.0.0.1
  5. logfile "6379.log"
  6. dbfilename "dump-6379.rdb"

redis6380.conf

  1. port 6380
  2. daemonize yes
  3. requirepass 12345678
  4. #bind 127.0.0.1
  5. logfile "6380.log"
  6. dbfilename "dump-6380.rdb"
  7. slaveof 192.168.92.128 6379

redis6381.conf

  1. port 6381
  2. daemonize yes
  3. requirepass 12345678
  4. #bind 127.0.0.1
  5. logfile "6381.log"
  6. dbfilename "dump-6381.rdb"
  7. slaveof 192.168.92.128 6379

Redis系列(六) 高可用之哨兵 - 图11

配置完成后,依次启动主节点和从节点:

  1. ./redis-server redis6379.conf
  2. ./redis-server redis6380.conf
  3. ./redis-server redis6381.conf

连接主节点查看主从状态是否正常,如下图所示:
Redis系列(六) 高可用之哨兵 - 图12

注意:当主从起来后,主节点可读写,从节点只可读不可写,到这里已经实现主从复制。

2、部署哨兵节点

哨兵节点本质上是特殊的Redis节点。3个哨兵节点的配置除了端口外,其他配置都一样。
/usr/local/bin/conf/sentinel_26379.conf
/usr/local/bin/conf/sentinel_26380.conf
/usr/local/bin/conf/sentinel_26381.conf
将三个文件的端口分别对应: 26379 26380 26381
以26379节点为例:

  1. #sentinel-26379.conf
  2. port 26379
  3. daemonize yes
  4. logfile "26379.log"
  5. sentinel monitor mymaster 192.168.92.128 6379 2 //监听主节点6379
  6. sentinel auth-pass mymaster 12345678 //连接主节点时的密码
  7. sentinel config-epoch mymaster 2 //故障转移时最多可以有2从节点同时对新主节点进行数据同步

sentinel monitor mymaster 192.168.92.128 6379 2 配置的含义是:该哨兵节点监控192.168.92.128:6379这个主节点,该主节点的名称是mymaster,最后的2的含义与主节点的故障判定有关:至少需要2个哨兵节点同意,才能判定主节点故障并进行故障转移。

3、启动sentinel服务

哨兵节点的启动有两种方式,二者作用是完全相同的:

  1. redis-sentinel sentinel-26379.conf
  2. redis-server sentinel-26379.conf --sentinel

关闭:./redis-cli -h 192.168.1.111 -p 26379 shutdown
其他哨兵节点启动时换对应的配置文件启动即可,同样,关闭的时候,改对应的端口即可。

4、故障转移演示

(1)首先,使用kill命令杀掉主节点:
查看主节点端口为6379
Redis系列(六) 高可用之哨兵 - 图13

杀掉进程之后,查看哨兵信息可以看到主节点已经变成6381端口了。

Redis系列(六) 高可用之哨兵 - 图14

同时可以发现,哨兵节点认为新的主节点仍然有2个从节点,这是因为哨兵在将6380切换成主节点的同时,将6379节点置为其从节点;虽然6379从节点已经挂掉,但是由于哨兵并不会对从节点进行客观下线,因此认为该从节点一直存在。当6379节点重新启动后,会自动变成6380节点的从节点。
进入主节点查看信息可以看到,6379端口服务已经被设置成从节点了:

Redis系列(六) 高可用之哨兵 - 图15

重启6379服务并进入查看信息,可以看到其是从节点,并且可以看到主节点是6381

Redis系列(六) 高可用之哨兵 - 图16

注意:
1、在故障转移阶段,哨兵和主从节点的配置文件都会被改写。
2、每个哨兵节点,只需要配置监控主节点,便可以自动发现其他的哨兵节点和从节点。

5、部署建议

1、sentinel monitor {masterName} {masterIp} {masterPort} {quorum}
参数masterIp切记不要写成127.0.0.1,要写成内网ip,例如:

  1. sentinel monitor mymaster 192.168.1.111 6379 2 //切记将IP不要写成127.0.0.1

2、哨兵sentinel个数为奇数,奇数哨兵个才能选举成功,一般建议3个。

3、哨兵可以监控多个主节点,但在实际运用中并不建议这么做,因为一旦主节点出现故障,会对多个主节点有影响,同时还会造成sentinel节点产生过多的网络连接,一般线上建议还是, 3个sentinel监听一个主节点。
Redis系列(六) 高可用之哨兵 - 图17

四、客户端访问哨兵系统

1、代码示例

以Java客户端Jedis为例,演示一下使用方法:下面代码可以连接我们刚刚搭建的哨兵系统,并进行各种读写操作。

  1. package com.james.cache;
  2. import java.util.HashSet;
  3. import java.util.Set;
  4. import org.junit.Test;
  5. import redis.clients.jedis.Jedis;
  6. import redis.clients.jedis.JedisSentinelPool;
  7. public class JedisSentinel {
  8. @Test
  9. public void testJedis() throws InterruptedException {
  10. Set<String> sentinels = new HashSet<String>();
  11. String hostAndPort1 = "39.108.149.79:26379";//172.18.86.251
  12. String hostAndPort2 = "39.108.149.79:26380";
  13. String hostAndPort3 = "39.108.149.79:26381";
  14. sentinels.add(hostAndPort1);
  15. sentinels.add(hostAndPort2);
  16. sentinels.add(hostAndPort3);
  17. String clusterName = "mymaster";
  18. String password = "12345678";
  19. JedisSentinelPool redisSentinelJedisPool = new JedisSentinelPool(clusterName, sentinels, password);
  20. Jedis jedis = null;
  21. try {
  22. jedis = redisSentinelJedisPool.getResource();
  23. jedis.set("name", "james11");
  24. System.out.println(jedis.get("name"));
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. } finally {
  28. redisSentinelJedisPool.returnBrokenResource(jedis);
  29. }
  30. redisSentinelJedisPool.close();
  31. }
  32. }

2、客户端原理

Jedis客户端对哨兵提供了很好的支持。如上述代码所示,我们只需要向Jedis提供哨兵节点集合和masterName,构造JedisSentinelPool对象;然后便可以像使用普通redis连接池一样来使用了:通过pool.getResource()获取连接,执行具体的命令。

在整个过程中,我们的代码不需要显式的指定主节点的地址,就可以连接到主节点;代码中对故障转移没有任何体现,就可以在哨兵完成故障转移后自动的切换主节点。之所以可以做到这一点,是因为在JedisSentinelPool的构造器中,进行了相关的工作;
1、遍历哨兵节点,获取主节点信息:遍历哨兵节点,通过其中一个哨兵节点+masterName获得主节点的信息;该功能是通过调用哨兵节点的sentinel get-master-addr-by-name命令实现,该命令示例如下:

Redis系列(六) 高可用之哨兵 - 图18

一旦获得主节点信息,停止遍历(因此一般来说遍历到第一个哨兵节点,循环就停止了)。
2、增加对哨兵的监听:这样当发生故障转移时,客户端便可以收到哨兵的通知,从而完成主节点的切换。具体做法是:利用redis提供的发布订阅功能,为每一个哨兵节点开启一个单独的线程,订阅哨兵节点的+switch-master频道,当收到消息时,重新初始化连接池。