type
status
date
slug
summary
tags
category
icon
password
概念
Cluster 是原生的分布式存储解决方案,通过分片来实现数据共享的,并提供复制、负载均衡和故障转移功能。集群节点采用的是无中心架构 ( 每个节点保存当前节点数据和整个集群状态、每个节点都和其他所有节点连接 ),最少配置数是 6 ( 3主3从 ),它采用的虚拟槽分区,所有的 key 通过哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值对。它的优点是:
- 按照槽指派的方式将数据存储在多个节点,节点间支持数据共享、动态调整数据分布
- 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除
- 高可用性:通过增加 slave 做备份,能够实现自动故障转移 ( failover ),节点之间通过 Gossip 协议交换状态信息
缺点如下:
- slave 仅作为备份使用,不提供读服务
- 数据通过异步复制,不保证数据的强一致性
- 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差
- 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的
- 不支持处理多个 keys 的命令,因为这需要在不同的节点间移动数据,从而达不到像 Redis 那样的性能,在高并发情况下可能会导致不可预料的错误
适用场景:大数据量、高并发、高可用的情况下,集群中 master 容量的总和就是 Cluster 中缓存的数据总和。
使用
先将 redis.conf 里面 cluster-enabled yes 选项前面的 # 删掉,表示服务器由单机模式转换成集群模式。在未形成可工作的集群之前,每个节点都是一个只包含自己的集群,我们要做的就是把多个节点连接到一个集群。
实现原理
节点
数据结构
clusterNode:保存了一个节点的当前状态,比如节点的创建时间,节点的名字,节点当前的配置纪元,节点的IP地址和端口号
clusterLink:clusterNode 结构的 link 属性是一个 clusterLink 结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区
clusterState:每个节点都保存着一个 clusterState 结构,这个结构记录了在当前节点的视角下,集群目前所处的状态。比如集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元
CLUSTER MEET 的握手
- 节点 A 为节点 B 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典中
- 节点 A 根据 ip 和 port 发送 MEET 消息给节点 B
- 节点 B 收到 MEET 消息,为节点 A 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典中
- 节点 B 向节点 A 发送 PONG 消息
- 节点 A 向节点 B 返回 PING 消息
槽指派
集群的整个数据库被分为16384个槽 ( slot ),数据库中每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。只有当这16384个槽全被指派完 Cluster 才能进入上线状态
记录节点的指派槽信息
传播节点的指派槽信息
一个节点除了会将自己负责处理的槽记录在 clusterNode 结构的 slots 属性和 numslots 属性之外,它还会将自己的 slots 数组通过消息发送给集群中其他的节点,以此来告知其他节点自己目前负责处理哪些槽。当节点 A 通过消息从节点 B 那里接收到节点B的 slots 数组时,节点 A 会在自己的 clusterState.nodes 字典中查找节点 B 对应的 clusterNode 结构,并对结构中的 slots 数组进行保存或者更新
记录集群所有槽的指派信息
clusterState.slots:记录了集群中所有槽的指派信息,数组中每个元素存储的是 0 或 1
clusterNode.slots:记录单个节点的槽指派信息,数组中每个元素存储的是 clusterNode
CLUSTER ADDSLOTS 命令的实现
可以用以下伪代码来表示:
在集群中执行命令
在集群中执行命令的流程可以用下图来表示:
计算键属于哪个槽
节点使用 CRC16 ( key ) & 16383 算法来计算给定槽key属于哪个槽。
CRC16 ( key ) 语句用于计算键 key 的 CRC - 16 校验和,& 16383 语句则用于计算 一个介于 0 至 16383 之间的整数作为 key 的槽号
判断槽是否由当前节点处理
如果 clusterState.slots[i] 等于 clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令
如果 clusterState.slots[i] 不等于 clusterState.myself,节点会根据 clusterState.slots[i] 指向的 clusterNode 锁记录的节点 IP 和端口号,向客户端返回 MOVED 错误
MOVED 错误
当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED 错误,指引客户端转向正在负责槽的节点
重新分片
可以将任意数量已经指派给了某个节点的槽改为指派给另一个节点,并且相关槽所属的键值也会从源节点被移动到目标节点,重新分片操作是由Redis集群管理软件 redis - trib 负责执行的。重新分片的流程可以用下图来表示:
- redis-trib 对目标节点发送 CLUSTER SETSLOT < slot > IMPORTING < source_id > 命令,让目标节点准备好从源节点导入槽 slot 的键值对。clusterState结构的 import_slots_form 数组记录了当前节点正在从其他节点导入的槽
- redis-trib 对源节点发送 CLUSTER SETSLOT < slot > MIGRATING < source_id > 命令,让源节点准备好将属于槽 slot 的键值对迁移至目标节点。clusterState结构的 migrating_slots_to 数组记录了当前节点正在迁移至其他节点的槽
- redis-trib对源节点发送CLUSTER GETKEYSINSLOT < slot > < count > 命令,获得最多 count 个属于槽 slot 的键值对的键名
- 对于步骤三获得的每个键名,redis-trib 都向源节点发送一个 MIGRATE < target_ip > < target_port > < key_name > 0 < timeout >命令,将被选中的键原子的从源节点迁移至目标节点
- 重复步骤3和4,直到源节点保存的所有属于槽slot的键值对都被迁移到目标节点为止,每次迁移的过程如下图所示:
- redis-trib向集群中的任意一个节点发送 CLUSTER SETSLOT < slot > NODE < target_id >命令,将槽slot指派给目标节点的信息发送给整个集群
ASK错误
重新分片的期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一个情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽的时候:
- 源节点在自己的数据库库里面找指定的键,如果找到的话,就直接执行客户端发送的命令
- 源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将检查自己的 clusterState migrating_slots_to[i] 看键 key 所属的槽i是否正在进行迁移,如果槽 i 的确在进行迁移的话,那么节点就会向客户端发送一个 ASK 错误,引导客户端到正在导入槽i的节点去查找键 key,再向新指向的节点发送 ASKING 命令
ASKING
ASKING 命令唯一要做的就是打开发送该命令的客户端的 REDIS_ASKING 标识,一般情况下,如果客户端向节点发送一个关于槽i的命令,而槽 i 又没有指派给这个节点的话,那么节点将向客户端返回一个 MOVED 错误,但是,如果节点的 import_slots_form 显示节点正在导入槽 i,并且发送命令的客户端带有 REDIS_ASKING 标识,那么节点将破例执行这个关于槽 i 的命令一次
ASK 错误和MOVED错误的区别
- MOVED 错误代表槽的负责原已经从一个节点转移到了另一个节点:在客户端收到关于槽 i 的 MOVED 错误之后,客户端每次遇到关于槽 i 的命令请求时,都可以直接将命令请求发送至 MOVED 错误所指向的节点
- ASK 错误只是两个在迁移槽过程中使用的一种临时的措施,在客户端收到关于槽i的错误 ASK 之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至 ASK 错误所只是的节点,但这种转向不会对客户端今后发送关于槽 i 的命令请求产生任何影响
复制与故障转移
复制
向一个节点发送命令:CLUSTER REPLICATE < node_id >
- 接收到这个命令的节点首先会在自己的 clusterState.nodes 字典中找到 node_id 所对应的 clusterNode,并将自己的 clusterState.myself.slaveof 指针指向这个结构,以此来记录这个节点正在复制的主节点
- 节点会修改自己在 clusterState.myself.flags 中的属性,关闭原本的 REDIS_NODE_MASTER 标识,打开 REDIS_NODE_SLAVE 标识,表示这个节点已经由原来的主节点变为从节点
- 节点会调用复制代码,并根据 clusterState.myself.slaveof 指向的 clusterNode 结构所保存的 IP 地址和端口号,对主节点进行复制
故障检测
集群中的每个节点都会定期地向集群中的其他节点发送 PING 消息,以此来检测对方是否在线,如果接收 PING 消息地节点没有在规定的时间内向发送 PING 消息的节点返回 PONG 消息,那么发送 PING 消息的节点就会将接收 PING 消息的节点标记位疑似下线
当一个主节点 A 通过消息得知 B 认为主节点 C 进入了疑似下线状态时,主节点 A 会在自己的 clusterState.nodes 字典中找到主节点 C 所对应的 clusterNode 结构,并将主节点 B 的下线报告添加到 clusterNode 结构的 fail_reports 链表里面
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点 x 报告为疑似下线,那么这个主节点 x 将被标记为已下线,将主节点 x 标记为已下线的节点会向集群广播一条关于主节点 x 的 FAIL 消息,所有收到这条 FAIL 消息的节点都会立即将主节点 x 标记为已下线
故障转移
不同于 Sentinel,当 slave 发现自己的 master 下线时,slave 会对 master 进行故障转移:
- 复制下线主节点的所有从节点中,会有一个被选中
- 被选中的从节点将执行 slaveof no one 命令,成为新的主节点
- 新的主节点撤销下线主节点对指派槽的管理,并将这些槽全部指派给自己
- 新的主节点向集群广播一条 PONG 消息,告诉集群中的其他节点自己成为了新的主节点
- 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成
选举新的主节点
- 当从节点发现自己复制的主节点进入已下线状态,就会向集群广播一条消息,要求所有收到这条消息并且具有投票权的主节点向这个从节点投票
- 集群中的主节点接收到这条广播消息,如果该主节点没有把票投给过其他从节点,则将票投给当前这个从节点
- 每个从节点统计收到的票数,如果票数大于等于 floor ( n/2 ) +1,则当前节点成为新的主节点
消息
集群中节点发送的消息主要由5种:
- MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入发送者当前所处的集群中
- PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测选中的节点是否在线
- PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息
- FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线
- PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH消息
参考文章
- Redis 设计与实现