美文网首页
Redis(十三):Redis Cluster

Redis(十三):Redis Cluster

作者: 雪飘千里 | 来源:发表于2023-08-13 21:38 被阅读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动态扩容、缩容

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

  • 1 在新节点部署redis cluster

  • 2 使用工具将redis-4加入集群 2.1 安装ruby环境 2.2 将redis-4加入集群

  • 3 将槽位重新分配 3.1 所有节点分出槽位给新节点 3.2 迁移指定节点的槽位给新节点 3.3 查看集群信息及状态

  • 4 配置主从

  • 5 查看集群信息及状态

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节点间通信的消息有以下几种类型:

  • Ping消息:节点按照配置的时间间隔向集群中其他节点发送 ping 消息(指的是心跳),消息中带有自己的状态,还有自己维护的集群元数据,和部分其他节点的元数据,用于检测节点是否在线,或者交换信息;

  • Pong消息:节点用于回应 PING 和 MEET 的消息,结构和 PING 消息类似,也包含自己的状态和其他信息,也可以用于信息广播和更新;

  • Meet消息:通知新节点的加入,通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群,然后新节点就会开始与其他节点进行通信;

  • Fail消息:广播节点下线,Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。

image.png

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

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

4.3 Redis Gossip 故障检测

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

  • 保存信息:当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表中,并后续关于结点D疑似下线的状态通过Gossip协议通知其他节点。

  • 一起裁定:如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。

  • 最终裁定:将 node 标记为 FAIL 需要满足以下两个条件:

    • 有半数以上的主节点将 node 标记为 PFAIL 状态。

    • 当前节点也将 node 标记为 PFAIL 状态。

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

5、故障转移

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

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

  • 1.集群中的其他节点会检测到主节点已下线。

  • 2.集群会选举一个新的主节点来接管故障主节点的槽(slot)。选举过程考虑节点的优先级、复制偏移量和最近一次通信时间。

  • 3.新的主节点开始复制故障主节点的数据,以便在故障转移完成后提供完整的数据服务。

  • 4.集群中的其他节点更新它们的配置,以将故障主节点的槽重新映射到新的主节点。

  • 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执行操作过程如下:

    1. 计算slot并根据slots缓存获取目标节点连接,执行命令
    1. 如果出现连接错误,使用随机连接重新执行命令,每次命令重试对redirections参数减1
    1. 如果捕获到MOVED重定向错误,使用cluster slots命令更新slots缓存
    1. 重复执行前3步,直到命令执行成功,或者当redirections<=0时抛出Jedis ClusterMaxRedirectionsException异常
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)

    1. 当客户端发送一个请求到节点1,节点发现这个slot并不是由自己管理,于是给客户端返回-MOVED 3999 127.0.0.1:6381响应,3999是客户端要访问的key的slot的编号,后面就是该slot所在的目标节点,-MOVED中的减号则是代表客户端的这个请求是错误的,这样就告诉了客户端:"slot不再我这儿,你去127.0.0.1:6381找3999号slot";
    1. 然后客户端更新本地缓存JedisClusterInfoCache,重建 slots->JedisPool 缓存映射,
    1. 客户端根据-MOVED的提示,将请求打到真正的目标节点。

Ask重定向(JedisAskDataException)

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

    1. 当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点,这时候,当客户端根据本地slots缓存发送命令到源节点,如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:
      (error) ASK {slot} {targetIP}:{targetPort}
    1. 客户端收到ASK重定向指令之后,先去目标节点执行一个不带参数的asking指令,然后在目标节点执行原来的操作请求指令;

      客户端先执行一个不带参数的asking指令的原因是,在迁移完成之前,按道理来说,这个slot还是不属于目标节点的,于是目标节点会给客户端返回-MOVED指令,让客户端去原节点执行操作,这样就形成了"互相推托"的重定向循环。
      asking指令就是告诉目标节点,"我的指令你必须处理,请求的slot就当成是你的吧"

    1. 如果数据在目标节点存在则执行命令,不存在则返回不存在信息
      迁移会影响服务效率,在正常情况下,一次请求就可以完成操作,而在迁移过程中,客户端需要请求3次(发送给原节点、发送给目标节点asking指令,发送给目标节点真正的处理请求)
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。

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

相关文章

网友评论

      本文标题:Redis(十三):Redis Cluster

      本文链接:https://www.haomeiwen.com/subject/htriudtx.html