Cluster、Slot

发布时间 2023-03-29 15:56:44作者: zhengbiyu
Redis 应对数据量增多的两种方案:纵向扩展(scale up)和横向扩展(scale out)。
  • 纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。
    • 优势:简单、直接
    • 劣势:
      • 当数据量过大时,使用RDB对数据进行持久化时,需要的内存也会增加,主线程fork子进程可能会阻塞。
      • 上限低,收到硬件和成本的限制。
  • 横向扩展:横向增加当前 Redis 实例的个数,做切片集群。

数据切片和实例的对应分布关系

Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
具体的映射过程分为两大步:首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
0
在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。也可以使用cluster addslots指定每个实例上的哈希槽个数。
实例 1 保存哈希槽 01,实例 2 保存哈希槽 23,实例 3 保存哈希槽 4。
redis-cli -h 172.16.19.3 –p 6379 cluster addslots 0,1
redis-cli -h 172.16.19.4 –p 6379 cluster addslots 2,3
redis-cli -h 172.16.19.5 –p 6379 cluster addslots 4

 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。

客户端如何定位数据?

客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。
客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?这是因为,Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。
客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
在集群中,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:
  • 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
  • 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。
此时,实例之间还可以通过相互传递消息,获得最新的哈希槽分配信息,但是,客户端是无法主动感知这些变化的。这就会导致,它缓存的分配信息和最新的分配信息就不一致了。
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。
//13320是对应key所在的哈希槽,在实例172.16.19.5上
GET hello:key
(error) MOVED 13320 172.16.19.5:6379
客户端拿到对应的实例ip后,进行重定向。
在实际应用时,如果某个哈希槽Slot 2的数据比较多,就可能会出现一种情况:客户端向实例A 发送请求,但此时,实例A上Slot 2 中的数据只有一部分迁移到了实例 3,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息,如下所示:
GET hello:key
(error) ASK 13320 172.16.19.5:6379
ASK 命令表示两层含义:第一,表明 Slot 数据还在迁移中;第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令。
和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息,属于临时让客户端请求新实例。

限制Redis Cluster规模的关键因素

Redis 官方给出了 Redis Cluster 的规模上限,就是一个集群运行 1000 个实例。实例间的通信开销会随着实例规模增加而增大,在集群超过一定规模时(比如 800 节点),集群吞吐量反而会下降。所以,集群的实际规模会受到限制。
Redis Cluster 在运行时,每个实例上都会保存 Slot 和实例的对应关系(也就是 Slot 映射表),以及自身的状态信息。为了让集群中的每个实例都知道其它所有实例的状态信息,实例之间会按照一定的规则进行通信。这个规则就是 Gossip 协议。Gossip 协议的工作原理可以概括成两点:
  • 每个实例之间会按照一定的频率,从集群中随机挑选一些实例,把 PING 消息发送给挑选出来的实例,用来检测这些实例是否在线,并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息,以及 Slot 映射表。
  • 一个实例在接收到 PING 消息后,会给发送 PING 消息的实例,发送一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。

Gossip 协议可以保证在一段时间后,集群中的每一个实例都能获得其它所有实例的状态信息。
这样一来,即使有新节点加入、节点故障、Slot 变更等事件发生,实例间也可以通过 PING、PONG 消息的传递,完成集群状态在每个实例上的同步。经过刚刚的分析,我们可以很直观地看到,实例间使用 Gossip 协议进行通信时,通信开销受到通信消息大小通信频率这两方面的影响,消息越大、频率越高,相应的通信开销也就越大。
实例间发送消息的频率有两个:
  • 每个实例每 1 秒发送一条 PING 消息。这个频率不算高,如果再降低该频率的话,集群中各实例的状态可能就没办法及时传播了。
  • 每个实例每 100 毫秒会做一次检测,给 PONG 消息接收超过 cluster-node-timeout/2 的节点发送 PING 消息。实例按照每 100 毫秒进行检测的频率,是 Redis 实例默认的周期性检查任务的统一频率。
配置项 cluster-node-timeout 定义了集群实例被判断为故障的心跳超时时间,默认是 15 秒。为了避免过多的心跳消息挤占集群带宽,我们可以调大 cluster-node-timeout 值,比如说调大到 20 秒或 25 秒。
在调整 cluster-node-timeout 值的前后,使用 tcpdump 命令抓取实例发送心跳信息网络包的情况。执行下面的命令后,我们可以抓取到 192.168.10.3 机器上的实例从 16379 端口发送的心跳网络包,并把网络包的内容保存到 r1.cap 文件中。
tcpdump host 192.168.10.3 port 16379 -i 网卡名 -w /tmp/r1.cap