Redis主从模式下,主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,这样非常不利于应用。Redis的Redis Sentinel(哨兵)架构来解决该问题

基本概念

  • 主节点:Redis主服务/数据库 一个独立的Redis进程
  • 从节点:Redis从服务/数据库 一个独立的Resid进程
  • Redis数据节点:主节点和从节点 主节点和从节点的进程
  • Sentinel节点:监控Redis数据节点 一个独立的Sentinel进程
  • Sentinel节点集合:若干Sentinel节点的抽象组合 若干Sentinel节点进程
  • Redis Sentinel:Redis高可用实现方案 Sentinel节点集合和Redis数据节点进程
  • 应用方:泛指一个或多个客户端 一个或者多个客户端进程或者线程

Redis Sentinel是Redis的高可用实现方案,在实际的生产环境中,对提高整个系统的高可用性是非常有帮助的

主从复制的问题

主从复制的好处作用

主从复制可以将主节点数据同步到从节点

  • 作为主节点的一个备份,主节点出现故障从节点可以作为后备,保证数据尽量不丢失
  • 从节点可以扩展主节点的读能力,读写分离

主从复制的问题

  • 主节点出现故障需要手动将一个从节点晋升为主节点,整个过程需要人工干预 (高可用问题)
  • 主节点的写能力收到单机的限制 (分布式问题)
  • 主节点的储存能力受到单机的限制 (分布式问题)

高可用

Redis主从复制模式下,一个主节点出现了故障不可达需要人工干预进行故障转移,如果无法及时感知到主节点的变化,必然会造成写数据丢失和
一定的读数据错误,应用方服务器不可用。

  • Redis主从故障转移步骤
    1.主节点发生故障,客户端连接失败,从节点与主节点连接失败,复制中断
    2.如果主节点无法重启,则需选择一个从节点执行slaveof no one晋升为新的主节点
    3.更新应用方的主节点信息,重新启动应用方
    4.将另一个从节点更新现在的主节点信息,复制新的主节点
    6.待原来的主节点恢复后,让他去复制新的主节点

  • 问题
    1.判断主节点故障的机制标准
    2.多个从节点如何保证只有一个被晋升为主节点
    3.通知客户端新的主节点机制是否足够健壮

Redis Sentinel的高可用性

Redis Sentinel与Redis主从复制模式只是多个若干Sentinel节点,并没有针对Redis节点做特殊处理
image.png
当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用

  • 哨兵模式:
    1.Redis Sentinel是一个分布式架构(指的是Redis数据节点、Sentinel节点集合、客户端分布在多个物理节点),包含若干个Sentinel节点和Redis数据节点
    2.每个Sentinel节点回对数据节点和其余Sentinel节点进行监控,当其发现节点不可达时,回对节点做下线标识
    3.如果下线的为主节点,Sentinel会和其他Sentinel节点“协商”,选举出一个Sentinel节点来完成故障自动转移的工作
    4.将变化实时通知Redis应用方

Redis Sentinel具有的功能

  • 具有的功能:
    1.监控:定期检测Redis数据节点、其余Sentinel节点是否可达
    2.通知:将故障转移的结果通知给应用发
    3.主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系
    4.配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息

多个Sentinel节点好处

  • Redis Sentinel包含若干个Sentinel节点好处
    1.故障判断是由多个Sentinel节点共同完成,可以有效地防止误判
    2.个别Sentinel节点不可用,整个Sentinel节点集合依然是健壮的

安装和部署Redis Sentinel

部署拓扑结构

image.png

部署Redis数据节点

在Redis Sentinel中Redis数据节点没有做任何特殊配置

启动主节点

启用另外的配置
image.png
image.png

启动两个从节点

配置如下
image.png
image.png

确认主从关系

image.png

部署Sentinel节点

Sentinel配置信息

image.png
image.png
配置信息如下:
image.png
上图少写了个s,导致报错了。。。
image.png
可以看出redis提示也非常人性化,改过之后如下
image.png

  • 配置分别为:
    1.Sentinel节点端口,默认为26379
    2.monitor mymaster 需要监控的主节点,2为失败后必须要2个Sentinel节点同意

启动Sentinel

redis-sentinel [配置文件] [--sentinel 参数]
image.png

确认

image.png

image.png

  • 至此Redis Sentinel已经搭建好了,建议:
    1.Redis Sentinel的所有节点部署在不同的物理机上
    2.Redis Sentinel中的数据节点和普通的Redis数据节点在配置上没有任何区别,只是添加了Sentinel节点对其进行监控

测试

image.png
image.png

  • 如上图所示:
    1.keys *所有都为空
    2.对主节点set Redis Sentinel
    3.两个从节点都有设置的信息
    4.强制杀死主节点,一个从节点晋升为主节点
    5.查看剩余节点信息get Redis都有数据
    6.重启被杀死的进程,查看角色为从节点
    7.且get Redis的信息也都存在

配置优化

默认配置文件为/etc/redis-sentinel.conf
启动后的config文件会被改变重写,如下
image.png

port 26380
logfile "26380.log"
dir "/usr/etc/Redis/data"
sentinel myid 1a72ed12b****************
sentinel monitor mymaster 127.0.0.1 6381 2
sentinel config-epoch mymaster 1
sentinel leader-epoch mymaster 1
# Generated by CONFIG REWRITE
sentinel known-slave mymaster 127.0.0.1 6379
sentinel known-slave mymaster 127.0.0.1 6380
sentinel known-sentinel mymaster 127.0.0.1 26381 17e01eda629c****************
sentinel known-sentinel mymaster 127.0.0.1 26379 cc4da90f**********************
sentinel current-epoch 1
  • 配置说明:
    1.port与dir:端口和工作目录
    2.sentinel monitor mymaster 127.0.0.1 6381 2:Sentinel节点定期监控的主节点ip和端口,和判定主节点不可用需要达到的票数虽然未体现从节点及其他Sentinel节点配置信息,但会通过主节点获得有关节点和Sentinel节点信息后进行监控
    3.所有节点启动后,获得到的节点会在config中重写,config文件会改变
    4.去除默认配置
    5.添加配置纪元相关参数

监控多个主节点

Redis Sentinel可以同时监控多个主节点,只需要制定多个masterName来区分不同的主节点即可

调整配置

Sentinel节点和普通的数据节点一样支持动态设置参数,且并不支持所有参数

部署技巧

Sentinel节点不应该部署在同一台物理“机器”上

如果出现硬件故障,所有的虚拟机都会受到影响,为了实现Sentinel节点集合真正的高可用,不应该部署在同一台物理机器上

部署至少3个且奇数个的Sentinel节点

领导者选举需要至少一半+1个节点,奇数点可以在满足的条件上节省一个节点

只有一套Sentinel还是每个主节点配置一套Sentinel

  • 一套Sentinel:
    优点:降低维护成本,秩序维护固定个数的Sentinel节点,集中对多个Redis数据节点进行管理就可以
    缺点:Sentinel节点集合出现异常时,会对多个Redis数据节点造成影响,如果监控的Redis数据较多会造成Sentinel节点产生过多的网络连接

  • 多套Sentinel:
    优缺点与上述相反
    会造成资源浪费。但每套Redis Sentinel彼此隔离,数据保护较安全

使用建议:如果监控的是同一个业务的多个主节点集合,可以使用一套Sentinel。否则应该使用多套Sentinel

API

Sentinel节点是一个特殊的Redis节点,有自己专门的API

sentinel masters

展示所有被检控的主节点状态以及相关的的信息统计
image.png

sentinel master [master_name]

展示指定[master name]的主节点状态及相关的统计信息
image.png

sentinel slaves [master name]

展示指定mastr name的从节点状态以及相关的统计信息

sentinel sentinels [master name]

展示指定master name的Sentinel节点集合(不包含当前Sentinel节点)

...

客户端连接

如果主节点挂掉了,虽然Redis Sentinel可以完成故障转移,但是客户端无法获取这个变化,那么使用Redis Sentinel的意义就不大了,所以各个语言的客户端都需要对Redis Sentinel进行显示的支持

Redis Sentinel的客户端

最了解主节点信息的就是Sentinel节点集合,通过masterName标识各个主节点。所以无论哪种编程语言的客户端,如果需要正确地连接Redis Sentinel,必须有Sentinel节点集合和masterName两个参数

Redis Sentinel客户端基本实现原理

  • 基本步骤:
    1.遍历Sentinel节点集合获取一个可用的Sentinel节点
    2.通过Sentinel get-master-addr-by-name 这个API获取对应主节点相关信息
    3.info replication验证当前获取的“主节点”是否为真正的主节点(防止故障转移期间主节点的变化)
    4.保持和Sentinel节点集合的“联系”,时刻获取关于主节点相关“信息”

Java操作Redis Sentinel

Jedis的连接池JedisPool,为了不与之相混淆,Jdeis针对Redis Sentinel给出了JedisSentinelPool,这个连接池保存的连接还是针对主节点的。

Jedis的源码

Jedis的JedisSentinelPool构造方法如下

public class JedisSentinelPool extends JedisPoolAbstract {

  protected GenericObjectPoolConfig poolConfig;

  protected int connectionTimeout;
  protected int soTimeout;
  protected String password;
  protected int database;
  protected String clientName;

  protected int sentinelConnectionTimeout;
  protected int sentinelSoTimeout;
  protected String sentinelPassword;
  protected String sentinelClientName;

  public JedisSentinelPool(String masterName, Set<String> sentinels,
      final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
      final String password, final int database, final String clientName,
      final int sentinelConnectionTimeout, final int sentinelSoTimeout, final String sentinelPassword,
      final String sentinelClientName) {
    this.poolConfig = poolConfig;
    this.connectionTimeout = connectionTimeout;
    this.soTimeout = soTimeout;
    this.password = password;
    this.database = database;
    this.clientName = clientName;
    this.sentinelConnectionTimeout = sentinelConnectionTimeout;
    this.sentinelSoTimeout = sentinelSoTimeout;
    this.sentinelPassword = sentinelPassword;
    this.sentinelClientName = sentinelClientName;
	//初始化
    HostAndPort master = initSentinels(sentinels, masterName);
//初始化连接池
    initPool(master);
  }
  • 上述参数含义如下:
    masterName:主节点名字
    sentinels:Sentinel节点集合
    poolConfig:common-pool连接池配置
    connectTimeout:连接超时
    soTimeout:读写超时
    password:主节点密码
    database:当前数据库索引
    clientName:客户端名

其中最主要的是初始化Sentinels和Pool

  private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
	//主节点
    HostAndPort master = null;
    boolean sentinelAvailable = false;

    log.info("Trying to find master from available Sentinels...");
	//开始遍历所有Sentinel节点
    for (String sentinel : sentinels) {
	//连接Sentinel 节点
      final HostAndPort hap = HostAndPort.parseString(sentinel);

      log.debug("Connecting to Sentinel {}", hap);

      Jedis jedis = null;
      try {
        jedis = new Jedis(hap.getHost(), hap.getPort(), sentinelConnectionTimeout, sentinelSoTimeout);
        if (sentinelPassword != null) {
          jedis.auth(sentinelPassword);
        }
        if (sentinelClientName != null) {
          jedis.clientSetname(sentinelClientName);
        }
	//获取主节点信息
        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

        // connected to sentinel...
        sentinelAvailable = true;

        if (masterAddr == null || masterAddr.size() != 2) {
          log.warn("Can not get master addr, master name: {}. Sentinel: {}", masterName, hap);
          continue;
        }
	//解析masterAddr获取主节点信息
        master = toHostAndPort(masterAddr);
        log.debug("Found Redis master at {}", master);
	//找到后跳出循环
        break;
      } catch (JedisException e) {
        // resolves #1036, it should handle JedisException there's another chance
        // of raising JedisDataException
        log.warn(
          "Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap,
          e.toString());
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }

    if (master == null) {
      if (sentinelAvailable) {
        // can connect to sentinel, but master name seems to not
        // monitored
        throw new JedisException("Can connect to sentinel, but " + masterName
            + " seems to be not monitored...");
      } else {
        throw new JedisConnectionException("All sentinels down, cannot determine where is "
            + masterName + " master is running...");
      }
    }

    log.info("Redis master running at " + master + ", starting Sentinel listeners...");
	//为每个Senrinel节点开启主节点switch的监控线程
    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();
    }

    return master;
  }


private void initPool(HostAndPort master) {
    synchronized(initPoolLock){
      if (!master.equals(currentHostMaster)) {
        currentHostMaster = master;
        if (factory == null) {
          factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
              soTimeout, password, database, clientName);
          initPool(poolConfig, factory);
        } else {
          factory.setHostAndPort(currentHostMaster);
          // although we clear the pool, we still have to check the
          // returned object
          // in getResource, this call only clears idle instances, not
          // borrowed instances
          internalPool.clear();
        }

        log.info("Created JedisPool to master at " + master);
      }
    }
  }
  • 具体过程如下:
    1.遍历Sentinel节点集合,找到一个可用的Sentinel节点,如果遍历后都找不到就抛出异常
    2.找到一个可用的Sentinel节点,执行 List masterAddr = jedis.sentinelGetMasterAddrByName(masterName);找到主节点信息
    3.JedisSentinelPool没有对主节点角色进行验证的代码,是因为getmasteraddbyname的API本身就会自动获取真正的主节点
    4.为每个Sentinel节点单独启动一个线程,利用Redis的发布订阅功能,每个线程订阅Sentinel节点上切换master的相关频道

Redis Sentinel的实现原理

三个定时监控任务

Redis Sentinel通过三个定时监控任务完成对各个节点发现和监控

每隔10S info命令

每隔10S,每个Sentinel节点回想主节点和从节点发送info命令获取最新的结构

  • 通过向主节点执行info命令,获取节点信息,故不需要显式配置监控从节点
  • 当有新的从节点加入时都可以立刻感知
  • 节点不可达或者故障转移后,可以通过info命令实时更新节点信息

每隔2S hello

每隔2S,每个Sentinel节点会向Redis数据节点的hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息

  • 发现新的Sentinel节点
  • Sentinel节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据

每隔1S 心跳任务ping

每隔Sentinel节点会向主节点、从节点、其余Sentinel发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达

主观下线和客观下线

主观下线

每个Sentinel节点心跳ping发送后超过预定时间没有收到有效回复,则Sentinel节点会对该节点做失败判定,存在误判的可能

客观下线

当SSentinel主观下线的节点是主节点时,该Sentinel节点会向其他Sentinel节点询问对主节点的判断,当超过个数,Sentinel节点认为主节点确实有问题,这时候该Sentinel节点会做出客观下线的决定

领导者Sentinel节点选举

故障转移的工作只需要一个Sentinel节点来完成即可,所以Sentinel节点之间会做一个领导者选举工作,选出一个Sentinel节点作为领导者进行故障转移的工作(Redis使用Raft算法实现)

  • 选举的过程:
    1.每个在线的Sentinel节点都有资格称为领导者,当其确认客观下线主节点是,回想其他Sentinel节点发送命令,请求自己设置为领导者
    2.收到命令的Sentinel节点如果没有同意过其他节点,将会同意该次请求,即Sentinel节点同意第一次收到请求设置为leader的请求,拒绝后面的
    3.如果该Sentinel节点发现自己的票数大于等于max(quorum,num(sentinels)/2+1),那么其将成为领导者
    4.如果此过程没有选举出领导者,将进入下一次选举

其实选举的过程非常快,基本上谁先完成客观下线,谁就是领导者

故障转移

选举出的领导者leader负责故障转移,步骤如下
image.png

在从节点列表中选出一个节点作为新的主节点

  • 过滤:“不健康”(主观下线、断线)、5s内没有回复过Sentinel节点ping响应
  • 选择:选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在继续
  • 选择复制偏移量最大(复制的最完整)的从节点,如果存在则返回,不存在继续
  • 选择runid最小的从节点

从节点称为主节点

Sentinel领导者节点会对第一步选出来的从节点执行slave on one,从节点称为主节点

其他从节点转移新主节点

Sentinel领导者节点回想剩余的从节点发送命令,让他们称为新主节点的从节点

Sentinel节点更新

Sentinel节点集合会将原来的主节点更新为从节点,保持对其关注,当其恢复后命令其复制新的主节点

开发与运维中的问题


这个家伙很懒,啥也没有留下😋