redis-集群切片

切片集群

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

在使用的过程中,我发现,Redis 的响应有时会非常慢。后来,我们使用 INFO 命令查看 Redis 的 latest_fork_usec 指标值(表示最近一次 fork 的耗时 ),结果显示这个指标值特别高,快到秒级别了。这跟 Redis 的持久化机制有关系。在使用 RDB 进行持久化时,Redis 会 fork 子进程来完成,fork 操作的用时和 Redis 的数据量是正相关的,而 fork 在执行时会阻塞主线程。数据量越大,fork 操作造成的主线程阻塞的时间越长 。所以,在使用 RDB 对 25GB 的数据进行持久化时,数据量较大,后台运行的子进程在 fork 创建时阻塞了主线程,于是就导致 Redis 响应变慢了。

这个时候,我们注意到了 Redis 的切片集群。虽然组建切片集群比较麻烦,但是它可以保存大量数据,而且对 Redis 主线程的阻塞影响较小。

切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存;

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

Redis 数据量增多处理

Redis 应对数据量增多的两种方案:纵向扩展(scale up)和横向扩展(scale out)

纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。就像下图中,原来的实例内存是 8GB,硬盘是 50GB,纵向扩展后,内存增加到 24GB,磁盘增加到 150GB。

横向扩展:横向增加当前 Redis 实例的个数,就像下图中,原来使用 1 个 8GB 内存、50GB 磁盘的实例,现在使用三个相同配置的实例。

  • 纵向扩展好处和缺点

    首先,纵向扩展的好处是,实施起来简单、直接。不过,这个方案也面临两个潜在的问题。第一个问题是,当使用 RDB 对数据进行持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞(比如刚刚的例子中的情况)。不过,如果你不要求持久化保存 Redis 数据,那么,纵向扩展会是一个不错的选择。

  • 纵向扩展好处和缺点

    这时,你还要面对第二个问题:纵向扩展会受到硬件和成本的限制。这很容易理解,毕竟,把内存从 32GB 扩展到 64GB 还算容易,但是,要想扩充到 1TB,就会面临硬件容量和成本上的限制了。在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择

数据切片后,在多个实例之间如何分布

客户端怎么确定想要访问的数据在哪个实例上?接下来,我们就一个个地解决

Redis Cluster 集群 哈希槽

从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。Redis Cluster 方案中就规定了数据和实例的对应规则。

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

首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。

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

当然, 我们也可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数。

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

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

示意图中的切片集群一共有 3 个实例,同时假设有 5 个哈希槽,我们首先可以通过下面的命令手动分配哈希槽:实例 1 保存哈希槽 0 和 1,实例 2 保存哈希槽 2 和 3,实例 3 保存哈希槽 4。

java 复制代码
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

在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 5 取模,再根据各自的模数结果,就可以被映射到对应的实例 1 和实例 3 上了

客户端如何定位数据

在定位键值对数据时,它所处的哈希槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。但是,要进一步定位到实例,还需要知道哈希槽分布在哪个实例上。

一般来说,客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。

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

客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。

Redis 集群hash在分配

实例和哈希槽的对应关系并不是一成不变的,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个(由于负载均衡)

集群中,实例有新增或删除,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 连接,并发送操作请求了。

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

hash链

原理简介:

每个数据存储进来的时候,要根据hash算法,进行算值取余,存入到对应的机器中。取数据的时候,用同样的hash算法对key进行计算,即可取出数据。

应用场景:

Redis集群扩容或宕机缩减,那么就需要进行全库数据的重洗,hash取模的值调整。这样,就比较耗费时间。所以,该方案,要预先估计一下自己公司业务的数据量多大,服务器的存储能力多大,然后,考虑在扩容时,所需的时间多久,只要在允许的时间范围内,能够完成重洗数据,那就可以采取该方案。该方案的一个好处就是,简单。存数据简单,取数据简单,理解容易。

hash环

原理简介

上面说的Hash链,只经过了1次hash,即把key hash到对应的机器编号。

而Hash环有2次Hash:

(1)把所有机器编号hash到这个环上

(2)把key也hash到这个环上。然后在这个环上进行匹配,看这个key和哪台机器匹配。这样,每个机器负责对应段上的数据。

应用场景

当hash链性能满足不了公司业务数据量的时候,就要采取该方案进行性能提升。

当服务器缩减时,对应段数据向下游转移即可,这样,就不会影响到其他段服务器的数据。

当服务器扩容时,对应服务器下游的服务器数据要进行重洗,把部分数据转移到新扩容的服务器上即可。这样,在查找时,按照最新的hash算法取余,即可取出对应的数据。
这相较于hash链,进行全库冲洗,还是节省了很多
也就是说,hash链和hash环,在新增服务器的时候,都是要冲洗数据的,只是,hash链是全库冲洗(算法复杂度是N),hash环是下游节点冲洗(算法复杂度是1)

那么会有道友问,我直接上第二种方案不就得了。这显然是不行的。

原因:
hash环是有大小的,它的特点是把hash链首尾相连,那么,假设你公司业务只有百万级数据量,你设置成一个hash环。假设,hash环周长是100,小厂有4台服务器,第一次可以人为均匀分布到环上,但是,如果业务量数据增加,导致需要扩容。这时候,如果你对hash环进行扩周长,如果不重新分配服务器在环上的位置,那就会出现数据倾斜问题,如果,重新均匀分布服务器在环上的位置,那么,就要全库重洗数据。所以,这样就和hash链没什么区别。还多出一个数据倾斜问题

那么有道友就说了,那我把hash环周长设置的超大。这样,不就可以减轻扩容时,数据倾斜问题的严重性了吗?并不是这样,当你保持环周长不变的前提下扩容的时候,数据倾斜和环的周长并没有关系。只是和你扩容的服务器策略有关,就是,假设第一次设置4台服务器,那么,你扩容的服务器必须是2的N次方台,这样才能人为的避免数据倾斜。那么,你小厂有这个实力吗?显然没有,不划算。

另外,你的环周长越大,也就意味着取余的除数越大,那么,计算取余的时间就越久,比如,你对2取余,口算即可。你对2的32次方取余,那就要多用很多时间,这样,随着积累,你浪费的时间就很多了。相对于公司业务,数据量不大,但是,损耗的计算时间却很多,那就很不划算了。所以,环的周长也是要考虑的点

所以,使用hash环算法,要考虑两点
1、公司自身实力,每次扩容需要的服务器数据量是:a*2^n。其中,a是第一次均匀分布的服务器数据量,n>=0的整数。这个扩容方法,解决数据倾斜问题。
2、hash环周长大小选择。周长越大,计算越耗费时间。所以,要根据公司业务量大小,选择合理的周长大小,不能太小,否则经常扩容,不能太大,浪漫每次的取余时间。
3、数据倾斜问题的本质,就在于服务器节点在环上的分布是否均匀还是密集。分布密集了,那就会出现数据倾斜问题。

相关推荐
m0_588383321 小时前
进阶SpringBoot之集合 Redis
java·开发语言·数据库·spring boot·redis
Jayden2 小时前
字节面试:Redis为什么要持久化?有几种方式?
redis·面试·mybatis
咖啡煮码3 小时前
RedisTemplate混用带来的序列化问题
spring boot·redis
液态不合群4 小时前
【解决方案】Java 互联网项目中常见的 Redis 缓存应用场景
java·redis·缓存
Jtti4 小时前
如何在Ubuntu上查看和刷新DNS缓存
linux·ubuntu·缓存
罗曼蒂克在消亡12 小时前
缓存的思考与总结
java·缓存·intellij idea
丁总学Java12 小时前
缓存穿透 问题(缓存空对象)
redis
林九生13 小时前
【Redis】个人笔记
数据库·redis·笔记
OEC小胖胖13 小时前
MyBatis系统学习(四)——MyBatis的关联映射和缓存机制
java·后端·学习·缓存·mybatis·web