redis核心篇(四)-高可用架构(二)之cluster

1.前言

通过上篇(redis核心篇(三)-高可用架构(一)之主从与哨兵)知道,哨兵可以在主从发生故障的时候,自动进行故障转移,保证了整个Redis服务的高可用;但是它的痛点是没有解决数据容量问题;随着业务规模的发展,Redis缓存的数据可能达到几十G甚至几百G;如果仅只能垂直拓展,简单增加机器内存,不仅让扩展成本大大增加,而且也会导致数据持久化异常缓慢,降低整个Redis服务RT;所以从redis3.0版本,官方提供了横向扩展的解决方案,通过引入cluster,将数据分散到不同节点上,来降低单实例的数据容量,达到数据分片目的。

2. cluster基本运行单元-节点node

老规矩,直接上图:通常一个 Redis 集群由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群

image.png

一个普通的redis服务如果想要成为cluster的节点,则必须在服务启动之前开启服务的集群模式:

cluster-enabled yes
复制代码

启动redis服务的时候,redis会检查cluster-enabled选项的值是否为YES,如果是,则开启服务器的集群模式,成为一个node,否则,则成为一个普通的Redis服务器;集群模式下的node除了会保持着有普通Redis服务器的特性,如:数据持久化,复制等特性等;还会有其它特有的一些特性,如clusterNode,clusterLink,clusterState等与集群相关的数据结构,这些数据结构的作用会在下面的集群实现原理做简要介绍。

细心小伙伴其实可能已经发现,仔细观察图右,redis的cluster架构模型其实也是一种去中心化的高可用模型。

2.1 节点关联

将原本不在同一个集群的节点,可以使用CLUSTER MEET命令来完成关联,该命令的格式如下:

CLUSTER MEET <ip> <port>
复制代码

如下图所示,通过向节点A发送CLUSTER MEET命令,客户端可以让接收命令的节点A将另一个节点B添加到节点A当前所在的集群里面,收到命令的节点A将于节点B进行握手。并为将来的进一步通信打好基础

image.png

在握手结束后,节点A和节点B的集群数据信息也将各自增加一份对方node信息,同时,节点A会将节点B的信息通过Gossip协议传播给集群中的其它节点,让其它节点也来与nodeB进行握手;最终,经过一段时间之后,节点B会被其集群它节点认识,节点B的node信息也会被存储在其它节点的集群数据信息里面。

3.cluster 实现原理

Redis 3.0 开始,redis官方提供了cluster集群方案来解决数据容量问题,该方案通过16384个hash槽(Hash Slot,接下来我会直接称之为 Slot)来实现数据与实例的映射关系,从而达到将数据分片,将数据均摊到集群中的各个节点中

3.1 hash solt

3.1.1 槽位指派

通过使用CLUSTER MEET命令可以将节点关联起来,但是关联起来的集群仍然无法向外提供服务,因为当前集群的16384槽位还没有被指派关联到对应的节点中;通过执行info命令查询,集群当前状态依旧处于下线状态:

image.png

通过向节点发送 CLUSTER ADDSLOTS 命令,可以将一个或者多个槽位分配给该节点:

redis-cli -h 127.0.0.1 –p 7000 cluster addslots 0,5460
redis-cli -h 127.0.0.1 –p 7001 cluster addslots 5461,10922
redis-cli -h 127.0.0.1 –p 7002 cluster addslots 10923,16383
复制代码

上述指令为每个实例分配哈希槽:实例 1负责 0 ~ 5460 哈希槽,实例 2 负责 5461~10922 哈希槽,实例 3 负责 10923 ~ 16383 哈希槽

image.png

当16384个槽都已经被指派给了相应的节点,集群进入上线状态,通过Info命令查询:

image.png

3.1.2 槽位信息存储·

在槽位指派完成之后,cluster各node都有了各自关联的槽位;关联的槽位信息存在clusterState结构中:

struct clusterNode {
    unsigned char slots[16384/8];
    int numsslots;
}
复制代码
  • numsslots:表示当前节点实例所管理的槽位个数。
  • slots:一个二进制位数组,数组长度2048(16384/8)个字节,包含16384个二进制位,从索引0开始,每一个二进制位表示一个槽位,如果索引对应的值为1,则表示该槽位被当前实例处理;0则表示不被该实例关联。

直接上图:这个数组索引1、3、5、8、9、10上的二进制位的值都为1,而其余所有二进制位的值都为0,这表示节点负责处理槽1、3、5、8、9、10。

image.png

3.1.3 槽位信息广播

一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽,

直接上图:节点1会将自己的槽位信息广播给节点2和节点3,同理,节点2和节点3也会广播自己的槽位信息:

image.png 集群中的每个节点都会将自己的槽位信息发送给集群中的其它节点,并且每个接收的节点都会将该信息保存在clusterNode结构里面:

typedef struct clusterNode {
    clusterNode *slots[13684];
}
复制代码

slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:

  • 如果slots[i]数组项为NULL,那么表示槽位i尚未指派给任何一个节点
  • 如果solots[i]指向一个clusterNode,则表示槽位i已经被指派给clusterNode所代表的节点。

3.1.4 hash槽重新分配

solt重新分配可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。

重新分配的操作不影响集群对我提供服务,可以在线进行,在此过程,源节点,目标节点都可以继续处理客户端请求。

重新分配由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分配所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分配操作。

image.png

  • 步骤1:对目标节点发送CLUSTER SETSLOT<slot>IMPORTING<source_id>命令,让目标节点准备好槽位x数据的迁出
  • 步骤2:对源节点发送CLUSTER SETSLOT<slot>MIGRATING<target_id>命令,让源节点准备好槽位x数据的迁入
  • 步骤3:redis-trib向源节点发送CLUSTER GETKEYSINSLOT<slot><count>命令,批量获取最多count个属于槽位x的键值对的键名
  • 步骤四:将步骤3获得的键值原子性的迁移到目标节点
  • 重复执行步骤3和步骤4,知道槽位x的数据迁移完毕

如果重新分配涉及多个槽,那么redis-trib将对每个给定的槽分别执行上面给出的步骤

3.1.5 MOVED 错误、ASK 错误

客户端在初次启动的时候,会从cluster集群拉取各个槽位信息,然后缓存到本地中;直接上图,为本地测试项目的启动日志:

image.png 当客户端请求时,先计算key的hash值,通过CRC16计算出一个值再对 16384 取模得到对应的Slot,然后再通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。

如果集群槽位信息发生变化,当客户端将请求发送到实例上,这个实例没有相应的数据,该Redis实例会返回ASK或者MOVE错误,来告诉客户端应该将请求发送到其他的实例上。

3.1.5.1 ASK错误

ASK错误,发生在Redis-trib在对槽位数据迁移的时候,如果一个 slot的数据比较多,部分迁移到目标节点,而另一部分的数据仍旧保存在源节点的情况:

如果请求的 key在当前节点找到就直接执行命令,否则就响应ASK错误:你先给node2发送一个 ASKING 命令,接着发送操作命令

image.png

ASK 错误指令并不会更新客户端缓存的哈希槽分配信息,所以当再请求槽位x的信息的时候,还是会先到node1实例。

3.1.5.2 MOVED 错误

MOVED 错误(负载均衡,数据已经迁移到其他实例上):当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点

同时,客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确

3.2 故障转移

3.2.1 故障检测

在高可用架构一篇介绍了哨兵的故障检测主要是通过监控实现的,cluster 又如何实现故障的自动检测呢?

3.2.1.1 PFAIL 疑似下线

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)

3.2.2 下线

一个节点认为疑似下线,并不代表所有节点都认为它失联了,Redis集群利用Gossip将这条失联消息广播给其它节点,如果节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。

3.2.2 自动切换主从

当一个 Slave 发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移,以下是故障转移的执行步骤

  • 选举master:从从节点列表挑选出一个从服务器,并将其转换为主服务器
  • 新主节点会撤销所有对已下线主节点的 slot 指派,并将这些 slots 指派给自己
  • 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽
  • 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成

选举新的master流程:

  • 集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。
  • 检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  • 这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  • 参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。
  • 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

其实看过上篇文章,关于哨兵的自动切换主从的小伙伴应该知道,这里选举新的master与选举领头sentinel非常相似,因为两者都是基于Raft算法的领头选举(leader election)方法来实现的。

4 集群规模

有了Cluster,是不是我们的Redis集群再也不用担心数据容量问题,可以无限水平扩容;答案是否定的,因为这里涉及到集群内部的消息通信开销,在Redis 官方经过大量的验证,给出规模上线是1000 个实例

集群内部的通信,主要有以下类型消息:

  • MEET消息:节点关联
  • PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线
  • PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识
  • FAIL消息:广播疑似下线的mater信息

4.1 Gossip协议消息体

cluster各个节点通过Gossip协议来交换各自关于不同节点的状态信息,Gossip协议的消息正文是clusterMsgDataGossip结构体组成:

typedef struct {
    char nodename[CLUSTER_NAMELEN];  //40字节
    uint32_t ping_sent; //4字节
    uint32_t pong_received; //4字节
    char ip[NET_IP_STR_LEN]; //46字节
    uint16_t port;  //2字节
    uint16_t cport;  //2字节
    uint16_t flags;  //2字节
    uint32_t notused1; //4字节
} clusterMsgDataGossip;
复制代码

消息的类型主要是通过消息头的type属性来判断是MEET消息、PING消息还是PONG消息。

所以每个实例发送一个 Gossip消息,就需要发送 104 字节。如果集群是 1000个实例,那么每个实例发送一个 PING 消息则会占用大约10KB,再加上回复PONG消息,就是20kb,集群规模的增加,心跳消息越来越多就会占据集群的网络通信带宽,降低了集群吞吐量。

5. 总结

  • 使用 Redis Cluster集群,可以解决哨兵无法解决的数据容量问题
  • 集群中的16384个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽又被指派给了其他节点
  • MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施,并不会更新客户端本地缓存
  • 集群中的节点通过发送和接收消息来进行通信,常见的消息包括MEET、PING、PONG、FAIL
  • 集群并不能无限增加,由于集群通过 Gossip协议传播集群实例信息,通信开销是限制集群大小的主要原因

猜你喜欢

转载自juejin.im/post/7053743219279921188