分享:Redis集群详述(面试官再怎么问也能轻轻松松!)

Redis/缓存系统
474
0
0
2022-07-10

1、简介

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

  • 在1000个节点的时候仍能表现得很好并且可扩展性是线性的。
  • 没有合并操作(多个节点不存在相同的键),这样在 Redis 的数据模型中最典型的大数据值中也能有很好的表现。
  • 写入安全,那些与大多数节点相连的客户端所做的写入操作,系统尝试全部都保存下来。但是Redis无法保证数据完全不丢失,异步同步的主从复制无论如何都会存在数据丢失的情况。
  • 可用性,主节点不可用,从节点能替换主节点工作。

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

Redis集群教程

redis.cn/topics/clus…

Redis集群规范

redis.cn/topics/clus…

Redis3主3从伪集群部署

blog.csdn.net/qq_41125219…

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

8624ecbdf7f691d78ef48bc5afdb6d18.png

资源清单:

节点 IP 槽(slot)范围 Master[0] 192.168.211.107:6319 Slots 0 - 5460 Master[1] 192.168.211.107:6329 Slots 5461 - 10922 Master[2] 192.168.211.107:6339 Slots 10923 - 16383 Slave[0] 192.168.211.107:6369 Slave[1] 192.168.211.107:6349 Slave[2] 192.168.211.107:6359

Redis集群.png

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如下所示:

初始slots[16384_8].png

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

Master[0]的clusterNode的slots.png

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数组:

clusterState中Slots数组.png

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所属的槽正在迁移怎么办?

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

u=79087421,2199932123&fm=26&fmt=auto.webp

这个问题官方也考虑了,还记得我们在聊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. 最后新的主节点将会负责原主节点的槽的响应工作

作者:李子捌

链接:juejin.cn/post/7016865316240097287