Redis核心技术07-切片集群
# Redis核心技术07-切片集群
在Redis持久化中,如果使用RDB进行持久化,Redis会fork子进程来完成,而fork操作用时是和Redis的数据量正相关的,而fork的执行会阻塞主线程。数据量越多,fork操作造成的主线程阻塞时间就越长。所以如果对大数据量进行持久化操作,后台运行的子进程fork创建时阻塞主线程,就会导致Redis响应变慢。
# 切片集群
切片集群,也叫分片集群。就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。比如25G的数据,我们启动5个Redis实例,如果按照平分的话,那么一个Redis实例就拥有5G的数据。
数据不一定是平分,有可能一个实例有6G数据,另一个实例只有4G数据。
通过切片集群,我们就可以从原来为25G数据生成RDB,到现在每个实例只需要为5G数据生成RDB。这意味着我们fork子进程的时间将大大减小,这样就不会给主线程带来较长时间的阻塞。采用多个实例保存数据切片后,既能保存大数据量,也避免fork子进程阻塞主线程导致响应变慢。
# 如何保存更多数据
Redis应对数据量增多的两种方案:纵向扩展(scale up)和横向扩展(scale out)。
- 纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。
- 横向扩展:横向增加当前 Redis 实例的个数。
纵向扩展的优缺点:
优点:
- 实施起来简单、直接。
缺点:
- 即使增加了存储的内存,但是如果对数据进行持久化,随着持久化的数据增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞。
- 需要考虑到硬件和成本的限制。
横向扩展的优缺点:
优点:
- 只需要增加Redis的实例个数,不用担心单个实例的硬件和成本。
缺点:
- 涉及到多个实例的分布式管理问题。
- 数据切片后,在多个实例之间如何分布?
- 客户端如何确认要访问的数据在哪个实例上?
# 数据切片和实例对应分布关系
在切片集群中,数据需要分布在不同实例上。这就需要数据和实例对应起来。实际上,切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案。从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。
具体来说,Redis Cluster方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。一个切片集群有16384个哈希槽。
具体的映射过程分为两大步:首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
此时数据已经和槽位对应上了,还需要将槽位和实例对应上。
我们在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。
现在我们假设有三个实例的Redis和5个槽位,那么他们的对应关系如下:
在设置实例和槽位关系的时候,我们也可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数。
在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
通过哈希槽,切片集群就实现了数据到哈希槽、哈希槽再到实例的分配。现在就需要让客户端知道要访问的数据在哪个实例上了。
# 客户端定位数据
在定位键值对数据时,所处的哈希槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。接下来要进一步定位到实例,就需要知道这个哈希槽分布在哪个实例上。
顺序是:键值key -> 哈希槽 -> 实例
客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发送给客户端。在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散(共享)。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。
客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
但是,在实际情况中,实例和哈希槽对应关系并非一直不变的。
- 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
- 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。
此时,实例之间可以通过相互传递消息来达到最新的哈希槽位信息的扩散,但是客户端是无法感知这些变化的。这就会导致客户端缓存的槽位分配信息和实际槽位分配信息不一致。
对于这种情况,Redis Cluster 方案提供了一种重定向机制,所谓的“重定向”,就是指:当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址。客户端要再给一个新实例地址发送操作命令。
GET hello:key
(error) MOVED 13320 172.16.19.5:6379
其中,MOVED 命令表示,客户端请求的键值对所在的哈希槽 13320,实际是在 172.16.19.5 这个实例上。通过返回的 MOVED 命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。这样一来,客户端就可以直接和 172.16.19.5 连接,并发送操作请求了。
在上图中,slot2已经从实例2迁移到实例3了,但是客户端缓存的槽位信息是slot2还是在实例2,那么客户端最先还是会发送请求给实例2,然后实例2给客户端一条MOVED命令,说slot2的最新位置在实例3,那么客户端会再次给实例3发送请求,同时还会更新本地缓存。
在实际情况中,在客户端请求某个实例的时候,对应的槽位并没有完全迁移完毕。比如此刻slot2中key1和key2已经迁移到实例3了,但是key3和key4还在实例2。
在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息,如下所示:
GET hello:key
(error) ASK 13320 172.16.19.5:6379
客户端向实例2请求key2后,就会收到实例2返回的ASK命令。ASK命令有两层含义:
- 表面slot数据还在迁移中
- ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时客户端需要往最新地址发送请求命令。
和 MOVED 命令不同,ASK 命令并不会更新客户端缓存的哈希槽分配信息。当客户端请求key3完毕后,如果再请求key4,它仍然会朝着实例2发送请求。