跳至主要內容

Redis-高可用篇-③切片集群

holic-x...大约 24 分钟RedisRedis

Redis-高可用篇-③切片集群

学习核心

  • 切片集群概念核心
  • Hash槽的算法

学习资料

切片集群:数据增多了,是该加内存还是加实例?open in new window

Redis 集群模式 实战上、下open in new window todo(docker或者虚拟机等构建集群环境)

切片集群

1.大数据量存储场景分析

场景分析(数据增多了是该加内存还是加实例?)

​ 需求:要用 Redis 保存 5000 万个键值对,每个键值对大约是 512B,为了能快速部署并对外提供服务,我们采用云主机来运行 Redis 实例,那么,该如何选择云主机的内存容量呢?

​ 粗略地计算一下,这些键值对所占的内存空间大约是 25GB(5000 万 *512B)

【方案1】:选择一台 32GB 内存的云主机来部署 Redis

​ 因为 32GB 的内存能保存所有数据,而且还留有 7GB,可以保证系统的正常运行。采用 RDB 对数据做持久化,以确保 Redis 实例故障后,可从 RDB 恢复数据。但是,在使用的过程中,会发现 Redis 的响应有时会非常慢。通过使用 INFO 命令查看 Redis 的 latest_fork_usec 指标值(表示最近一次 fork 的耗时),结果显示这个指标值特别高,快到秒级别。

​ 这跟 Redis 的持久化机制有关系。在使用 RDB 进行持久化时,Redis 会 fork 子进程来完成,fork 操作的用时和 Redis 的数据量是正相关的,而 fork 在执行时会阻塞主线程。数据量越大,fork 操作造成的主线程阻塞的时间越长。所以,在使用 RDB 对 25GB 的数据进行持久化时,数据量较大,后台运行的子进程在 fork 创建时阻塞了主线程,于是就导致 Redis 响应变慢了(基于持久化优化也可考虑通过混合持久化模式进行优化(AOF、RDB混合))

【方案2】:构建切片集群架构

​ 基于此大数据量存储场景,引入Redis 的切片集群概念(虽然组建切片集群比较麻烦,但是它可以保存大量数据,而且对 Redis 主线程的阻塞影响较小)。切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。

​ 基于上述场景,可以把 25GB 的数据平均分成 5 份(也可以不做均分,可结合实际服务器资源进行调整),使用 5 个实例来保存,每个实例只需要保存 5GB 数据。

image-20240722101207118

​ 在切片集群中,实例在为 5GB 数据生成 RDB 时,数据量就小了很多,fork 子进程一般不会给主线程带来较长时间的阻塞。采用多个实例保存数据切片后,既能保存 25GB 数据,又避免了 fork 子进程阻塞主线程而导致的响应突然变慢。在实际应用 Redis 时,随着用户或业务规模的扩展,保存大量数据的情况通常是无法避免的。而切片集群是一个非常好的解决方案

2.如何保存更多的数据?

​ 在上述案例中,在大数据存储场景中,单机的吞吐无法承受持续扩增的流量,于是选择使用了大内存云主机和切片集群两种方法。实际上,这两种方法分别对应着 Redis 应对数据量增多的两种方案:纵向扩展(scale up)和横向扩展(scale out)。

  • 纵向扩展(scale up)升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU
    • 优点:实施起来简单、直接,仅涉及单个实例,其数据存取非常明确
    • 缺点:
      • 纵向扩展会受到硬件和成本的限制(32GB->64GB->1TB,随着扩容需求增加会面临着硬件容量和成本限制)
      • 当使用RDB进行持久化时,大数据量存储场景的持久化操作在一定程度上会影响性能(主线程 fork 子进程时就可能会阻塞)
  • 横向扩展(scale out)横向增加当前 Redis 实例的个数面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择
    • 优点:扩展性更好,通过增加示例个数达到扩容目的,单个实例的硬件和成本的限制较小
    • 缺点:涉及到多个实例的分布式管理问题,需要解决两个问题:
      • 数据切片后,在多个实例之间如何分布?
      • 客户端怎么确定想要访问的数据在哪个实例上?

​ 结合图示分析,如果需要扩展实例以达到保存大数据量的目的

  • 纵向扩展:原来的实例内存是 8GB,硬盘是 50GB,纵向扩展后,内存增加到 24GB,磁盘增加到 150GB
  • 横向扩展:原来使用 1 个 8GB 内存、50GB 磁盘的实例,现在使用三个相同配置的实例

image-20240722101728045

3.切片集群的分布式管理问题(横向扩展)

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

​ 首先先理解切片集群和 Redis Cluster 的联系与区别,然后进一步理解数据和实例之间如何对应。

​ Cluster 即 集群模式,类似MySQL,Redis 集群也是一种分布式数据库方案,集群通过分片(sharding)模式来对数据进行管理,并具备分片间数据复制、故障转移和流量调度的能力。这种分治模式很常见,例如微服务系列的拆分策略、MySQL系列的分库分表实践

Redis Cluster 是一种方案,用于实现切片集群,其规定了数据和实例的对应规则

​ 切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案。在 Redis 3.0 之前,官方并没有针对切片集群提供具体的方案。从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。Redis Cluster 方案中就规定了数据和实例的对应规则。

Redis Cluster 方案采用哈希槽(Hash Slot,下述称为Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384(2的14次方) 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。

​ 具体的映射过程分为两大步:首先根据键值对的 key,按照CRC16 算法open in new window计算一个 16 bit 的值;然后再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。

哈希槽如何被映射到具体的 Redis 实例上的呢?

​ 如果有多个实例节点,那么每个实例节点将管理其中一部分的槽位,槽位的信息会存储在各自所归属的节点中。数据和槽点的分配有两种方式:

​ 【方式1】cluster create:在部署 Redis Cluster 方案时,可以使用cluster create命令创建集群,此时 Redis 会自动把这些槽平均分布在集群实例上。如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。

​ 【方式2】cluster meet + cluster addslots

  • 使用 cluster meet 命令手动建立实例间的连接,将独立的节点进行联通,形成集群;
  • 再使用 cluster addslots 命令,指定每个实例上的哈希槽个数;

​ 假设集群中不同 Redis 实例的内存大小配置不一,如果把哈希槽均分在各个实例上,在保存相同数量的键值对时,和内存大的实例相比,内存小的实例就会有更大的容量压力。遇到这种情况时,可以根据不同实例的资源配置情况,使用 cluster addslots 命令手动分配哈希槽,每个节点负责集群中的一部分数据,数据量可以不均匀(例如性能好的实例节点可以多分担一些压力)

image-20240722104542338

​ 结合上述简化图示分析,切片集群中有3个实例,设有5个哈希槽,可通过下述命令手动分配哈希槽:

  • 实例1保存哈希槽0、1;实例2保存哈希槽2、3;实例1保存哈希槽4;
redis-cli -h 192.168.1.100 –p 6379 cluster addslots 0,1
redis-cli -h 192.168.1.200 –p 6379 cluster addslots 2,3
redis-cli -h 192.168.1.300 –p 6379 cluster addslots 4

​ 在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 5 取模,再根据各自的模数结果,就可以被映射到对应的实例 1 和实例 3 上。且需注意:在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作

​ 结合上图示分析,上述3个实例节点组成一个Redis集群,集群之间的信息通过Gossip协议进行交互。当所有的哈希槽都有实例节点进行管理时,集群处于online状态;如果有一个哈希槽没有被管理到,则集群处于offline状态

客户端如何定位数据?

​ 在定位键值对数据时,它所处的哈希槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。但是,要进一步定位到实例,还需要知道哈希槽分布在哪个实例上。一般来说,客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。

​ 客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?=》因为Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。

​ 客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。但是,在集群中,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:

  • 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
  • 负载均衡场景中,节点变化时 Redis 需要把哈希槽在所有实例上重新分布一遍;

​ 此时,实例之间还可以通过相互传递消息,获得最新的哈希槽分配信息,但是,客户端是无法主动感知这些变化的。这就会导致,它缓存的分配信息和最新的分配信息就不一致了,那该怎么办呢?

客户端 MOVED 重定向命令

​ Redis Cluster 方案提供了一种**重定向机制,**所谓的“重定向”,就是指客户端给一个实例发送数据读写操作时,如果这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。

​ 那客户端又是怎么知道重定向时的新实例的访问地址呢?当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。

# 参考示例
GET hello:key
(error) MOVED 13320 192.168.1.300:6379

​ MOVED 命令表示,客户端请求的键值对所在的哈希槽 13320,实际是在192.168.1.300 这个实例上。通过返回的 MOVED 命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。这样一来,客户端就可以直接和192.168.1.300 连接,并发送操作请求了。

​ 结合图示理解 MOVED 重定向命令的使用方法:

image-20240722130657734

​ 结合图示分析:由于负载均衡,Slot 2 中的数据已经从实例 2 迁移到了实例 3,但是客户端缓存仍然记录着“Slot 2 在实例 2”的信息,所以会给实例 2 发送命令。实例 2 中此时并没有slot2 的数据,于是给客户端返回一条 MOVED 命令,把 Slot 2 的最新位置(Slot2 在实例 3 上)返回给客户端,客户端就会再次向实例 3 发送请求,同时还会更新本地缓存,把 Slot 2 与实例的对应关系更新过来。

客户端 ASK 重定向命令

​ 基于上述图示分析,当客户端给实例 2 发送命令时,Slot 2 中的数据已经全部迁移到了实例 3。但在实际应用时,如果 Slot 2 中的数据比较多,就可能会出现一种情况:客户端向实例 2 发送请求,但此时Slot 2 中的数据只有一部分迁移到了实例 3,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息,如下所示

​ 例如Slot 2 中有key1、key2、key3、key4 数据, 由于负载均衡 Slot 2 正从实例 2 往实例 3 迁移,key1 和 key2 已完成迁移,但key3 和 key4 还在实例 2。客户端向实例 2 请求 key2 后,就会收到实例 2 返回的 ASK 命令,其具有两层含义:

  • Slot 数据还在迁移中
  • ASK 命令将客户端所请求数据的最新实例地址返回给客户端

​ 随后客户端需要给实例3发送ASKING命令,然后再发送操作命令获取数据

image-20240722132906473

​ 和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。所以,如果此时客户端再次请求 Slot 2 中的数据,它还是会给实例 2 发送请求。即 ASK 命令的作用只是让客户端能给新实例发送一次请求,而不像 MOVED 命令那样,会更改本地缓存,让后续所有命令都发往新实例

4.切片集群总结

切片集群引入

​ 哨兵模式实现了故障自动转移的能力,但随着业务规模的不断扩展,用户量膨胀,并发量持续提升,会出现了 Redis 响应慢的情况。使用 Redis Cluster 集群,主要解决大数据量存储导致的各种慢问题,便于横向拓展。在面对千万级甚至亿级别的流量的时候,很多公司的做法是在千百台的实例节点组成的集群上进行流量调度、服务治理的。

​ Cluster 是具备Master 和 Slave模式,Redis 集群中的每个实例节点都负责一些槽位,节点之间保持TCP通信,当Master发生了宕机, Redis Cluster自动会将对应的Slave节点选为Master,来继续提供服务。

​ 在应对数据量扩容时,有纵向扩展和横向扩展两种方案:

  • 纵向扩展:通过增加内存实现,方案简单直接,但是会造成数据库的内存过大,导致性能变慢
  • 横向扩展:Redis 切片集群提供了横向扩展的模式,通过使用多个实例,并给每个实例配置一定数量的哈希槽,数据可以通过键的哈希值映射到哈希槽,再通过哈希槽分散保存到不同的实例上。此方案扩展性好,不管有多少数据,切片集群都能应对。

切片集群的分布式管理问题

​ 客户端能够快捷的连接到服务端,主要是将slots与实例节点的映射关系存储在本地,当需要访问的时候,对key进行CRC16计算后,再对16384 取模得到对应的 Slot 索引,再定位到相应的实例上。实现高效的连接。

​ 但基于集群的实例增减,或者是为了实现负载均衡而进行的数据重新分布,会导致哈希槽和实例的映射关系发生变化,客户端发送请求时,会收到命令执行报错信息。掌握MOVED 和 ASK 命令应用,进而解决相关场景问题

切片集群的实现

​ Redis 官方通过Redis Cluster 方案实现切片集群,其定义了数据和实例的对应规则

​ 在 Redis 3.0 之前,Redis 官方并没有提供切片集群方案,但其实业界已经有了一些切片集群的方案,例如基于客户端分区的 ShardedJedis,基于代理的 Codis、Twemproxy 等。这些方案的应用早于 Redis Cluster 方案,在支撑的集群实例规模、集群稳定性、客户端友好性方面也都有着各自的优势。基于此,当再碰到业务发展带来的数据量巨大的难题时,就可以根据这些方案的特点,选择合适的方案实现切片集群,以应对业务需求

Cluster 实现原理

​ 在前面的案例学习中了解了Redis高可用的两种架构模式:主从模式、哨兵模式。在Redis实例发生故障时,其具备主从自动切换、故障转移的能力,以保证服务的高可用。 但随着业务规模的不断扩展、用户量膨胀,并发量持续提升。原有的主从架构,已经远远达不到场景需求了,会伴随着一些问题出现,比如:

  • 单机的CPU、内存、连接数、计算力都是有极限的,不能无限制的承载流量的扩增
  • 超额的请求、大规模的数据计算,导致必然的慢响应,需要适当的推进架构的演进,来满足发展的需要。

​ 因此引入切片集群架构,通过Redis Cluster方案实现切片集群。基于上述场景分析,掌握Redis Cluster的场景应用,现结合案例拆解Cluster的实现原理。

1.集群的组群过程

​ 集群是由一个个互相独立的节点(readis node)组成的, 一开始他们都是隔离、毫无联系的。需要通过一些操作,把这些节点聚集在一起,才能组成真正的可协调工作的集群。各个节点的联通是通过 CLUSTER MEET 命令完成的:CLUSTER MEET <ip> <port> ​ 做法:其中一个node向另外一个 node(指定 ip 和 port) 发送 CLUSTER MEET 命令,让两个节点进行握手(handshake操作) ,握手成功之后,node 节点就会将握手另一侧的节点添加到当前节点所在的集群中。以此类推,一步步的将需要聚集的节点都圈入同一个集群中

image-20240722183523813

2.集群数据分片原理

哈希槽的划分

​ 将整个Redis数据库划分为16384个哈希槽,Redis集群可能有n个实例节点,每个节点可以处理0个 到至多 16384 个槽点,这些节点把 16384个槽位瓜分完成。而实际存储的Redis键值信息也必然归属于这 16384 个槽的其中一个。slots 与 Redis Key 的映射是通过以下两个步骤完成的:

  • 使用 CRC16 算法计算键值对信息的Key,会得出一个 16 bit 的值。
  • 将第1步中得到的 16 bit 的值对 16384 取模,得到的值会在 0 ~ 16383 之间,映射到对应到哈希槽中。

​ 可能在一些特殊的情况下,如果想把某些key固定到某个slot上面,也就是同一个实例节点上。可以用hash tag能力,强制 key 所归属的槽位等于 tag 所在的槽位。其实现方式为在key中加个{},例如test_key{1}。使用hash tag后客户端在计算key的crc16时,只计算{}中数据。如果没使用hash tag,客户端会对整个key进行crc16计算。下面演示下hash tag使用:

# 如下示案例:使用hash tag 后会对应到通一个hash slot:1024127.0.0.1:6380> cluster keyslot user:case{1}
(integer) 1024
127.0.0.1:6380> cluster keyslot user:favor
(integer) 1023
127.0.0.1:6380> cluster keyslot user:info{1}
(integer) 1024

哈希槽的映射

​ 有两种方式可以实现:

  • 方式1:初始化的时候均匀分配 ,使用cluster create创建,将 16384 个slots 平均分配在集群实例上,比如有n个节点,那每个节点的槽位就是 16384 / n 个
  • 方式2:联通集群、手动分配哈希槽,优点在于可结合服务器资源灵活适配,让性能好的实例节点多分担一些压力
    • 通过CLUSTER MEET命令将多个节点联通成一个集群,刚联通的时候因为还没分配哈希槽,还是处于offline状态
    • 使用 cluster addslots 命令来指定哈希槽的分配
# 例如有4个节点:实例1管理0-7120哈希槽;实例2管理7121-9945哈希槽;实例3管理9946-13005哈希槽;实例4管理13006-16383哈希槽
redis-cli -h 192.168.0.1 –p 6379 cluster addslots ,7120
redis-cli -h 192.168.0.2 –p 6379 cluster addslots 7121,9945
redis-cli -h 192.168.0.3 –p 6379 cluster addslots 9946,13005
redis-cli -h 192.168.0.4 –p 6379 cluster addslots 13006,16383

3.数据复制过程和故障转移

数据复制

​ Cluster 是具备Master 和 Slave模式,Redis 集群中的每个实例节点都负责一些槽位,例如图示四个节点分管了不同的槽位区间。而每个Master至少需要一个Slave节点,Slave 节点是通过主从复制方式同步主节点数据。 节点之间保持TCP通信,当Master发生了宕机, Redis Cluster自动会将对应的Slave节点选为Master,来继续提供服务。与纯主从模式不同的是,主从节点之间并没有读写分离, Slave 只用作 Master 宕机的高可用备份,所以更合理来说应该是主备模式。 ​ 如果主节点没有从节点,那么一旦发生故障时,集群将完全处于不可用状态。 但也允许配置 cluster-require-full-coverage参数,及时部分节点不可用,其他节点正常提供服务,这是为了避免全盘宕机。主从切换之后,故障恢复的主节点,会转化成新主节点的从节点。这种自愈模式对提高可用性非常有帮助

故障检测

​ 一个节点认为某个节点宕机不能说明这个节点真的挂起了,无法提供服务了。只有占据多数的实例节点都认为某个节点挂起了,这时候cluster才进行下线和主从切换的工作。Redis 集群的节点采用 Gossip 协议来广播信息,每个节点都会定期向其他节点发送ping命令,如果接受ping消息的节点在指定时间内没有回复pong,则会认为该节点失联了(PFail),则发送ping的节点就把接受ping的节点标记为主观下线。 ​ 如果集群半数以上的主节点都将主节点 xxx 标记为主观下线,则节点 xxx 将被标记为客观下线,然后向整个集群广播,让其它节点也知道该节点已经下线,并立即对下线的节点进行主从切换

主从故障转移

当一个从节点发现自己正在复制的主节点进入了已下线,则开始对下线主节点进行故障转移,故障转移的步骤如下:

  • 如果只有一个slave节点,则从节点会执行SLAVEOF no one命令,成为新的主节点;
  • 如果是多个slave节点,则采用选举模式进行,竞选出新的Master;
    • 集群中设立一个自增计数器,初始值为 0 ,每次执行故障转移选举,计数就会+1;
    • 检测到主节点下线的从节点向集群所有master广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,所有收到消息、并具备投票权的主节点都向这个从节点投票;
    • 如果收到消息、并具备投票权的主节点未投票给其他从节点(只能投一票),则返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示支持;
    • 参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的选票 大于等于 (n/2) + 1 支持,n代表所有具备选举权的master,那么这个从节点就被选举为新主节点;
    • 如果这一轮从节点都没能争取到足够多的票数,则发起再一轮选举(自增计数器+1),直至选出新的master;
  • 新的主节点会撤销所有对已下线主节点的slots指派,并将这些slots全部指派给自己;
  • 新的主节点向集群广播一条PONG消息,让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且接管了原本由已下线节点负责处理的槽;
  • 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成;

client 访问数据集群的过程

定位数据所在节点

​ Redis 中的每个实例节点会将自己负责的哈希槽信息 通过 Gossip 协议广播给集群中其他的实例,实现了slots分配信息的扩散。这样的话,每个实例都知道整个集群的哈希槽分配情况以及映射信息。所以客户端想要快捷的连接到服务端,并对某个redis数据进行快捷访问,一般是经过以下步骤:

  • 客户端连接任一实例,获取到slots与实例节点的映射关系,并将该映射关系的信息缓存在本地
  • 将需要访问的redis信息的key,经过CRC16计算后,再对16384 取模得到对应的 Slot 索引
  • 通过slot的位置进一步定位到具体所在的实例,再将请求发送到对应的实例上

4.Redis Cluster是AP架构还是CP架构?

​ 所谓CAP分别为:C强一致性(Consistency),A可用性(Availability)和P分区容错性(Partition Tolerance)。

​ 作为一个分布式系统分区容错性P一定是需要考虑的。但需注意,分区容错性是允许某部分或者一些节点或者数据丢失的情况下,系统仍能继续工作。这个取决于分布式系统里面各自的算法,常见的共识算法。

​ 至于C和A则根据具体场景,因此拆解出CP、AP架构:

  • CP架构更强调数据的一致性,如果有节点挂掉,则所有节点返回失败的信息;
  • AP架构更强调服务的可用性,如果有节点挂掉,则节点返回自身写入的最新信息;

实验验证:验证redis cluster是AP还是CP架构,查看集群节点挂掉之后能否正常提供服务

todo:redis cluster 集群搭建open in new window

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3