redis-集群

集群

Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移的功能

Redis集群是一种AP模型,它旨在提供高可用性和分区容错性, 集群内部是通过Gossip协议实现数据一致性的,存在以下几种通信信息

  • MEET 加入集群
  • PING 心跳检测
  • PONG 心跳回应
  • FAIL 主观下线

启动集群

启动节点

Redis服务器在启动时会根据cluster-enabled配置选项确定是否开启集群模式,集群模式主要多了cluseterNode等结构保存集群信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct clusterNode {
// 创建节点实践
mstime_t ctime;
// 节点名字
char name[REDIS_CLUSTER_NAMELEN];
...
// 保存连接节点的一些信息
clusterLink *link;
}

typedef struct clusterLink {

// 连接创建实践
mstime_t ctime;
// TCP 套接字描述符
int fd;
// 输出缓冲区,保存着等待发送给其他节点的信息
sds sndbuf;
// 输入缓冲区,保存着从其他节点接收到的信息
sds rcvbuf;
// 与这个连接相关的节点
clusterNode *node;
} clusterLink;

// 集群状态
typedef struct clusterState {
clusterNode *myself;
...
// 集群节点名单
dict *nodes;
} clusterState;

MEET命令实现

Example.
向节点A发送MEET ip port 节点B的信息,此时节点A将与节点B发生握手,来确认彼此的存在

  • 节点A会为节点B创建一个ClusterNode,并添加到ClusterState字典中
  • 节点A为节点B发送一条MEET消息,表示接收到MEET命令
  • 节点B同样为节点A创建一个ClusterNode,并添加到ClusterState中
  • 节点B返回一条PONG消息,节点A接收到后返回PING消息,此时握手结束

此后通过GOSSIP协议,节点会逐步知道集群的信息,集群信息达成最终一致性

槽指派

节点的槽指派信息

在上文提到的clusterNode中,会存有一个数组记录节点的槽指派信息
unsigned char slots[16384 / 8], 该数组是比特数组,对于第i个槽位是否由本节点计算可以通过以下公式:

1
slots[i / 8] >> (i % 8) & 1 == 1?

集群的槽指派信息

在上文提到的clusterstate结构体中存有一个字段
clusterNode slots[16834]
这个用于记录每个槽指派的节点,这个与单节点clusterNode结构体中的slots数组相辅相成,一个用于单节点明确是否负责槽,一个用于明确集群中槽的分配情况

这些槽位信息是通过前文提到的GOSSIP协议进行传播的,保持集群中信息的一致性

只有当集群中16384个槽都被指派了节点后集群才能进入上线状态

cluster addslots 命令用于槽指派,比如cluster addslots 1 2 就是把槽1,2都指派给对应的节点

集群中命令的执行

我们知道集群中数据已经被切片成16384个槽,因此对一个服务端进行get操作时(举个例子),不一定在该节点上,具体来说需要经过以下几个步骤:

  • 计算键属于哪个槽这里采用crc16算法
  • 判断槽是否由自己负责,如果不是则通过clusterState中的slot数组找到对应的节点
  • 返回 MOVED信息
  • 客户端接收到MOVED信息后重定向发起命令

节点还会保存槽与键的关系,通过skipList,因为存在命令cluster getkeysinslot slot count 返回最多count个属于slot的数据库键

重新分片

重新分片是利用redis-trib实现的

这里不强调重新分配的指令,而是梳理重新分配的流程

  1. 开始对槽slot重新分片
  2. 目标节点准备导入槽slot的键值对
  3. 源节点准备迁移槽slot的键值对
  4. 源节点如果保存了槽slot的键,则逐步开始迁移,如果没有则直接把槽指派给目标节点
  5. 完成对槽slot的重新分片

那这里会存在一个问题,当槽迁移时,客户端发来命令怎么办?继续往下看

ASK错误

ASK错误指的就是当槽发生迁移,但是请求达到了节点上,此时就会返回ask错误。这也是gossip协议产生的最终一致性问题,在迁移过程时,槽的分配情况在集群中还未能更新至一致。简单的例子,比如“cat”和“dog”都是要迁移的槽位的键,而“cat”已经完成了迁移,但是“dog”还没有,此时get cat已经打到了该节点上,那么就会返回ask 指向正确的节点

其实 clusterState中维护了两个数组 importing_slots_from 代表的是从某个节点迁出 migrating_slots_to 代表的是迁入某个节点

但一般而言,如果客户端向节点发送一个关于槽i的命令,如果节点本身发现没有槽i没有被指派给自己,则会返回错误,而这里需要通过上文提到的importing_slots_from和ASKING命令实现

当返回ASK错误时,客户端会先向redirect的节点发送ASKING,类似激活开关位,下次访问时如果槽i没有被指派给自己但是,在importing_slots_from这个数组中,并且上述的开关位被激活,则会执行该命令

复制与故障转移

Redis集群中的节点分为主节点和从节点,可以划分主从节点形成主从架构,其中主节点用于处理槽,而从节点用于复制某个主节点。

当一个节点成为其他节点的从节点时,
在clusterState中myself即指向自身的clusterNode指针中的slaveof指针指向主节点的clusterNode,同时集群中的其他节点维护主节点clusterNode中slaves属性和numslaves属性

故障检测

我们提到Redis集群是以gossip协议的形式进行消息的传播的,集群中的每个节点都会定期地向集群中的其他节点发送PING(心跳检测), 接收到PING的节点需要定期返回PONG消息,否则则会标记位疑似下线。

集群中的各个节点会通过互相发消息的方式来交换集群中各个节点的状态信息,例如某个节点是疑似下线,还是已下线

当主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到C对应的clusterNode添加下线报告,具体的结构为:

1
2
3
4
struct clusterNodeFailReport {
struct clusterNode *node;
mstime_t time;
}

如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点报告为疑似下线,那么这个主节点x将被标记为已下线,将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的都会把这个节点标记为已下线

故障转移

这里的故障转移和sentinel下的故障转移类似,这里重新梳理一遍

  1. 集群维护一个变量配置纪元,即termId
  2. 当发生一次故障转移时,termId++
  3. 对于每个配置纪元,每个主节点都有一次投票的机会,这里投票的原则是先到先得
  4. 当从节点发送主节点已下线,向集群广播,要求投票的消息
  5. 主节点拥有投票权,以先到先得的方式投票
  6. 从节点如果收到了半数以上的票数则变为主节点
  7. 若平票,则再次选举

选举出的新主节点会执行以下流程:

  1. 执行slave of no one 变为主节点
  2. 撤销已下线主节点对槽的指派,并由自己负责
  3. 发送PONG命令,传递信息,让集群知道已经发生了故障转移
  4. 新主节点接收并处理客户端命令

消息

我们之前一直提到redis集群是通过gossip协议实现一致性的,具体来说存在五种Gossip消息:

  • MEET消息:用于加入节点
  • PING消息: 集群中的每个节点默认每个一秒钟就会从已知接待你列表中随机选出五个节点,然后对这五个节点中最长没有发送过ping消息的节点发送ping消息,以此检测被选中节点是否在线,除此之外,如果节点A最后一次收到节点B发送PONG消息的时间超过设置的一半,也会发送PING,防止消息更新滞后
  • PONG消息:回应PING或MEET消息,或者广播PONG消息立即刷新关于这个节点的认识,比如故障转移操作成功执行后
  • FAIL消息:传播节点下线消息
  • PUBLISH: 当一个节点收到PUBLISH消息时,节点会执行该命令,并广播(因为不同客户端可能在不同节点订阅了相同的频道)

消息头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typedef struct {
// 消息的长度
uint32_t tolen;

// 消息的类型
uint16_t type;

// 消息正文包含的节点数,只有部分消息使用
uint16_t count;

// 发送者所处的配置纪元
uint64_t configEpoch;

...
// 集群状态
unsigned char state;

// 消息的正文
union chlusterMsgData data;
} clusterMsg;

union clusterMsgData {
struct {
clusterMsgDataGossip gossip[1];
} ping;

struct {
clusterMsgDataFail about;
} fail;

struct {
clusterMsgDataPublish msg;
} publish;
}

PING消息实现

当每次发送PING消息时,发送者都从自己的已知节点列表中随机选出两个节点,并发送这两个节点的信息。

  • 如果被选中节点不存在与接收者的已知节点中,两者之间进行握手
  • 如果选中节点存在于,则进行更新

MEET、PONG消息同理

FAIL消息实现

FAIL消息中就记录了下线的节点名,其他节点接收到了该消息后把节点标记为下线

PUBLISH消息的实现

PUBLISH channel messsage 当一个节点收到这个消息时会集群广播消息,具体原因在上文解释过了

1
2
3
4
5
6
typedef struct {
uint32_t channel_len;
uint32_t message_len;
// 前两个字段记录长度,即记录在bulk_data的消息位置
unsigned char bulk_data[8];
} clusterMsgDataPublish;