Redis 开发与运维Redis Sentinel 哨兵

Posted 木兮同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 开发与运维Redis Sentinel 哨兵相关的知识,希望对你有一定的参考价值。


一、客户端连接

Redis Sentinel 客户端基本实现原理

  • 遍历 Sentinel 节点集合获取一个可用的 Sentinel 节点
  • 通过 sentinel get-master-addr-by-name master-name 这个 API 来获取对应主节点的相关信息
  • 验证当前获取的“主节点”是否是真正的主节点,这样做的目的是为了防止故障转移期间主节点的变化
  • 保持和 Sentinel 节点集合的“联系”,是可获取关于主节点的相关“信息”

Java 操作 Redis Sentinel

  • Jedis 针对 Redis Sentinel 给出了一个 JedisSentinelPool,很显然这个连接池保存的连接还是针对主节点的。Jedis 给出了很多构造方法,其中最全的如下:
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)
  • 具体参数含义
    • masterName:主节点名
    • sentinels:Sentinel 节点集合
    • poolConfig:common-pool 连接池配置
    • connectTimeout:连接超时
    • soTimeout:读写超时
    • password:主节点密码
    • database:当前数据库索引
    • clientName:客户端名
  • 使用方式和 JedisPool 非常类似,如下:
	Jedis jedis = null;
	try {
		jedis = jedisSentinelPool.getResource();
		// jedis command
	} catch (Exception ex) {
		if (ex instanceof JedisConnectionException) {
			jedis.close();
		}
		throw new RedisClientException(ex);
	} finally {
		if (isReturn) {
			jedis.close();
		}
	}

JedisSentinelPool 的实现过程

  • 下面是 JedisSentinelPool 的初始化方法:
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) {
	  ...
	  HostAndPort master = initSentinels(sentinels, masterName);
	  ...    
  }
}
  • 下面是初始化代码中重要函数 initSentinels(Set<String> sentinels, final String masterName)
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
	// 主节点
    HostAndPort master = null;
	// 遍历所有 sentinel 节点
    for (String sentinel : sentinels) {
      // 连接 sentinel 节点	
      final HostAndPort hap = HostAndPort.parseString(sentinel);

      Jedis jedis = null;
      try {
        jedis = new Jedis(hap.getHost(), hap.getPort(), sentinelConnectionTimeout, sentinelSoTimeout);
        if (sentinelUser != null) {
          jedis.auth(sentinelUser, sentinelPassword);
        } else if (sentinelPassword != null) {
          jedis.auth(sentinelPassword);
        }
        if (sentinelClientName != null) {
          jedis.clientSetname(sentinelClientName);
        }

		// 使用 sentinel get-master-addr-by-name masterName 获取主节点信息
        List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);

		// 命令返回列表为空或者长度不为 2,继续从下一个 sentinel 节点查询
        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);
		// 找到后直接跳出 for 循环
        break;
      } catch (JedisException e) {
        log.warn(
          "Cannot get master address from sentinel running @ {}. Reason: {}. Trying next one.", hap, e);
      } finally {
        if (jedis != null) {
          jedis.close();
        }
      }
    }
	
	// 为每个 sentinel 节点开启主节点 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;
  }
  • 具体过程如下
    • 遍历 Sentinel 节点集合,找到一个可用的 Sentinel 节点,如果找不到就从 Sentinel 节点集合中去找下一个,如果找不到直接抛异常给客户端
    • 找到一个可用的 Sentinel 节点,执行 sentinelGetMasterAddrByName(masterName),找到对应主节点信息
    • JedisSentinelPool 中没有发现对主节点角色验证的代码,这是因为 get-master-addr-by-name masterName 这个 API 本身就会自动获取真正的主节点
    • 为每一个 Sentinel 节点单独启动一个线程,利用 Redis 的发布订阅功能,每个线程订阅 Sentinel 节点上切换 master 的相关频道 +switch-master
  • 下面的代码就是 MasterListener 的核心监听器代码,代码中比较重要的部分就是订阅 Sentinel 节点的 +switch-master 频道,它就是 Redis Sentinel 在结束对主节点故障转移后会发布切换主节点的消息,Sentinel 节点基本将故障转移的各个阶段发生的行为都通过这种发布订阅的形式对外提供,开发者只需订阅感兴趣的频道即可,这里比较关心的是 +switch-master 频道:
	// 客户端订阅 Sentinel 节点上`+switch-master`(切换主节点)频道
	jedis.subscribe(new JedisPubSub() {
	  @Override
	  public void onMessage(String channel, String message) {
	    String[] switchMasterMsg = message.split(" ");
	    if (switchMasterMsg.length > 3) {
	      // 判断是否为当前 masterName
	      if (masterName.equals(switchMasterMsg[0])) {
	        // 发现当前 masterName 发生 switch,使用 initPool 重新初始化连接池
	        initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
	      }
	    }
	  }
	}, "+switch-master");

二、实现原理

三个定时监控任务

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

  • 每隔 10 秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构,这个定时任务的作用具体可以表现在三个方面:
    • 通过向主节点执行 info 命令,获取从节点的信息,这也是为什么 Sentinel 节点不需要显示配置监控从节点的原因。
    • 当有新的从节点加入时都可以立刻感知出来
    • 节点不可达或者故障转移后,可以通过 info 命令实时更新节点拓扑信息
  • 每隔 2 秒,每个 Sentinel 节点会向 Redis 数据节点的 __sentinel__:hello 频道上发送该 Sentinel 节点对于主节点的判断以及当前 Sentinel 节点的信息,同时每个 Sentinel 节点也会订阅该频道,来了解其他 Sentinel 节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:
    • 发现新的 Sentinel 节点,如果是新加入的 Sentinel 节点,将该 Sentinel 节点信息保存起来,并与该 Sentinel 节点创建连接
    • Sentinel 节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据
  • 每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条 ping 命令做一次心跳检测,来确认这些节点当前是否可达

主观下线和客观下线

  • 主观下线:上面第三个定时任务,每个 Sentinel 节点会每隔 1 秒对所有节点发送 ping 命令做心跳检测,当这些节点超过 down-after-milliseconds 没有进行有效回复,Sentinel 节点就会对该节点做失败判定,这个行为叫做主观下线。所以主观下线是当前 Sentinel 节点的一家之言,存在误判的可能
  • 客观下线:当 Sentinel 主观下线的节点是主节点时,该 Sentinel 节点会通过 sentinel is-master-down-by-addr 命令向其他 Sentinel 节点询问对主节点的判断,当超过 <quorum> 个数的 Sentinel 节点认为主节点确实有问题,这时该 Sentinel 节点会做出客观下线的决定。

领导者 Sentinel 节点选举

故障转移的工作实际上只需要一个 Sentinel 节点来完成即可,所以 Sentinel 节点之间会做一个领导者选举的工作,选出一个 Sentinel 节点作为领导者进行故障转移的的工作。Redis 使用了 Raft 算法实现领导者选举,这里给出大致思路:

  • 每个在线的 Sentinel 节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他 Sentinel 节点发送 sentinel is-master-down-by-addr 命令,要求将自己设置为领导者。
  • 收到命令的 Sentinel 节点,如果没有同意过其他 Sentinel 节点的命令,将同意该请求,否则拒绝。
  • 如果该 Sentinel 节点发现自己的票数已经大于等于max(quorum, num(sentinels)/2 + 1),那么它将成为领导者。
  • 如果此过程没有选出领导者,将进入下一次选举。

故障转移

领导者选举出的 Sentinel 节点负责故障转移,具体步骤如下:

  • 从节点列表中选出一个节点作为新的主节点,选择方法如下:
    • 过滤“不健康”节点(主观下线、短线)
    • 选择 slave-priority 最高的从节点列表,存在则返回,不存在则继续
    • 选择复制偏移量的最大的从节点(复制的最完整),如果存在则返回,不存在则继续
    • 选择 runid 最小的从节点
  • Sentinel 领导者节点会对第一步选出来的从节点执行 slaveof no one 命令让其成为主节点
  • Sentinel 领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点
  • Sentinel 节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点

三、开发与运维中的问题

节点运维

  • 节点下线
    • 介绍节点下线之前,先要弄清两个概念:临时下线和永久下线
      • 临时下线:暂时将节点关掉,之后还会重新启动,继续提供服务
      • 永久下线:将节点关掉后不再使用,需要做一些清理工作,如删除配置文件、持久化文件、日志文件等
    • 通常来看无论是主节点、从节点还是 Sentinel 节点,下线原因无外乎以下几种:
      • 节点所在的机器出现了不稳定或者即将过保被回收
      • 节点所在的机器性能比较差或者内存比较小,无法支撑应用方的需求
      • 节点自身出现服务不正常情况,需要快速处理
    • 主节点下线:需要选出一个“合适”的从节点,使用故障转移将从节点晋升为主节点
    • 从节点下线:只需要确定好是临时还是永久下线后执行相应操作即可。如果使用了读写分离,下线从节点需要保证应用方可以感知从节点的下线变化,从而把读取请求路由到其他节点。
  • 节点上线
    • 添加从节点:添加方式是增加 slaveof {masterIp} {masterPort} 的配置,使用 redis-server 启动即可,它将被 Sentinel 节点发现。场景大致有如下几种:
      • 使用了读写分离,但现有的从节点无法支撑应用方的流量
      • 主节点没有可用的从节点,无法支持故障转移
      • 添加一个更强悍的从节点利用手动故障转移替换主节点
    • 添加 Sentinel 节点:添加方式是增加 sentinel monitor 主节点的配置,使用 redis-sentinel 启动即可,它将被其余 Sentinel 节点自动发现,场景大致如下几种:
    • 添加主节点:因为 Redis Sentinel 中只能有一个主节点,所以不需要添加主节点,如果需要替换主节点可以使用手动故障转移。

高可用读写分离

  • 从节点的作用

    • 第一,当主节点出现故障时,作为主节点的后备“顶”上来实现故障转移,Redis Sentinel 已经实现了该功能的自动化,实现了真正的高可用
    • 第二,扩展主节点的读能力,尤其是在读多写少的场景非常适用,通常模型如下:
      读写分离
    • 这种模型中,从节点并不是高可用的,如果 slave-1 节点出现故障,首先客户端 client-1 将与其失联,其次 Sentinel 节点只会对该节点做主观下线,因为 Redis Sentinel 的故障转移是针对主节点的。所以很多时候从节点只是作为一个热备,不让它参与客户端的读操作,就是为了保证整体高可用性,但在很多时候确实需要读写分离的场景,如何实现从节点的高可用时非常有必要的。
  • Redis Sentinel 读写分离设计思路

    • Redis Sentinel 在对各个节点的监控中,如果有对应事件的发生,都会发出相应的事件消息,其中和从节点变动的事件有以下几个:
      • +switch-master:切换主节点(原来的从节点晋升为主节点),说明减少了某个从节点
      • +convert-to-slave:切换从节点(原来的主节点降级为从节点),说明添加了某个从节点
      • +sdown:主观下线,说明可能某个从节点可能不可用(因为对从节点不会做客观下线),所以在实现客户端时可以采用自身策略来实现类似主观下线的功能
      • +reboot:重新启动了某个节点,如果它的角色是 slave,那么说明添加了某个从节点
    • 所以在设计 Redis Sentinel 的从节点高可用时,只要能够实时掌握所有从节点的状态,把所有从节点看做一个资源池,无论是上线还是下线从节点,客户端都能及时感知到(将从资源池中添加或者删除),这样从节点高可用的目标就达到了。
      读写分离高可用

来源:《Redis 开发与运维》第 9 章 哨兵

以上是关于Redis 开发与运维Redis Sentinel 哨兵的主要内容,如果未能解决你的问题,请参考以下文章

Redis 开发与运维Redis Sentinel 哨兵

Redis 开发与运维Redis Sentinel 哨兵

Redis开发与运维

《Redis开发与运维》

redis 开发与运维 学习心得

Redis开发与运维:数据迁移