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

相关文章

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

Redis 中的热点键和数据倾斜示例详解

《Redis中的热点键和数据倾斜示例详解》热点键是指在Redis中被频繁访问的特定键,这些键由于其高访问频率,可能导致Redis服务器的性能问题,尤其是在高并发场景下,本文给大家介绍Redis中的热... 目录Redis 中的热点键和数据倾斜热点键(Hot Key)定义特点应对策略示例数据倾斜(Data S

redis+lua实现分布式限流的示例

《redis+lua实现分布式限流的示例》本文主要介绍了redis+lua实现分布式限流的示例,可以实现复杂的限流逻辑,如滑动窗口限流,并且避免了多步操作导致的并发问题,具有一定的参考价值,感兴趣的可... 目录为什么使用Redis+Lua实现分布式限流使用ZSET也可以实现限流,为什么选择lua的方式实现

Redis中管道操作pipeline的实现

《Redis中管道操作pipeline的实现》RedisPipeline是一种优化客户端与服务器通信的技术,通过批量发送和接收命令减少网络往返次数,提高命令执行效率,本文就来介绍一下Redis中管道操... 目录什么是pipeline场景一:我要向Redis新增大批量的数据分批处理事务( MULTI/EXE

Redis中高并发读写性能的深度解析与优化

《Redis中高并发读写性能的深度解析与优化》Redis作为一款高性能的内存数据库,广泛应用于缓存、消息队列、实时统计等场景,本文将深入探讨Redis的读写并发能力,感兴趣的小伙伴可以了解下... 目录引言一、Redis 并发能力概述1.1 Redis 的读写性能1.2 影响 Redis 并发能力的因素二、

Redis中的常用的五种数据类型详解

《Redis中的常用的五种数据类型详解》:本文主要介绍Redis中的常用的五种数据类型详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Redis常用的五种数据类型一、字符串(String)简介常用命令应用场景二、哈希(Hash)简介常用命令应用场景三、列表(L

Redis解决缓存击穿问题的两种方法

《Redis解决缓存击穿问题的两种方法》缓存击穿问题也叫热点Key问题,就是⼀个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击,本文给大家介绍了Re... 目录引言解决办法互斥锁(强一致,性能差)逻辑过期(高可用,性能优)设计逻辑过期时间引言缓存击穿:给

Redis中如何实现商品秒杀

《Redis中如何实现商品秒杀》:本文主要介绍Redis中如何实现商品秒杀问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录技术栈功能实现步骤步骤一:准备商品库存数据步骤二:实现商品秒杀步骤三:优化Redis性能技术讲解Redis的List类型Redis的Set