Redis(十三):Redis Cluster

2023-08-13  本文已影响0人  雪飘千里

1、Reids Cluster架构

Redis(四):集群模式这篇文章中有介绍redis集群的各种模式,当时公司主要用的还是自定义的哨兵Sentinel Sharding集群模式,但是这种模式只适合小公司,而且随着redis版本的更新,以及后来换工作,发现使用redis cluster的越来越多了。

Reids Cluster 集群模式见Redis(四):集群模式中介绍,核心是下面两张图

img img

2、Hash槽(slot) 与 一致性hash

2.1 Hash槽(slot)

hash槽是 Redis Cluster管理数据的基本单位,Redis Cluster 采用虚拟哈希槽分区,所有的键通过CRC16校验后对16384取模来决定放置哪个槽(Slot),每一个节点负责维护一部分槽以及槽所映射的键值数据,像上图中,0-4000属于第一个节点。

一个 Redis 集群包含 16384 个hash slot,客户端在写数据时,先计算出哈希值,然后对key取模(CRC16(key)%16384),确定该数据需要写入哪个hash slot,然后根据hash slot查到对应的redis 节点,将数据直接写入相应的主节点,同时主节点也会把数据同步到相应的副本节点上,读取数据也是一样的过程。

hash slot并不强制要求在每个节点上均匀分布,在性能好配置高的机器上,hash slot数量可以分配多一点,当然,通常情况下,运维都是均匀分配的。

使用哈希槽进行扩缩容就会很方便,如果我们想要新添加个节点D, 我们只需要从之前的节点分部分哈希槽到节点D上。 如果我想移除某个节点,只需要将该节点中的哈希槽移到另外两个节点上,然后将该节点从集群中移除即可。从一个节点将哈希槽移动到另一个节点并不会停止服务(渐进式rehash),所以无论添加或是删除节点都不会造成集群的不可用,这样就实现了动态扩缩容。

为什么Redis Cluster会设计成16384个槽呢

*如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大,浪费带宽,而且redis的集群主节点数量基本不可能超过1000个,如果节点过1000个,也会导致网络拥堵。对于节点数在1000以内的redis cluster集群,16384个槽位够用了,没有必要继续拓展。

2.2 一致性Hash算法

按照常用的hash算法来将对应的key哈希到一个具有232次方个桶的空间中,即0~(232)-1的数字空间。现在我们可以将这些数字头尾相连,想象成一个闭合的环形;

在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到换种(一般情况下对机器的hash计算是采用机器的IP或者唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。

然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。

image.png

2.3 Redis Cluster 为啥采用hash slot

扩容和缩容

一致性哈希算法在新增和删除节点后,数据会按照顺时针来重新分布节点。而redis cluster的新增和删除节点都需要手动来分配槽区。但是一致性哈希的节点分布基于圆环,无法很好的手动设置数据分布,比如有些节点的硬件差,希望少存一点数据,这种很难操作。而哈希槽可以很灵活的配置每个节点占用哈希槽的数量。

比如,Hash slot 新增/删除节点特别方便,

高可用

一致性哈希是创建虚拟节点来实现节点宕机后的数据转移并保证数据的安全性和集群的可用性的。redis cluster是采用master节点有多个slave节点机制来保证数据的完整性的,master节点写入数据,slave节点同步数据。当master节点挂机后,slave节点会通过选举机制选举出一个节点变成master节点,实现高可用。

一致性哈希的某个节点宕机或者掉线后,当该机器上原本缓存的数据被请求时,会从数据源重新获取数据,并将数据添加到失效机器后面的机器,这个过程被称为 "缓存抖动" ,容易造成缓存雪崩

而采用hash槽+副本,节点宕机的时候从节点自动接替,不易造成雪崩,可以保证高可用。

算法

hash slot计算时用的是CRC16算法,比hash算法要简单多了。

3、Redis Cluster动态扩容、缩容

动态扩容、缩容都是运维在业务低峰期手动执行的,简单的分为以下几步,具体执行方式网上大把资料可查,而且通常公司都是有统一的脚本的执行。

4、通讯协议gossip

redis cluster是去中心化的,彼此之间状态同步靠 gossip 协议通信。

集中式:元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力;比如storm,其元数据就存储在zookeeper中;

去中心化:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后

4.1 Gossip 协议简介

Gossip 协议又称 epidemic 协议(epidemic protocol),是基于流行病传播方式的节点或者进程之间信息交换的协议,在P2P网络和分布式系统中应用广泛,它的方法论也特别简单:

在一个处于有界网络的集群里,如果每个节点都随机与其他节点交换特定信息,经过足够长的时间后,集群各个节点对该份信息的认知终将收敛到一致。

这里的“特定信息”一般就是指集群状态、各节点的状态以及其他元数据等。Gossip协议是完全符合 BASE 原则,可以用在任何要求最终一致性的领域,比如分布式存储和注册中心。另外,它可以很方便地实现弹性集群,允许节点随时上下线,提供快捷的失败检测和动态负载均衡等。

此外,Gossip 协议的最大的好处是,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。这就允许 Redis Cluster 或者 Consul 集群管理的节点规模能横向扩展到数千个。

4.2 Redis Gossip 通信类型

Redis节点间通信的消息有以下几种类型:

image.png

具体通信过程参考 Redis集群–节点通信的过程(原理)

总起来说Redis官方集群是一个去中心化的类P2P网络,P2P早些年非常流行,像电驴、BT、迅雷什么的都是P2P网络。在Redis集群中Gossip协议充当了去中心化的通信协议的角色,依据制定的通信规则来实现整个集群的无中心管理节点的自治行为。

4.3 Redis Gossip 故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。

也就是说当前节点发现其他结点疑似挂掉了,那么就写在自己的小本本上,等着通知

5、故障转移

当一个主节点发生故障时,集群会执行故障转移以维护系统的高可用性。

以下是故障转移的主要步骤:

详细过程可参考:Redis集群–故障转移的过程(原理)

6、jedis Cluster请求路由源码分析

jedis cluster初始化时,会随机选择一个node,初始化hashslot -> JedisPool,node -> JedisPool(每个节点对应自己的连接池) 映射表

//JedisClusterInfoCache.java

//节点与其对应连接池的映射关系
 private final Map<String, JedisPool> nodes = new HashMap<String, JedisPool>();
//槽位与槽位所在节点对应连接池的映射
 private final Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();
//JedisPool 对应的jedis中,会包含服务端host 和 port,所以slots可以根据slot获取到node节点信息

每次基于JedisCluster执行操作过程如下:

private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
    if (attempts <= 0) {
      throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections?");
    }

    Jedis connection = null;
    try {

      if (redirect != null) {
// 获取连接
        connection = this.connectionHandler.getConnectionFromNode(redirect.getTargetNode());
// 如果是 Ask 异常,发送 ack 命令
        if (redirect instanceof JedisAskDataException) {
          // TODO: Pipeline asking with the original command to make it faster....
          connection.asking();
        }
      } else {
        if (tryRandomNode) {
          connection = connectionHandler.getConnection();
        } else {
          connection = connectionHandler.getConnectionFromSlot(slot);
        }
      }

      return execute(connection);

    } catch (JedisNoReachableClusterNodeException jnrcne) {
      throw jnrcne;
    } catch (JedisConnectionException jce) {
      // release current connection before recursion
      releaseConnection(connection);
      connection = null;
 // 当重试到 1 次时,更新本地 slots 缓存
      if (attempts <= 1) {
        //We need this because if node is not reachable anymore - we need to finally initiate slots
        //renewing, or we can stuck with cluster state without one node in opposite case.
        //But now if maxAttempts = [1 or 2] we will do it too often.
        //TODO make tracking of successful/unsuccessful operations for node - do renewing only
        //if there were no successful responses from this node last few seconds
        this.connectionHandler.renewSlotCache();
      }
//  出现连接错误,使用随机连接 递归执行重试
      return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
    } catch (JedisRedirectionException jre) {
      // if MOVED redirection occurred,
// 如果是 MOVED 异常,更新 slots 缓存
      if (jre instanceof JedisMovedDataException) {
        // it rebuilds cluster's slot cache recommended by Redis cluster specification
        this.connectionHandler.renewSlotCache(connection);
      }

      // release current connection before recursion
      releaseConnection(connection);
      connection = null;
// 递归,执行重试
      return runWithRetries(slot, attempts - 1, false, jre);
    } finally {
      releaseConnection(connection);
    }
  }

Moved重定向( JedisMovedDataException )

(slot 已经完成了迁移)
如果说进行了reshard这样的操作,可能slot已经不在那个node上了,就会返回moved,如果JedisCluter API发现对应的节点返回moved,那么利用该节点的元数据,更新JedisClusterInfoCache映射表缓存,重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错(JedisClusterMaxRedirectionsException)

Ask重定向(JedisAskDataException)

(slot 正在迁移)
如果hash slot正在迁移,那么会返回ask重定向给jedis, jedis接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以JedisCluster API收到ask是不会更新hashslot本地缓存 。

ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别,ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存,但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。

7、Hash tag

hash tag用于redis集群中。其实现方式为在key中加个{},例如test{1}。使用hash tag后客户端在计算key的crc16时,只计算{}中数据。如果没使用hash tag,客户端会对整个key进行crc16计算。下面演示下hash tag使用。

通过hash tag指定特定值,使得同一份值可以落在每个节点上。

通过运维可以获取到 hash槽与节点的关系,如果想要在每个节点上都缓存一份数据,那就需要在项目启动时,初始化计算出多个tag值,每个tag值对应一个节点。

在缓存值时,key后面随机添加上tag值,这样每个节点都会有一个数据。只要redis 集群(或者 slot)不变更,那么这些tag就可以是固定的,比如 1,2,3,4分别对应4个node。

但是通常是不会这么麻烦处理的,因为如果真的有这种需求,可以直接把数据缓存到内存中,这样效率更高。

上一篇 下一篇

猜你喜欢

热点阅读