Redis Cluster

简述

Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,每一个分片由一个主从架构负责,提供了复制和故障转移功能。

启动集群节点

一个集群节点其实就是运行在一个运行在集群模式下的Redis服务器,通过以下配置开启Redis服务器的集群模式:

cluster-enabled yes

开启集群模式之后,Redis服务器会继续使用单机模式下的服务器功能,会继续使用redisServer来保存服务器的运行状态,与集群相关的数据则会保存到clusterState和clusterNode结构中。
在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
连接各个节点的工作可以使用CLUSTER MEET命令来完成,该命令的格式如下:

CLUSTER MEET <ip> <port>

向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。

clusterNode和clusterState

clusterNode保存了一个集群节点的信息,如创建时间、节点名称、纪元、ip和port等。
每一个集群节点启动的时候都会用一个clusterNode结构保存自己的状态,并且会为集群中的启动节点(主节点和从节点)也创建一个相应的clusterNode结构记录状态。

struct clusterNode {
    //创建节点的时间
    mstime_t ctime;
    //节点的名字,由40个十六进制字符组成
    //例如68eef66df23420a5862208ef5b1a7005b806f2ff
    char name[REDIS_CLUSTER_NAMELEN ];
    //节点标识
    //使用各种不间的标识值记录节点的角色(比如主节点或者从节点),
    //以及节点目前所处的状态(比如在线或者下线)。
    int flags;
    //节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;
    //节点的IP地址
    char ip[REDIS_IP_STR_LEN];
    //节点的端口号
    int port;

    // ...
};

每个集群节点都保存着一个clusterstate结构(保存整个集群的状态信息),这个结构记录了在当前节点的视角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元,诸如此类。

typedef struct clusterstate {
    //指向当前节点的指针
    clusterNode *myself;
    //集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;
    //集群当前的状态:是在线还是下线
    int state;
    //集群中至少处理着一个槽的节点的数量
    int size;
    //集群节点名单(包括myself节点)
    //字典的键为节点的名字,字典的值为节点对应的clusterNode结构
    dict *nodes;

    //...
}clusterstate;

在这里插入图片描述

槽 slot

Redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。
可以通过命令给节点添加处理的slot:

CLUSTERADDSLOTS <slot> [slot ...]

例如,在节点执行以下命令,意思是把0-5000的槽交给该节点进行处理。

CLUSTERADDSLOTS 0 1 2 3 4 ... 5000

节点记录自己处理的槽信息

每一个clusterNode结构都会使用slots属性和numslot属性来记录了节点负责处理哪些槽:

struct clusterNode {
    // ...
    unsigned char slots [16384/8];
    int numslots;
    // ...
};

slots属性是一个二进制位数组 ( bit array ),这个数组的长度为16384/8=2048个字共包含16384个二进制位。
Redis以0为起始索引,16383为终止索引,对slots数组中的16384个二进制位进行号,并根据索引i上的二进制位的值来判断节点是否负责处理槽i,如果索引i上的二进制位为1,则代表节点负责处理槽i。
对应一个给定节点的slots数组来说,程序检查节点是否负责处理某个槽,又或者将某个槽指派给节点负责,这两个动作的复杂度都是O(1)。
numslots记录了节点处理的总槽数。

节点传播自己的槽信息

集群节点会向其他的节点传播自己的slots和numslots,以此来告知其他节点自己负责的槽信息(16384 = 2KB)。
为什么是16384(2^14)个槽位?
在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,虽然使用CRC16算法最多可以分配65535个槽位(8KB),设计者认为一般情况下一个redis集群不会有超过1000个master节点,所以16384的槽位是个比较合适的选择。

clusterState记录所有槽的指派信息

通过clusterNode的slots数组可以很快定位节点是否负责处理某个槽,但是要查找一个槽负责的节点是哪个的时候,就不得不去遍历所有的clusterNode才能确认,以此Redis服务器在clusterState结构中记录所有槽的指派信息。
使用clusterState的slots数组可以在O(1)的时间复杂度里确认槽对应的处理节点。

typedef struct clusterstate {
    //...
    clusterNode *slots [16384];
    //...
) clusterstate;
  • slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针:如果slots [i]指针指向NULL,那么表示槽i尚未指派给任何节点,此时集群处于下线状态。
  • 如果slots [i]指针指向一个clusterNode结构,那么表示槽i已经指派给了clusterNode结构所代表的节点。

命令CLUSTERADDSLOTS的实现

在当前节点执行CLUSTERADDSLOTS命令时,首先会根据命令给出的槽去查看clusterstate的slots数组,看看指定的槽是不是指向都为null,如果不是就报错,如果是,就把指针指向当前节点,并且把当前节点的clusterNode结构的slots和numslots进行相应的修改,处理完成之后,当前节点会发送信息通知集群中的其他节点自己当前处理槽的信息。

MOVED错误

当我们在集群中处理操作数据的命令的时候,会经过以下几个步骤:

  1. 计算key属于哪个槽位
  2. 判断槽位是否由当前节点负责
  3. 如果不是,则要返回MOVED错误给客户端(MOVED错误携带目标节点的信息)
  4. 客户端收到MOVED错误之后,要自己转向目标节点

一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是换一个套接字来发送命令。如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据MOVED错误提供的IP地址和端口号来连接节点,然后再进行转向。

重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。重新分片操作可以在线(online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。

原理

在这里插入图片描述
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽slot进行重新分片的步骤如下:

  1. redis-trib对目标节点发送CLUSTER SETSLor IMPORTING <source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
  2. redis-trib对源节点发送CLUSTER SETSLOT MTGRATING <target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。
  3. redis-trib向源节点发送CLUSTER GETKEYSINSLoT 命令,获得最多count个属于槽slot的键值对的键名(key name )。
  4. 对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip> <target_port> <key_name> 0 命令,将被选中的键原子地从源节点迁移至目标节点。
  5. 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。每次迁移键的过程如图17-24所示。
  6. redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT NODE<target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。

快速定位属于某一个槽的key

在clusterstate中使用了一个跳跃表来保存slot和key之间的映射关系。

typedef struct clusterstate {
    // ...
    zskiplist *slots_to_keys;
    // ...
) clusterstate;

该跳表以slot作为score,以key作为member,通过跳表可以快速的得到一个slot下的所有key。

ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:

  • 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
  • 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
    在这里插入图片描述

ASK错误和MOVED错误的区别

ASK错误和 MOVED错误都会导致客户端转向,它们的区别在于:

  1. MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
  2. 与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。

故障转移

设置从节点

向一个节点发送命令:

CLUSTER REPLICATE <node_id>

可以让接收命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制。
接收到该命令的节点首先会在自己的clusterstate.nodes字典中找到node_id所对应节点的clusterNode结构,并将自己的clusterstate.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点。

struct clusterNode {
    // ...

    //如果这是一个从节点,那么指向主节点
    struct clusterNode *slaveof;

    // ...
};

clusterState中的nodes字典保存了主节点的信息,而主节点的clusterNode结构又保存自己的从节点信息。

struct clusterNode {
    // ...

    // 正在复制这个主节点的从节点数量
    int numslaves;
    // 一个数组
    // 每个数组项指向一个正在复制这个主节点的从节点的clusterNode结构
    struct clusterNode **slaves;

    // ...
};

故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,例如某个节点是处于在线状态、疑似下线状态( PFAIL ),还是已下线状态(FAIL )。
如果在一个集群里面,半数以上负责处理槽的主节点(包括下线节点)都将某个主节点x报告为疑似下线,那么这个主节点×将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中。
  2. 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  4. 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
  5. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举

新的主节点是通过选举产生的,以下是集群选举新的主节点的方法:

  1. 集群的配置纪元是一个自增计数器,它的初始值为0。
  2. 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
  3. 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
  4. 当从节点发现自己正在复制的主节点进人已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  5. 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  6. 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
  7. 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
  8. 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
  9. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

这个选举新主节点的方法和第16章介绍的选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法的领头选举(leader election)方法来实现的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值