您的位置:首页 > 数据库 > Redis

Redis(开发与运维):50---集群之(请求路由:请求重定向(MOVED)、ASK重定向)

2020-05-31 23:46 363 查看

一、请求重定向

MOVED重定向

  • 概念:在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。如下图所示

演示案例

  • 例如,在之前搭建的集群上执行如下命令:
[code]127.0.0.1:6379> set key:test:1 value-1
OK
  • 执行set命令成功,因为键key:test:1对应槽5191正好位于6379节点负责的槽范围内,可以借助cluster keyslot {key}命令返回key所对应的槽,如下所示:
[code]127.0.0.1:6379> cluster keyslot key:test:1
(integer) 5191
127.0.0.1:6379> cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 10 connected
1366-4095 4097-5461 12288-13652
...
  • 再执行以下命令,由于键对应槽是9252,不属于6379节点,则回复MOVED {slot} {ip} {port}格式重定向信息:
[code]127.0.0.1:6379> set key:test:2 value-2
(error) MOVED 9252 127.0.0.1:6380
127.0.0.1:6379> cluster keyslot key:test:2
(integer) 9252
  • 重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。在6380节点上成功执行之前的命令:
[code]127.0.0.1:6380> set key:test:2 value-2
OK
  • 使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作,如下所示:
[code]#redis-cli -p 6379 -c
127.0.0.1:6379> set key:test:2 value-2
-> Redirected to slot [9252] located at 127.0.0.1:6380
OK
  • redis-cli自动帮我们连接到正确的节点执行命令,这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发,如下图所示

  • 节点对于不属于它的键命令只回复重定向响应,并不负责转发。熟悉Cassandra的用户希望在这里做好区分,不要混淆。正因为集群模式下把解析发起重定向的过程放到客户端完成,所以集群客户端协议相对于单机有了很大的变化
  • 键命令执行步骤主要分两步: 计算槽
  • 查找槽所对应的节点

①计算槽

  • Redis首先需要计算键所对应的槽。根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数,使每个键都可以映射到0~16383槽范围内。伪代码如下:
[code]def key_hash_slot(key):
int keylen = key.length();
for (s = 0; s < keylen; s++):
if (key[s] == '{'):
break;
if (s == keylen) return crc16(key,keylen) & 16383;
for (e = s+1; e < keylen; e++):
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
/* 使用{和}之间的有效部分计算槽 */
return crc16(key+s+1,e-s-1) & 16383;
  • 根据伪代码,如果键内容包含{和}大括号字符,则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽
  • cluster keyslot命令就是采用key_hash_slot函数实现的,例如:
[code]127.0.0.1:6379> cluster keyslot key:test:111
(integer) 10050
127.0.0.1:6379> cluster keyslot key:{hash_tag}:111
(integer) 2515
127.0.0.1:6379> cluster keyslot key:{hash_tag}:222
(integer) 2515
  • 其中键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化
  • 例如在集群模式下使用mget等命令优化批量调用时,键列表必须具有相同的slot,否则会报错。这时可以利用hash_tag让不同的键具有相同的slot达到优化的目的。命令如下:
[code]127.0.0.1:6385> mget user:10086:frends user:10086:videos
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:6385> mget user:{10086}:friends user:{10086}:videos
1) "friends"
2) "videos"
  • 开发提示:Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛

②槽节点查找

  • Redis计算得到键对应的槽后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息,内部保存在clusterState结构中,结构所示:
[code]typedef struct clusterState {
clusterNode *myself; /* 自身节点,clusterNode代表节点结构体 */
clusterNode *slots[CLUSTER_SLOTS]; /* 16384个槽和节点映射数组,数组下标代表对应的槽 */
...
} clusterState;
  • slots数组表示槽和节点对应关系,实现请求重定向伪代码如下:
[code]def execute_or_redirect(key):
int slot = key_hash_slot(key);
ClusterNode node = slots[slot];
if(node == clusterState.myself):
return executeCommand(key);
else:
return '(error) MOVED {slot} {node.ip}:{node.port}';
  • 根据伪代码看出节点对于判定键命令是执行还是MOVED重定向,都是借助slots [CLUSTER_SLOTS]数组实现。根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据 重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销, 这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一 种实现:Smart(智能)客户端

二、Smart客户端

待续

三、ASK重定向

①客户端ASK重定向流程

  • Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点,如下图所示

  • 当出现上述情况时,客户端键命令执行流程将发生变化,如下所示: 1)客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直 接执行并返回结果给客户端
  • 2)如果键对象不存在,则可能存在于目标节点,这时源节点会回复 ASK重定向异常。格式如下:(error) ASK {slot} {targetIP}:{targetPort}
  • 3)客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返 回不存在信息
  • ASK重定向整体流程如下图所示:
    • ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别: ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移 完成,因此只能是临时性的重定向,客户端不会更新slots缓存
    • 但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存

    ②节点内部处理

    • 为了支持ASK重定向,源节点和目标节点在内部的clusterState结构中维 护当前正在迁移的槽信息,用于识别槽迁移情况,结构如下:
    [code]typedef struct clusterState {
    clusterNode *myself; /* 自身节点 /
    clusterNode *slots[CLUSTER_SLOTS]; /* 槽和节点映射数组 */
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];/* 正在迁出的槽节点数组 */
    clusterNode *importing_slots_from[CLUSTER_SLOTS];/* 正在迁入的槽节点数组*/
    ...
    } clusterState;
    • 节点每次接收到键命令时,都会根据clusterState内的迁移属性进行命令 处理,如下所示: 如果键所在的槽由当前节点负责,但键不存在则查找migrating_slots_to 数组查看槽是否正在迁出,如果是返回ASK重定向
    • 如果客户端发送asking命令打开了CLIENT_ASKING标识,则该客户端 下次发送键命令时查找importing_slots_from数组获取clusterNode,如果指向 自身则执行命令
    • 需要注意的是,asking命令是一次性命令,每次执行完后客户端标识都 会修改回原状态,因此每次客户端接收到ASK重定向后都需要发送asking命令
    • 批量操作。ASK重定向对单键命令支持得很完善,但是,在开发中我 们经常使用批量操作,如mget或pipeline。当槽处于迁移状态时,批量操作会受到影响
  • 例如,手动使用迁移命令让槽4096处于迁移状态,并且数据各自分散在 目标节点和源节点,如下所示:
  • [code]#6379节点准备导入槽4096数据
    127.0.0.1:6379>cluster setslot 4096 importing 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756
    OK
    
    #6385节点准备导出槽4096数据
    127.0.0.1:6379>cluster setslot 4096 migrating cfb28ef1deee4e0fa78da86abe5d24566744411e
    OK
    
    # 查看槽4096下的数据
    127.0.0.1:6385> cluster getkeysinslot 4096 100
    1) "key:test:5028"
    2) "key:test:68253"
    3) "key:test:79212"
    
    # 迁移键key:test:68253和key:test:79212到6379节点
    127.0.0.1:6385>migrate 127.0.0.1 6379 "" 0 5000 keys key:test:68253 key:test:79212
    OK
    • 现在槽4096下3个键数据分别位于6379和6380两个节点,使用Jedis客户 端执行批量操作。mget代码如下:
    [code]@Test
    public void mgetOnAskTest() {
    JedisCluster jedisCluster = new JedisCluster(new HostAndPort("127.0.0.1", 6379));
    List<String> results = jedisCluster.mget("key:test:68253", "key:test:79212");
    System.out.println(results);
    results = jedisCluster.mget("key:test:5028", "key:test:68253", "key:test:79212");
    System.out.println(results);
    }
    • 运行mget测试结果如下: 第1个mget运行成功,这是因为键key:test:68253,key:test:79212 已经迁移到目标节点,当mget键列表都处于源节点/目标节点时,运行成功
    • 第2个mget抛出异常,当键列表中任何键不存在于源节点时,抛出异常

    • 综上所处,当在集群环境下使用mget、mset等批量操作时,slot迁移数据期间由于键列表无法保证在同一节点,会导致大量错误
    • Pipeline代码如下:

    • Pipeline的代码中,由于Jedis没有开放slot到Jedis的查询,使用了匿名内 部类暴露JedisSlotBasedConnectionHandler。通过Jedis获取Pipeline对象组合3 条get命令一次发送。运行结果如下:

    • 结果分析:返回结果并没有直接抛出异常,而是把ASK异常 JedisAskDataException包含在结果集中。但是使用Pipeline的批量操作也无法 支持由于slot迁移导致的键列表跨节点问题
    • 得益于Pipeline并没有直接抛出异常,可以借助于JedisAskDataException 内返回的目标节点信息,手动重定向请求给目标节点,修改后的程序如下:

    • 修改后的Pipeline运行结果以下:

    • 根据结果,我们成功获取到了3个键的数据。以上测试能够成功的前提 是: 1)Pipeline严格按照键发送的顺序返回结果,即使出现异常也是如此 (更多细节见“Pipeline”:https://blog.csdn.net/qq_41453285/article/details/106060154
    • 2)理解ASK重定向之后,可以手动发起ASK流程保证Pipeline的结果正 确性
  • 综上所处,使用smart客户端批量操作集群时,需要评估mget/mset、 Pipeline等方式在slot迁移场景下的容错性,防止集群迁移造成大量错误和数 据丢失的情况
  • 开发提示:集群环境下对于使用批量操作的场景,建议优先使用Pipeline方式,在 客户端实现对ASK重定向的正确处理,这样既可以受益于批量操作的IO优 化,又可以兼容slot迁移场景
  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: