写点什么

Redis 集群模式,你若还是一知半解,试试仔细阅读一遍这篇文章

作者:李子捌
  • 2021 年 11 月 27 日
  • 本文字数:6801 字

    阅读完需:约 22 分钟

Redis集群模式,你若还是一知半解,试试仔细阅读一遍这篇文章

1、简介

Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片(sharding)进行数据共享,Redis 集群主要实现了以下目标:


  • 在 1000 个节点的时候仍能表现得很好并且可扩展性是线性的。

  • 没有合并操作(多个节点不存在相同的键),这样在 Redis 的数据模型中最典型的大数据值中也能有很好的表现。

  • 写入安全,那些与大多数节点相连的客户端所做的写入操作,系统尝试全部都保存下来。但是 Redis 无法保证数据完全不丢失,异步同步的主从复制无论如何都会存在数据丢失的情况。

  • 可用性,主节点不可用,从节点能替换主节点工作。


关于 Redis 集群的学习,如果没有任何经验的弟兄们建议先看下这三篇文章(中文系列):Redis 集群教程


http://redis.cn/topics/cluster-tutorial.html


Redis 集群规范


http://redis.cn/topics/cluster-spec.html


Redis3 主 3 从伪集群部署


https://blog.csdn.net/qq_41125219/article/details/118686281


下文内容依赖下图三主三从结构开展:



资源清单:



2、集群内部

Redis 集群没有使用一致性 hash, 而是引入了 哈希槽的概念。Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,这种结构很容易添加或者删除节点。集群的每个节点负责一部分 hash 槽,比如上面资源清单的集群有 3 个节点,其槽分配如下所示:


  • 节点 Master[0] 包含 0 到 5460 号哈希槽

  • 节点 Master[1] 包含 5461 到 10922 号哈希槽

  • 节点 Master[2] 包含 10923 到 16383 号哈希槽


深入学习 Redis 集群之前,需要了解集群中 Redis 实例的内部结构。当某个 Redis 服务节点通过 cluster_enabled 配置为 yes 开启集群模式之后,Redis 服务节点不仅会继续使用单机模式下的服务器组件,还会增加 custerState、clusterNode、custerLink 等结构用于存储集群模式下的特殊数据。​


如下三个数据承载对象一定要认真看,尤其是结构中的注释,看完之后集群大体上怎么工作的,心里就有数了,嘿嘿嘿;

2.1 clsuterNode

clsuterNode 用于存储节点信息,比如节点的名字、IP 地址、端口信息和配置纪元等等,以下代码列出部分非常重要的属性:


typedef struct clsuterNode {
// 创建时间 mstime_t ctime; // 节点名字,由40位随机16进制的字符组成(与sentinel中讲的服务器运行id相同) char name[REDIS_CLUSTER_NAMELEN]; // 节点标识,可以标识节点的角色和状态 // 角色 -> 主节点或从节点 例如:REDIS_NODE_MASTER(主节点) REDIS_NODE_SLAVE(从节点) // 状态 -> 在线或下线 例如:REDIS_NODE_PFAIL(疑似下线) REDIS_NODE_FAIL(下线) int flags; // 节点配置纪元,用于故障转移,与sentinel中用法类似 // clusterState中的代表集群的配置纪元 unit64_t configEpoch; // 节点IP地址 char ip[REDIS_IP_STR_LEN]; // 节点端口 int port; // 连接节点的信息 clusterLink *link; // 一个2048字节的二进制位数组 // 位数组索引值可能为0或1 // 数组索引i位置值为0,代表节点不负责处理槽i // 数组索引i位置值为1,代表节点负责处理槽i unsigned char slots[16384/8]; // 记录当前节点处理槽的数量总和 int numslots; // 如果当前节点是从节点 // 指向当前从节点的主节点 struct clusterNode *slaveof; // 如果当前节点是主节点 // 正在复制当前主节点的从节点数量 int numslaves; // 数组——记录正在复制当前主节点的所有从节点 struct clusterNode **slaves; } clsuterNode;
复制代码


上述代码中可能不太好理解的是 slots[16384/8],其实可以简单的理解为一个 16384 大小的数组,数组索引下标处如果为 1 表示当前槽属于当前 clusterNode 处理,如果为 0 表示不属于当前 clusterNode 处理。clusterNode 能够通过 slots 来识别,当前节点处理负责处理哪些槽。初始 clsuterNode 或者未分配槽的集群中的 clsuterNode 的 slots 如下所示:



假设集群如上面我给出的资源清单,此时代表 Master[0]的 clusterNode 的 slots 如下所示:


2.2 clusterLink

clusterLink 是 clsuterNode 中的一个属性,用于存储连接节点所需的相关信息,比如套接字描述符、输入输出缓冲区等待,以下代码列出部分非常重要的属性:


typedef struct clusterState {
// 连接创建时间 mstime_t ctime; // TCP 套接字描述符 int fd; // 输出缓冲区,需要发送给其他节点的消息缓存在这里 sds sndbuf; // 输入缓冲区,接收打其他节点的消息缓存在这里 sds rcvbuf; // 与当前clsuterNode节点代表的节点建立连接的其他节点保存在这里 struct clusterNode *node;} clusterState;
复制代码

2.3 custerState

每个节点都会有一个 custerState 结构,这个结构中存储了当前集群的全部数据,比如集群状态、集群中的所有节点信息(主节点、从节点)等等,以下代码列出部分非常重要的属性:


typedef struct clusterState {
// 当前节点指针,指向一个clusterNode clusterNode *myself; // 集群当前配置纪元,用于故障转移,与sentinel中用法类似 unit64_t currentEpoch; // 集群状态 在线/下线 int state; // 集群中处理着槽的节点数量总和 int size; // 集群节点字典,所有clusterNode包括自己 dict *node; // 集群中所有槽的指派信息 clsuterNode *slots[16384]; // 用于槽的重新分配——记录当前节点正在从其他节点导入的槽 clusterNode *importing_slots_from[16384]; // 用于槽的重新分配——记录当前节点正在迁移至其他节点的槽 clusterNode *migrating_slots_to[16384]; // ... } clusterState;
复制代码


在 custerState 有三个结构需要认真了解的,第一个是 slots 数组,clusterState 中的 slots 数组与 clsuterNode 中的 slots 数组是不一样的,在 clusterNode 中 slots 数组记录的是当前 clusterNode 所负责的槽,而 clusterState 中的 slots 数组记录的是整个集群的每个槽由哪个 clsuterNode 负责,因此集群正常工作的时候 clusterState 的 slots 数组每个索引指向负责该槽的 clusterNode,集群槽未分配之前指向 null。​


如图展示资源清单中的集群 clusterState 中的 slots 数组与 clsuterNode 中的 slots 数组:



Redis 集群中使用两个 slots 数组的原因是出于性能的考虑:


  • 当我们需要获取整个集群中 clusterNode 分别负责什么槽时,只需要查询 clusterState 中的 slots 数组即可。如果没有 clusterState 的 slots 数组,则需要遍历所有的 clusterNode 结构,这样显然要慢一些

  • 此外 clusterNode 中的 slots 数组也有存在的必要,因为集群中任意一个节点之间需要知道彼此负责的槽,此时节点之间只需要互相传输 clusterNode 中的 slots 数组结构就行。


第二个需要认真了解的结构是 node 字典,该结构虽然简单,但是 node 字典中存储了所有的 clusterNode,这也是 Redis 集群中的单个节点获取其他主节点、从节点信息的主要位置,因此我们也需要注意一下。第三个需要认真了解的结构是 importing_slots_from[16384]数组和 migrating_slots_to[16384],这两个数组在集群重新分片时需要使用,需要重点了解,后面再说吧,这里说的话顺序不太对。​

3、集群工作

3.1 槽(slot)如何指派?

Redis 集群一共 16384 个槽,如上资源清单我们在三主三从的集群中,每个主节点负责自己相应的槽,而在上面的三主三从部署的过程中并未看到我指定槽给对应的主节点,这是因为 Redis 集群自己内部给我们划分了槽,但是如果我们想自己指派槽该如何整呢?我们可以向节点发送如下命令,将一个或多个槽指派给当前节点负责:


CLUSTER ADDSLOTS


比如我们想把 0 和 1 槽指派给 Master[0],我们只需要想 Master[0]节点发送如下命令即可:


CLUSTER ADDSLOTS 0 1


当节点被指派了槽后,会将 clusterNode 的 slots 数组更新,节点会将自己负责处理的槽也就是 slots 数组通过消息发送给集群中的其他节点,其他节点在接收当消息后会更新对应 clusterNode 的 slots 数组以及 clusterState 的 solts 数组。​

3.2 ADDSLOTS 在 Redis 集群内部是如何实现的呢?

这个其实也比较简单,当我们向 Redis 集群中的某个节点发送 CLUSTER ADDSLOTS 命令时,当前节点首先会通过 clusterState 中的 slots 数组来确认指派给当前节点的槽是否没有指派给其他节点,如果已经指派了,那么会直接抛出异常,返回错误给指派的客户端。如果指派给当前节点的所有槽都未指派给其他节点,那么当前节点会将这些槽指派给自己。指派主要有三个步骤:


  1. 更新 clusterState 的 slots 数组,将指定槽 slots[i]指向当前 clusterNode

  2. 更新 clusterNode 的 slots 数组,将指定槽 slots[i]处的值更新为 1

  3. 向集群中的其他节点发送消息,将 clusterNode 的 slots 数组发送给其他节点,其他节点接收到消息后也更新对应的 clusterState 的 slots 数组和 clusterNode 的 slots 数组

3.3 集群这么多节点,客户端怎么知道请求哪个节点?

在了解这个问题之前先要知道一个点,Redis 集群是怎么计算当前这个键属于哪个槽的呢?根据官网的介绍,Redis 其实并未使用一致性 hash 算法,而是将每个请求的 key 通过 CRC16 校验后对 16384 取模来决定放置到哪个槽中。


HASH_SLOT = CRC16(key) mod 16384


此时,当客户端连接向某个节点发送请求时,当前接收到命令的节点首先会通过算法计算出当前 key 所属的槽 i,计算完后当前节点会判断 clusterState 的槽 i 是否由自己负责,如果恰好由自己负责那么当前节点就会之间响应客户端的请求,如果不由当前节点负责,则会经历如下步骤:


  1. 节点向客户端返回 MOVED 重定向错误,MOVED 重定向错误中会将计算好的正确处理该 key 的 clusterNode 的 ip 和 port 返回给客户端

  2. 客户端接收到节点返回的 MOVED 重定向错误时,会根据 ip 和 port 将命令转发给正确的节点,整个处理过程对程序员来说透明,由 Redis 集群的服务端和客户端共同负责完成。

3.4 如果我想将已经分配给 A 节点的槽重新分配给 B 节点,怎么整?

这个问题其实涵括了很多问题,比如移除 Redis 集群中的某些节点,增加节点等都可以概括为把哈希槽从一个节点移动到另外一个节点。并且 Redis 集群非常牛逼的一点也在这里,它支持在线(不停机)的分配,也就是官方说集群在线重配置(live reconfiguration )。​


在将实现之前先来看下 CLUSTER 的指令,指令会了操作就会了:


  • CLUSTER ADDSLOTS slot1 [slot2] … [slotN]

  • CLUSTER DELSLOTS slot1 [slot2] … [slotN]

  • CLUSTER SETSLOT slot NODE node

  • CLUSTER SETSLOT slot MIGRATING node

  • CLUSTER SETSLOT slot IMPORTING node


CLUSTER 用于槽分配的指令主要有如上这些,ADDSLOTS 和 DELSLOTS 主要用于槽的快速指派和快速删除,通常我们在集群刚刚建立的时候进行快速分配的时候才使用。CLUSTER SETSLOT slot NODE node 也用于直接给指定的节点指派槽。如果集群已经建立我们通常使用最后两个来重分配,其代表的含义如下所示:


  • 当一个槽被设置为 MIGRATING,原来持有该哈希槽的节点仍会接受所有跟这个哈希槽有关的请求,但只有当查询的键还存在原节点时,原节点会处理该请求,否则这个查询会通过一个 -ASK 重定向(-ASK redirection)转发到迁移的目标节点。

  • 当一个槽被设置为 IMPORTING,只有在接受到 ASKING 命令之后节点才会接受所有查询这个哈希槽的请求。如果客户端一直没有发送 ASKING 命令,那么查询都会通过 -MOVED 重定向错误转发到真正处理这个哈希槽的节点那里。


上面这两句话是不是感觉不太看的懂,这是官方的描述,不太懂的话我来给你通俗的描述,整个流程大致如下步骤:


  1. redis-trib(集群管理软件 redis-trib 会负责 Redis 集群的槽分配工作),向目标节点(槽导入节点)发送 CLUSTER SETSLOT slot IMPORTING node 命令,目标节点会做好从源节点(槽导出节点)导入槽的准备工作。

  2. redis-trib 随即向源节点发送 CLUSTER SETSLOT slot MIGRATING node 命令,源节点会做好槽导出准备工作

  3. redis-trib 随即向源节点发送 CLUSTER GETKEYSINSLOT slot count 命令,源节点接收命令后会返回属于槽 slot 的键,最多返回 count 个键

  4. redis-trib 会根据源节点返回的键向源节点依次发送 MIGRATE ip port key 0 timeout 命令,如果 key 在源节点中,将会迁移至目标节点。

  5. 迁移完成之后,redis-trib 会向集群中的某个节点发送 CLUSTER SETSLOT slot NODE node 命令,节点接收到命令后会更新 clusterNode 和 clusterState 结构,然后节点通过消息传播槽的指派信息,至此集群槽迁移工作完成,且集群中的其他节点也更新了新的槽分配信息。

3.5 如果客户端访问的 key 所属的槽正在迁移怎么办?

优秀的你总会想到这种并发情况,牛皮呀!大佬们!



这个问题官方也考虑了,还记得我们在聊 clusterState 结构的时候么?importing_slots_from 和 migrating_slots_to 就是用来处理这个问题的。


typedef struct clusterState {
// ... // 用于槽的重新分配——记录当前节点正在从其他节点导入的槽 clusterNode *importing_slots_from[16384]; // 用于槽的重新分配——记录当前节点正在迁移至其他节点的槽 clusterNode *migrating_slots_to[16384]; // ... } clusterState;
复制代码


  • 当节点正在导出某个槽,则会在 clusterState 中的 migrating_slots_to 数组对应的下标处设置其指向对应的 clusterNode,这个 clusterNode 会指向导入的节点。

  • 当节点正在导入某个槽,则会在 clusterState 中的 importing_slots_from 数组对应的下标处设置其指向对应的 clusterNode,这个 clusterNode 会指向导出的节点。


有了上述两个相互数组,就能判断当前槽是否在迁移了,而且从哪里迁移来,要迁移到哪里去?搞笑不就是这么简单……


此时,回到问题中,如果客户端请求的 key 刚好属于正在迁移的槽。那么接收到命令的节点首先会尝试在自己的数据库中查找键 key,如果这个槽还没迁移完成,且当前 key 刚好也还没迁移完成,那就直接响应客户端的请求就行。如果该 key 已经不在了,此时节点会去查询 migrating_slots_to 数组对应的索引槽,如果索引处的值不为 null,而是指向了某个 clusterNode 结构,那说明这个 key 已经被迁移到这个 clusterNode 了。这个时候节点不会继续在处理指令,而是返回 ASKING 命令,这个命令也会携带导入槽 clusterNode 对应的 ip 和 port。客户端在接收到 ASKING 命令之后就需要将请求转向正确的节点了,不过这里有一点需要注意的地方**(因此我放个表情包在这里,方便读者注意)。**



前面说了,当节点发现当前槽不属于自己处理时会返回 MOVED 指令,那么在迁移中的槽时怎么处理的呢?这个 Redis 集群是这个玩的。节点发现槽正在迁移则向客户端返回 ASKING 命令,客户端会接收到 ASKING 命令,其中包含了槽迁入的 clusterNode 的节点 ip 和 port。那么客户端首先会向迁入的 clusterNode 发送一条 ASKING 命令,这个命令必须要发目的是告诉当前节点,你要破例处理这次请求,因为这个槽已经迁移到你这里了,你不能直接拒绝我(因此如果 Redis 未接收到 ASKING 命令,会直接查询节点的 clusterState,而正在迁移中的槽还没有更新到 clusterState 中,那么只能直接返回 MOVED,这样不就会一直循环很多次……),接收到 ASKING 命令的节点会强制执行一次这个请求(只执行一次,下次再来需要重新提前发送 ASKING 命令)。​

4、集群故障

Redis 集群故障比较简单,这个和 sentinel 中主节点宕机或者在指定最长时间内未响应,重新在从节点中选举新的主节点的方式其实差不多。当然前提是 Redis 集群中的每个主节点,我们提前设置了从节点,要不就嘿嘿嘿……没戏。其大致步骤如下:


  1. 正常工作的集群,每个节点之间会定期向其他节点发送 PING 命令,如果接收命令的节点未在规定时间内返回 PONG 消息 ,当前节点会将接收命令的节点的 clusterNode 的 flags 设置为 REDIS_NODE_PFAIL,PFAIL 并不是下线,而是疑似下线。

  2. 集群节点会通过发送消息的方式来告知其他节点,集群中各个节点的状态信息

  3. 如果集群中半数以上负责处理槽的主节点都将某个主节点设置为疑似下线,那么这个节点将会被标记位下线状态,节点会将接收命令的节点的 clusterNode 的 flags 设置为 REDIS_NODE_FAIL,FAIL 表示已下线

  4. 集群节点通过发送消息的方式来告知其他节点,集群中各个节点的状态信息,此时下线节点的从节点在发现自己的主节点已经被标记为下线状态了,那么是时候挺身而出了

  5. 下线主节点的从节点,会选举出一个从节点作为最新的主节点,执行被选中的节点指向 SLAVEOF no one 成为新的主节点

  6. 新的主节点会撤销掉原主节点的槽指派,并将这些槽指派修改为自己,也就是修改 clusterNode 结构和 clusterState 结构

  7. 新的主节点向集群广播一条 PONG 指令,其他节点将会知道有新的主节点产生,并更新 clusterNode 结构和 clusterState 结构

  8. 新的主节点如果会向原主节点剩余的从节点发送新的 SLAVEOF 指令,使其成为自己的从节点

  9. 最后新的主节点将会负责原主节点的槽的响应工作


这里我写得非常模糊,如果需要细致挖掘的一定要看这篇文章:


http://redis.cn/topics/cluster-spec.html


或者可以看下黄健宏老师的《Redis 设计与实现》这本书写得挺好,我也参考了很多内容。

发布于: 7 小时前阅读数: 6
用户头像

李子捌

关注

华为云享专家 2020.07.20 加入

公众号【李子捌】

评论

发布
暂无评论
Redis集群模式,你若还是一知半解,试试仔细阅读一遍这篇文章