Redis Cluster 集群一致性原理及slot迁移测试

2024-06-11 05:48

本文主要是介绍Redis Cluster 集群一致性原理及slot迁移测试,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

参考:Redis Cluster原理与管理;Inconsistent slot mapping;Redis中文文档

集群信息一致性问题

主从和slot的一致性是由epoch来管理的. epoch就像Raft中的term, 但仅仅是像. 每个节点有一个自己独特的epoch和整个集群的epoch, 为简化下面都称为node epoch和cluster epoch. node epoch一直递增, 其表示某节点最后一次变成主节点或获取新slot所有权的逻辑时间. cluster epoch则是整个集群中最大的那个node epoch. 我们称递增node epoch为bump epoch, 它会用当前的cluster epoch加一来更新自己的node epoch.

在使用gossip协议中, 如果多个节点声称不同的集群信息, 那对于某个节点来说究竟要相信谁呢? Redis Cluster规定了每个主节点的epoch都不可以相同. 而一个节点只会去相信拥有更大node epoch的节点声称的信息, 因为更大的epoch代表更新的集群信息.
原则上:
(1)如果epoch不变, 集群就不应该有变更(包括选举和迁移槽位)
(2)每个节点的node epoch都是独一无二的

(3)拥有越高epoch的节点, 集群信息越新


Epoch Collision

实际上, 在迁移slot或者使用cluster failover的时候, 如果多个节点同时bump epoch, 就有可能出现多个节点拥有同一个epoch, 违反上述原则(2)和(3). 这个时候拥有较小node id的节点就会自动再一次bump epoch, 以保证原则(3). 而原则(2)实际上因此也并不严格成立, 因为解决epoch collision需要一小段时间.

slot

最大的问题在于slot. 我们遇到过数次迁移slot失败后出现slot不一致的情况. 如果还没搞懂它怎么管slot, 请记住下面这句话:
不要用乱用cluster setslot node.实在要使用 如果此时的节点没有importing flag则必须要给它发一次cluster bumpepoch.
我相信大多数不一致问题都是我们作死用这个命令造成的. 除了它我暂时还没找到有什么大概率的情况会导致不一致.

slot 管理

首先我们搞清楚slot究竟是怎么管的. 每个节点都有一份16384长的表对应每个slot究竟归哪个节点, 并且会保存当前节点所认为的其它节点的node epoch. 这样每个slot实际上绑定了一个节点及其node epoch. 然后由自认为拥有某slot的节点来负责通知其它节点这个slot的归属. 其它节点收到这个消息后, 会对比该slot原先绑定节点的node epoch, 如果收到的是更大的node epoch则更新, 否则不予理睬. 除此之外, 除了使用slot相关命令做变更, 集群没有其它途径修改slot的归属.

     slot x 是我管的, 我的node epoch是 y
node A ------------------------------> node B(原来slot x归node C管, 如果 y 比 node C 的node epoch大, 我就更新slot x的归属)

这实际上依赖上述的原则(3), 并且相信slot的旧主人还没有更新epoch.

迁移slot的一致性

下面来看迁移slot如何保证slot归属的一致性.
从node A迁移一个槽位到node B的流程是:
(1) node A调用cluster setslot migrating设置migrating flag, node B调用cluster setslot importing设置importing flag
(2) 调用migrate指令迁移所有该slot的数据到node B
(3) 对两个节点使用cluster setslot node来消除importing和migrating flag, 并且设置槽位

重点在于迁移最后一步消除importing flag使用的cluster setslot node,如果对一个节点使用cluster setslot node的时候节点有importing flag, 节点会bump epoch, 这样这个节点声称slot所有权时别的节点就会认可.

但是这里并没有跑一遍选举中的投票流程. 如果另外一个节点也同时bump epoch, 就出现epoch collision. 这里是一个不完美但又略精妙的地方. 不管这个清importing flag的节点在解决collision后是否获得更高的epoch, 其epoch肯定大于migrating那个节点之前的epoch.

但这里还是有漏洞, 万一node B在广播自己的新node epoch前, node A做了什么变更而获取了一个更大的node epoch呢? 万一发生collision的是node A和node B两个节点呢? 这个时候假如node A的node id更小, node A会拿到更大的新epoch. 只要某个节点收到node A的消息, 这个slot的迁移信息就永远写不进这个节点了, 因为node A的node epoch比node B更大.

上面提到的cluster setslot node的问题在于, 如果节点没有importing flag, 它会直接设置槽位, 但不会增加自己的node epoch.这样当他告诉别的节点对这个槽位的所有权时, 其他节点并不认可. 这实际上违反了上述原则(1). 详细见这里.所以实在要在迁移slot以外的地方用这个命令, 必须要给它发一次cluster bumpepoch.

注意的地方

cluster setslot node在源节点和目标节点都须要执行,因此cluster bumpepoch也须要执行两次

思考

当slot处于migrating或者importing状态时,客户端该如何访问该slot所属的key

(1)当一个槽被设置为 MIGRATING  状态时, 原来持有这个槽的节点仍然会继续接受关于这个槽的命令请求, 但只有命令所处理的键仍然存在于节点时, 节点才会处理这个命令请求。如果命令所使用的键不存在与该节点, 那么节点将向客户端返回一个 -ASK 转向(redirection)错误, 告知客户端, 要将命令请求发送到槽的迁移目标节点。

(2)当一个槽被设置为 IMPORTING 状态时, 节点仅在接收到 ASKING 命令之后, 才会接受关于这个槽的命令请求。如果客户端没有向节点发送 ASKING 命令, 那么节点会使用 -MOVED 转向错误将命令请求转向至真正负责处理这个槽的节点。

假设现在, 我们有 A 和 B 两个节点, 并且我们想将槽 8 从节点 A 移动到节点 B , 于是我们:

  • 向节点 B 发送命令 CLUSTER SETSLOT 8 IMPORTING A
  • 向节点 A 发送命令 CLUSTER SETSLOT 8 MIGRATING B

每当客户端向其他节点发送关于哈希槽 8 的命令请求时, 这些节点都会向客户端返回指向节点 A 的转向信息:

  • 如果命令要处理的键已经存在于槽 8 里面, 那么这个命令将由节点 A 处理。
  • 如果命令要处理的键未存在于槽 8 里面(比如说,要向槽添加一个新的键), 那么这个命令由节点 B 处理。

这种机制将使得节点 A 不再创建关于槽 8 的任何新键。


关于ASK转向

当节点需要让一个客户端长期地(permanently)将针对某个槽的命令请求发送至另一个节点时, 节点向客户端返回  MOVED  转向,
另一方面, 当节点需要让客户端仅仅在下一个命令请求中转向至另一个节点时, 节点向客户端返回 ASK 转向。

比如说, 在我们上一节列举的槽 8 的例子中, 因为槽 8 所包含的各个键分散在节点 A 和节点 B 中, 所以当客户端在节点 A 中没找到某个键时, 它应该转向到节点 B 中去寻找, 但是这种转向应该仅仅影响一次命令查询, 而不是让客户端每次都直接去查找节点 B :在节点 A 所持有的属于槽 8 的键没有全部被迁移到节点 B 之前, 客户端应该先访问节点 A , 然后再访问节点 B

因为上述原因,如果我们要在查找节点 A 之后, 继续查找节点 B , 那么客户端在向节点 B 发送命令请求之前, 应该先发送一个 ASKING命令, 否则这个针对带有 IMPORTING 状态的槽的命令请求将被节点 B 拒绝执行接收到客户端 ASKING 命令的节点将为客户端设置一个一次性的标志(flag), 使得客户端可以执行一次针对 IMPORTING 状态的槽的命令请求。

从客户端的角度来看, ASK 转向的完整语义(semantics)如下:

  • 如果客户端接收到 ASK 转向, 那么将命令请求的发送对象调整为转向所指定的节点。
  • 先发送一个 ASKING 命令,然后再发送真正的命令请求。
  • 不必更新客户端所记录的槽 8 至节点的映射: 槽 8 应该仍然映射到节点 A , 而不是节点 B 。

一旦节点 A 针对槽 8 的迁移工作完成, 节点 A 在再次收到针对槽 8 的命令请求时, 就会向客户端返回 MOVED 转向, 将关于槽 8 的命令请求长期地转向到节点 B 。

注意, 即使客户端出现 Bug , 过早地将槽 8 映射到了节点 B 上面, 但只要这个客户端不发送 ASKING 命令, 客户端发送命令请求的时候就会遇上 MOVED 错误, 并将它转向回节点 A 。


测试

环境模拟:



oldkey{TEST_ASK}是slot迁移前set的,属于8003节点,new key{TEST_ASK}是slot迁移后set的,属于8001节点,以上过程
也间接印证了一旦原节点slot处于migrating状态,不再处理关于迁移的slot的任意新的键

客户端测试:
一:客户端使用单机Jedis实例

(1)测试对迁移前的key读取情况
public static void main(String[] args) {//key-redis节点映射的map,解决ASK 只应该是一次性的,因此将map定义在方法内部Map<String,HostAndPort> askHostAndPortMap = new HashMap<String, HostAndPort>();Boolean returnFlag;String key = "oldkey{TEST_ASK}";HostAndPort hp;Jedis jedis = null;String host = BaseConfig.HOST;int port = BaseConfig.PORT;//8001do {returnFlag = Boolean.FALSE;hp = askHostAndPortMap.get(key);//从缓存中取出HostAndPortif (hp == null)hp = movedHostAndPortMap.get(key);//从缓存中取出HostAndPorttry {if (hp != null){host = hp.getHost();port = hp.getPort();} else {port = BaseConfig.PORT;host = BaseConfig.HOST;}jedis = jedisUtils.getJedis(host,port);if (askHostAndPortMap.size()>0){askHostAndPortMap.clear();//清空askMapjedis.asking();//上次返回了ask}String value = jedis.get(key);System.out.println(key+":"+value);} catch (JedisMovedDataException e) {//rediscluster:当前key所在的slot不是当前连接的redis节点,jedis抛出moved,要求客户端重定向到正确的redis节点returnFlag = Boolean.TRUE;movedHostAndPortMap.put(key, e.getTargetNode());//更新缓存的mapSystem.out.println("我进行了一次moved:"+e.getTargetNode().getHost()+":"+e.getTargetNode().getPort());} catch (JedisAskDataException e) {//rediscluster:当前key所在的slot处于migrating/importing状态,jedis抛出ask,要求客户端重定向到正确的redis节点returnFlag = Boolean.TRUE;askHostAndPortMap.put(key, e.getTargetNode());//更新缓存的mapSystem.out.println("我进行了一次ask:"+e.getTargetNode().getHost()+":"+e.getTargetNode().getPort());} catch (Exception e) {e.printStackTrace();} finally {if (jedis != null)jedisUtils.closeJedis(jedis,host,port);//释放连接}} while (returnFlag);}
控制台输出:

moved之后从8003上读取到了value,测试通过

(2)测试对迁移后的新key读取情况,将key替换为新的key
String key = "newkey{TEST_ASK}";
控制台输出:

move到8003尝试读取,服务端返回ask定向,最终在8001上读取到了value,测试通过

(3)测试客户端未发送asking的情况 注释asking发送
if (askHostAndPortMap.size()>0){askHostAndPortMap.clear();//清空askMap//jedis.asking();//上次返回了ask}
控制台输出:

由于8001没有接收到asking指令,请求被拒绝,节点使用 -MOVED 转向错误将命令请求转向至真正负责处理这个槽的节点8003
由于客户端始终不会发送asking指令,进入了死循环,测试通过

(4)测试slot迁移完毕后的情况
迁移slot

控制台输出:



发现slot迁移完毕后,直接在8001上读取到了value,默认连接8001所以无须重定向,测试通过。

二:客户端使用JedisCluster实例
经测试当客户端使用JedisCluster实例时不须要考虑MOVED和ASK转向,内部已经封装了对其的一些处理,直接调用即可
@Test
public void testJedisCluster() {try {String key = "newkey{TEST_ASK}";System.out.println(key+":"+jedisCluster.get(key));jedisCluster.close();} catch (IOException e) {e.printStackTrace();}
}

至此,slot迁移完毕,迁移中间状态的读取测试完毕。

这篇关于Redis Cluster 集群一致性原理及slot迁移测试的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1050343

相关文章

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

Redis MCP 安装与配置指南

《RedisMCP安装与配置指南》本文将详细介绍如何安装和配置RedisMCP,包括快速启动、源码安装、Docker安装、以及相关的配置参数和环境变量设置,感兴趣的朋友一起看看吧... 目录一、Redis MCP 简介二、安www.chinasem.cn装 Redis MCP 服务2.1 快速启动(推荐)2.

java如何实现高并发场景下三级缓存的数据一致性

《java如何实现高并发场景下三级缓存的数据一致性》这篇文章主要为大家详细介绍了java如何实现高并发场景下三级缓存的数据一致性,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 下面代码是一个使用Java和Redisson实现的三级缓存服务,主要功能包括:1.缓存结构:本地缓存:使

在MySQL中实现冷热数据分离的方法及使用场景底层原理解析

《在MySQL中实现冷热数据分离的方法及使用场景底层原理解析》MySQL冷热数据分离通过分表/分区策略、数据归档和索引优化,将频繁访问的热数据与冷数据分开存储,提升查询效率并降低存储成本,适用于高并发... 目录实现冷热数据分离1. 分表策略2. 使用分区表3. 数据归档与迁移在mysql中实现冷热数据分

Redis中Stream详解及应用小结

《Redis中Stream详解及应用小结》RedisStreams是Redis5.0引入的新功能,提供了一种类似于传统消息队列的机制,但具有更高的灵活性和可扩展性,本文给大家介绍Redis中Strea... 目录1. Redis Stream 概述2. Redis Stream 的基本操作2.1. XADD

MySQL 迁移至 Doris 最佳实践方案(最新整理)

《MySQL迁移至Doris最佳实践方案(最新整理)》本文将深入剖析三种经过实践验证的MySQL迁移至Doris的最佳方案,涵盖全量迁移、增量同步、混合迁移以及基于CDC(ChangeData... 目录一、China编程JDBC Catalog 联邦查询方案(适合跨库实时查询)1. 方案概述2. 环境要求3.

Jenkins分布式集群配置方式

《Jenkins分布式集群配置方式》:本文主要介绍Jenkins分布式集群配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1.安装jenkins2.配置集群总结Jenkins是一个开源项目,它提供了一个容易使用的持续集成系统,并且提供了大量的plugin满

Knife4j+Axios+Redis前后端分离架构下的 API 管理与会话方案(最新推荐)

《Knife4j+Axios+Redis前后端分离架构下的API管理与会话方案(最新推荐)》本文主要介绍了Swagger与Knife4j的配置要点、前后端对接方法以及分布式Session实现原理,... 目录一、Swagger 与 Knife4j 的深度理解及配置要点Knife4j 配置关键要点1.Spri

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语