第 17 章 集群

17.1 节点

一个集群通常多个节点组成,开始时相互独立,处于一个只包含自己的集群中

连接各个节点的工作使用 CLUSTER MEET 命令来完成:

CLUSTER MEET <ip> <port> 


节点向 ip 和 port 指定的节点进行握手,成功后就添加到节点所在的集群中

17.1.1 启动节点

节点是运行在集群模式下的 Redis 服务器,启动时会根据 cluster-enabled 配置选项是否为 yes 来决定是否开启服务器的集群模式

节点会继续使用单机模式下使用的服务器组件,如文件事件处理器、时间事件处理器、数据库、RDB 和 AOF 持久化、发布与订阅、复制模块、Lua 脚本执行等

还好继续使用 redisServer 结构来保存服务器的状态,使用 redisClient 来保存客户端的状态,而集群模式下使用的数据,将保存在 cluster.h/clusterNode、clusterLink、clusterState 结构中

17.1.2 集群数据结构

clusterNode 保存了节点当前的状态,创建时间、名字、配置纪元、IP、端口号等

节点不仅使用一个来记录自己的状态,还为集群其他节点创建相应的结构

Redis设计与实现 第 17 章 集群_服务器

link 属性是 clusterLink 结构,保存了节点所需的有关信息

Redis设计与实现 第 17 章 集群_redis_02

redisClient 结构和 clusterLink 结构相似,区别在于 redisClient 是连接客户端的,而 clusterLink 是连接节点的

每个节点都保存着 clusterState 结构,记录了当前节点的视角下,集群目前所处的状态

Redis设计与实现 第 17 章 集群_键值对_03

17.1.3 CLUSTER MEET 命令的实现

向 A 发送此命令,将 B 连接

  1. A 为 B 创建 clusterNode 结构,添加到 clusterState.nodes 字典里
  2. A 根据 IP、端口,向 B 发送 MEET 信息
  3. 正常情况下 B 收到 A 的信息,为 A 创建 clusterNode 结构,添加到自己的 clusterState.nodes 字典中
  4. B 向 A 返回 PONG 信息
  5. 正常情况 A 收到 B 的 PONG 信息,知道 B 已经正常接收 MEET 信息
  6. A 向 B 发送 PING 信息
  7. 正常情况 B 收到 PING 信息,B 通过此信息知道 A 收到 PONG 信息,握手完成

Redis设计与实现 第 17 章 集群_数据库_04

  1. A 通过 Gossip 协议传播给集群其他的节点,让其他节点也与 B 握手,最后 B 被集群中所有节点认识

17.2 槽指派

集群通过分片的方式来保存数据库中的键值对,被分为 16384 个槽 slot,每个键都属于 16384 个 slot 之一,每个节点可以处理 0 个或 16384 个槽

如果全部槽都有节点在处理,则称集群处于上线状态,如果有一个没在处理,则集群处于下线状态

通过向节点发送 CLUSTER ADDSLOTS 命令,可以将一个或多个槽指派给节点负责

Redis设计与实现 第 17 章 集群_数据库_05

17.2.1 记录节点的槽指派信息

clusterNode 结构的 slots 属性和 numslot 属性记录了节点负责处理哪些槽

Redis设计与实现 第 17 章 集群_服务器_06

slots 为一个二进制位数组,长度为 16834 / 8 = 2048 个字节

0 为起始索引,16383 为终止索引,如果此索引上二进制位为 1 ,表示由此节点负责,否则不是

17.2.2 传播节点的槽指派信息

节点不仅记录自己的槽指派信息,同时也会传播告知其他节点自己目前负责处理的槽

其他节点收到信息后会在自己的 clusterState.nodes 字典查找节点对应的 clusterNode 结构,并对结构中的 slots 数组进行保存

17.2.3 记录集群所有槽的指派信息

clusterState 结构中的 slots 数组记录了集群中所有槽的指派信息

Redis设计与实现 第 17 章 集群_键值对_07

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

  • 指向 NULL:槽 i 没有指派给任何节点
  • 指向 clusterNode 结构:此结构代表的节点

17.2.4 CLUSTER ADDSLOTS 命令的实现

Redis设计与实现 第 17 章 集群_服务器_08

17.3 在集群中执行命令

当客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库属于哪个键,并检查这个槽是否指派给了自己

  • 槽属于当前节点,直接执行
  • 槽不属于当前节点,节点向客户端返回一个 MOVED 错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令

Redis设计与实现 第 17 章 集群_数据库_09

17.3.1 计算键书属于哪个槽

Redis设计与实现 第 17 章 集群_服务器_10

CRC16 为计算 key 的 CRC-16 校验和, & 16384 语句计算一个介于 0 - 16384 的整数作为键 key 的槽号

CLUSTER KEYSOLT <key> 


可以查看键属于哪个槽

17.3.2 判断槽是否由当前节点负责处理

计算出槽 i 之后,节点检查自己在 clusterState.slots 数组的项 i ,判断是否自己负责

  • clusterState.slots[i] = clusterState.myself,当前节点负责,执行客户端发送的命令
  • 否则根据 clusterState.slots[i] 指向的 clusterNode 结构记录的节点 IP 和端口号,向客户端返回 MOVED 错误,指引客户端转向至处理槽 i 的节点上

17.3.3 MOVED 错误

格式为:

MOVED <slot> <ip>:<port>


一个集群客户端通常会与多个节点创建套接字连接,所谓的节点转向实际上是换一个套接字来发送命令

如果尚未创建与即将转向的节点的套接字则会根据 MOVED 错误提供的 IP 和端口号来连接节点再进行转向

集群模式下 MOVED 错误不会被打印出来,客户端根据此错误自动转向并打印转向信息

17.3.4 节点数据库的实现

集群节点保存键值对以及键值对过期时间的方式与单机服务器一致

区别是集群的节点只能使用 0 号数据库,而单机服务器没有限制

节点会用 clusterState 结构中的 slots_to_keys 跳跃表来保存槽和键之间的关系

slots_to_keys 跳跃表每个节点的分值是一个槽号,每个节点的成员是一个数据库键:

  • 当节点往数据库添加新的键值对时,节点会将键以及键的槽号关联到 slots_to_keys 跳跃表
  • 删除某个键值对时,节点会在 slots_to_keys 跳跃表接触键与槽的关联

通过 slots_to_keys 跳跃表,节点可以很方便对属于某个或某些槽的数据库键进行批量操作

CLUSTER GETKEYSINSLOT <slot> <count> 


返回最多 count 个属于槽 slot 的数据库键

17.4 重新分片

重新分片:将任意数量已经指派给某个节点的槽改为指派给另外一个节点,且相关槽所属的键值对也会从源节点被移动到目标节点

可以在线进行,集群不需要下线,源节点和目标节点都可以继续处理命令请求

重新分片的原理:

redis-trib 负责执行的,Redis 提供重新分片的所有命令,redis-trib 通过向源节点和目标节点发送命令进行分片:

  1. redis-trib 对目标发送​​CLUSTER SETSLOT <slot> IMPORTING <source_id> ​​,让目标节点准备好从源节点导入属于槽 slot 的键值对
  2. redis-tirb 对源发送 ​​CLUSTER SETSLOT <slot> MIGRATING <target_id> ​​命令,让源节点准备好将属于槽 slot 的键值对迁移到目标节点
  3. redis-trib 向源发送 ​​CLUSTER GETKEYSINSLOT <slot> <count> ​​ 命令,获得最多 count 个属于槽 slot 的键值对的键名
  4. 对于 3 获得的键名,redis-trib 都向源发送一个 ​​MIGRATE <target_ip> <target_port> <key_name> O <timeout> ​​命令,将被选择的键原子地从源迁移至目标
  5. 重复 3 4 ,直到源保存的所有属于槽 slot 的键值对都被迁移到目标节点为止
  6. redis-trib 向集群中任意一个节点发送 ​​CLUSTER SETSLOT <slot> NODE <target_id> ​​命令,将槽 slot 指派给目标节点,这信息将通过消息发送整个集群,最后集群所有节点都会知道槽 slot 已经指派给目标节点

Redis设计与实现 第 17 章 集群_服务器_11

重新分片如果涉及多个槽,则对每个槽执行上面的步骤

Redis设计与实现 第 17 章 集群_数据库_12

17.5 ASK 错误

Redis设计与实现 第 17 章 集群_数据库_13

17.5.1 CLUSTER SETSLOT IMPORTING 命令的实现

clusterState 结构的 importing_slots_from 记录了当前节点正在从其他节点导入的槽

importing_slots_from[i] 不为 NULL,而是指向一个 clusterNode 结构,则表示当前节点正在从 clusterNode 代表的节点导入槽 i

重新分片,向目标节点发送命令:

CLUSTER SETSLOT IMPORTING <source_id>

将目标节点的 clusterState.importing_slots_from[i] 设置为 source_id 代表的 clusterNode 结构

17.5.2 CLUSTER SETSLOT MIGRATING 命令的实现

clusterState 结构的 migreting_slots_to 记录了当前节点正在迁移至其他节点的槽

migreting_slots_to[i] 不为 NULL,而是指向一个 clusterNode 结构,则表示当前节点正在向 clusterNode 代表的节点迁移槽 i

重新分片,向源节点发送命令:

CLUSTER SETSLOT MIGRATING <target_id>

将源节点 clusterState.migrating_slots_to[i] 的值设置为 target_id

17.5.3 ASK 错误

Redis设计与实现 第 17 章 集群_redis_14

17.5.4 ASKING 命令

唯一要做的是打开发送该命令的客户端的 REDIS_ASKING 表示

Redis设计与实现 第 17 章 集群_服务器_15

当客户端接收到 ASK 错误并转向正在导入槽的节点时,客户端会先向节点发送一个 ASKING 命令,再发送想要执行的命令,因为如果不发送 ASKING 命令而直接发送想要执行的命令的话,客户端发送的命令将被节点拒绝执行,并返回 MOVED 错误

此标识是一次性标识,节点执行了一个带有 REDIS_ASKING 命令的标识的客户端发送的命令后,客户端的标识就被移除

17.5.5 ASK 错误和MOVED 错误的区别

  • MOVED 代表槽的负责权已经从一个节点转移到另外一个节点
  • 客户端收到槽 i 的 MOVED 错误后,客户端每次遇到 槽 i 的命令请求时,都直接将命令请求发送至 MOVED 错误指向的节点
  • ASK 错误是两个节点迁移槽过程使用的临时措施
  • 客户端收到槽 i 的ASK 错误后,客户端只会在接下来一次的命令请求关于槽 i 的命令发送至 ASK 错误指示的节点,但这种转向只是一次性的,后续的仍会发送至当前的节点,除非 ASK 错误再次出现
  • 17.6 复制与故障转移
    节点分为主节点和从节点,主节点处理槽,从节点复制某个主节点,并在被复制的主节点下线时代替下线主节点继续处理命令请求
    17.6.1 设置从节点
    向一个节点发送命令:
    CLUSTER REPLICATE <node_id>
    让接收命令的节点成为 node_id 指定的节点的从节点,对其进行复制

节点在自己的 clusterState.nodes 字典中找到 node_id 对应节点的 clusterNode 结构,并将自己的 clutserState.myself.slaveof 指针指向此结构,记录本节点只在复制的主节点

节点修改自己在 clusterState.myself.flags 的属性,关闭原来的 REDIS_NODE_MASTER 标识,打开 REDIS_NODE_SLAVE 标识,表示成为从节点

最后节点调用复制代码,根据指向的 clusterNode 结构保存的 IP 地址和端口进行复制,与单机 Redis 复制功能使用相同代码

一个节点成为从节点,并开始复制某个主节点的信息会通过消息发送给集群其他节点,最后所有节点都会知道

所有节点的代表主节点的 clusterNode 结构的 slaves 属性和 numslaves 属性记录了这个主节点的从节点名单

17.6.2 故障检测

集群中每个节点会定期地向其他节点发送 PING 消息,来检测对方是否在线,如果没有在规定的时间内返回 PONG 消息,则会被标记疑似下线

当主节点 A 通过消息得知主节点 B 认为主节点 C 进入了疑似下线状态,则会在自己的 clusterState.nodes 字典中找到 C 对应的 clusterNode 结构,并将 B 的下线报告添加到 clusterNode 结构的 fail_reports 链表里面

Redis设计与实现 第 17 章 集群_服务器_16

每个下线报告由一个 clusterNodeFailReport 结构表示:

在一个集群中,半数主节点都将某个主节点 x 报告为疑似下线,则 x 将被标记为已下线,再广播出去,所有收到此广播的主节点便将 x 标记为已下线

17.6.3 故障转移

当一个主节点进入已下线状态,从从节点对其进行故障转移:

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

17.6.4 选举新的主节点

  1. 集群配置纪元是自增计数器,初始值为 0
  2. 某个节点开始一次故障转移,则配置纪元的值增 1
  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. 如果在一个配置纪元中没有一个从节点能获得 N / 2 + 1 的支持票,则集群进入下一个配置纪元,再次进行选举,直到选出新的主节点为止

17.7 消息

种类:

  • MEET :发送者发送此消息请求接收者加入发送者所处的集群
  • PING:每秒从已知列表选出五个节点,对其中最长时间没有发送 PING 消息的节点发送 PING 消息,检查节点是否在线;节点 A 最后一次收到节点 B 发送的 PONG 消息超过设置的 cluster-node-timeout 选项设置的时间的一半,则 A 也会向 B 发送 PING 消息,防止随机时挑选不到而导致 B 的消息滞后
  • PONG:接收者收到 MEET 或 PING 消息时,向发送者确认消息已到达;通过集群广播自己的 PONG 消息来让集群其他节点立即刷新关于节点的认识
  • FAIL:一个主节点判断另外一个主节点进入 FAIL 状态时进行广播,所有收到此消息的节点会将 FAIL 状态节点标记已下线
  • PUBLISH:一个节点收到 PUBLISH 命令,执行后广播一条 PUBLISH 消息,所有收到的节点执行相同的操作

消息由消息头和消息正文组成

17.7.1 消息头

消息头包裹在消息外,还记录了发送者部分信息

由 cluster.h/clusterMsg 表示:

Redis设计与实现 第 17 章 集群_服务器_17

Redis设计与实现 第 17 章 集群_服务器_18

clusterMsg.data 指向 cluster.h/clusterMsgData 消息正文:

Redis设计与实现 第 17 章 集群_redis_19

clusterMsg 的 currentEpoch、sender、myslots 等记录了发送者的信息,接收者根据这些消息在自己的 clusterState.nodes 字典中找到发送者对应的 clusterNode 结构并更新

17.7.2 MEET、PING、PONG 消息的实现

通过 Gossip 协议交换信息,使用这三种实现,都由两个 cluster.h/clusterMsgDataGossip 结构组成

Redis设计与实现 第 17 章 集群_数据库_20

因为都是使用相同的消息正文,所以节点通过消息头的 Type 属性来判断一条消息是哪种类型

每次发送消息时,发送者从自己已知节点列表中随机取出两个节点(主或从),保存在两个 clusterMsgDataGossip 结构中,记录了被选中节点名字,发送者与被选择节点最后一次发送和接收 PING 消息的时间戳、被选中节点 IP、端口,标识值 Redis设计与实现 第 17 章 集群_服务器_21

接收者收到消息后访问这两个结构,根据是否认识其中被选中节点进行操作:

  • 不存在已知节点列表:第一次接收到,根据 IP、端口号进行握手
  • 存在已知节点列表:更新对应的 clusterNode 结构

Redis设计与实现 第 17 章 集群_服务器_22

17.7.3 FAIL 消息的实现

节点数量比较大时,单纯使用 Gossip 协议来传播节点的已下线消息会带来延迟,因为 Gossip 协议需要一段时间才能通知整个集群,而发送 FAIL 消息可以让整个集群都知道某个主节点已下线,从而尽快判断是否将集群标记为已下线,又或者对下线节点进行故障转移

FAIL 消息的正文由 cluter.h/clusterMsgDataFail 结构表示,其只包含一个 nodename 属性,记录了已下线节点的名字

Redis设计与实现 第 17 章 集群_键值对_23

集群中节点名字是唯一的,所以 FAIL 消息只需要保存下线节点的名字,接收节点即需要根据这个名字则可以判断

Redis设计与实现 第 17 章 集群_数据库_24

17.7.4 PUBLISH 消息的实现

客户端向集群中某个节点发送消息:

PUBLISH

接受者不仅会向 channel 发送 message,还好向集群广播 PUBLISH 消息,再接收者重复,即向集群中某个节点发送此命令都会导致集群中所有节点向 channel 频道发送 message 消息

PUBLISH 消息正文由 cluster.h/clusterMsgDataPublish 结构表示:

Redis设计与实现 第 17 章 集群_redis_25

bulk_data 属性是字节数组,保存了客户端通过 PUBLISH 命令发送给节点的 channel、message 参数,channel_len、message_len 则保存了长度

  • 0 - channel_len 字节是 channel 参数
  • channel_len + messgae_len - 1 字节是 message 参数