Redis保存1000万个键值对,大约占了内存空间大约是20GB。所以就选择一台32GB内存的云主机来部署Redis。但是,在使用的过程中,我发现,Redis的响应有时会非常慢。然后,使用INFO命令查看Redis的latest_fork_usec指标值(表示最近一次fork的耗时),结果显示这个指标值特别高,快到秒级别了。
这主要是在RDB进行持久化fork子进程时,fork 执⾏的耗时与 Redis 数据量成正相关,而fork在执行时会阻塞主线程,数据量越大,fork操作造成的主线程阻塞的时间越长。所以就导致了redis相应变慢了。
这时就可以使用redis cluster方案了。
切片集群,也叫分片集群,就是指启动多个Redis实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。
为了保存大量数据,我们使用了大内存云主机和切片集群两种方法。实际上,这两种方法分别对应着Redis应对数据量增多的两种方案:纵向扩展(scale up)和横向扩展(scale out)。
两种方式的优缺点
纵向扩展的优点:实施起来简单、直接。
纵向扩展的缺点:
横向扩展的优点:不需要担⼼单个实例的硬件和成本的限制。
横向扩展的缺点:切片集群不可避免地涉及到多个实例的分布式管理问题。
如何解决数据切片后,在多个实例之间如何分布和客户端怎么确定想要访问的数据在哪个实例上的问题?
切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案。在Redis 3.0之前,官方并没有针对切片集群提供具体的方案。从3.0开始,官方提供了一个名为Redis Cluster的方案,用于实现切片集群。Redis Cluster方案中就规定了数据和实例的对应规则。
Redis Cluster方案采用哈希槽(Hash Slot,接下来直接称之为Slot),来处理数据和实例之间的映射关系。在Redis Cluster方案中,一个切片集群共有16384个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的key,被映射到一个哈希槽中。
映射过程步骤:
在部署Redis Cluster方案时,可以使用cluster create命令创建集群。Redis会自动把这些槽平均分布在集群实例上。例如,如果集群中有N个实例,那么,每个实例上的槽个数为16384/N个。
也可以使用cluster meet命令手动建立实例间的连接,形成集群,再使用cluster addslots命令,指定每个实例上的哈希槽个数。
假设集群中不同Redis实例的内存大小配置不一,如果把哈希槽均分在各个实例上,在保存相同数量的键值对时,和内存大的实例相比,内存小的实例就会有更大的容量压力。遇到这种情况时,你可以根据不同实例的资源配置情况,使用cluster addslots命令手动分配哈希槽。
数据、哈希槽、实例这三者的映射分布情况示意图:
在集群运行的过程中,key1和key2计算完CRC16值后,对哈希槽总个数5取模,再根据各自的模数结果,就可以被映射到对应的实例1和实例3上了。
注意:在手动分配哈希槽时,需要把16384个槽都分配完,否则Redis集群无法正常工作。
在集群创建的时候,实例只知道自己被分配的哈希槽,但是不知道其他实例拥有的哈希槽信息。客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。
客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?
实例和哈希槽的对应关系改变了怎么办?
一般实例和哈希槽改变的原因有一下两种:
此时,实例之间还可以通过相互传递消息,获得最新的哈希槽分配信息,但是,客户端是无法主动感知这些变化的。这就会导致,它缓存的分配信息和最新的分配信息就不一致了,那该怎么办呢?
Redis Cluster方案提供了一种**重定向机制,**所谓的“重定向”,就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。
客户端又是怎么知道重定向时的新实例的访问地址呢?
当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽,那么,这个实例就会给客户端返回下面的MOVED命令响应结果,这个结果中就包含了新实例的访问地址。
GET hello:key
(error) MOVED 13320 172.16.18.5:6379
MOVED的意思是:客户端请求的键值对所在的哈希槽13320,实际是在172.16.18.5这个实例上。通过返回的MOVED命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。这样客户端就可以直接和172.16.19.5连接,并发送操作请求了。
MOVED重定向命令的图示:
可以看到,由于负载均衡,Slot 2中的数据已经从实例2迁移到了实例3,但是,客户端缓存仍然记录着“Slot 2在实例2”的信息,所以会给实例2发送命令。实例2给客户端返回一条MOVED命令,把Slot 2的最新位置(也就是在实例3上),返回给客户端,客户端就会再次向实例3发送请求,同时还会更新本地缓存,把Slot 2与实例的对应关系更新过来。
数据只迁移了一部分就有请求怎么办?
如果Slot 2中的数据比较多,就可能会出现一种情况:客户端向实例1发送请求,但此时,Slot 2中的数据只有一部分迁移到了实例2,还有部分数据没有迁移。在这种迁移部分完成的情况下,客户端就会收到一条ASK报错信息,如下所示:
GET hello:key
(error) ASK 13320 172.16.18.5:6379
⽐如客户端请求定位到 key = “key1”的槽 13320在实例 172.16.18.1上,节点 1 如果找得到就直接执⾏命令,否则响应 ASK 错误信息,并指引客户端转向正在迁移的⽬标节点 172.16.18.5。
ASK命令含义:
和MOVED命令不同,ASK命令并不会更新客户端缓存的哈希槽分配信息。所以,在上图中,如果客户端再次请求Slot 2中的数据,它还是会给实例2发送请求。这也就是说,ASK命令的作用只是让客户端能给新实例发送一次请求,而不像MOVED命令那样,会更改本地缓存,让后续所有命令都发往新实例。
Redis Cluster方案通过哈希槽的方式把键值对分配到不同的实例上,这个过程需要对键值对的key做CRC计算,然后再和哈希槽做映射,这样做有什么好处吗?如果用一个表直接把键值对和实例的对应关系记录下来(例如键值对1在实例2上,键值对2在实例1上),这样就不用计算key和哈希槽的对应关系了,只用查表就行了,Redis为什么不这么做呢?
Redis Cluster不采用把key直接映射到实例的方式,而采用哈希槽的方式原因:
整个集群存储key的数量是无法预估的,key的数量非常多时,直接记录每个key对应的实例映射关系,这个映射表会非常庞大,这个映射表无论是存储在服务端还是客户端都占用了非常大的内存空间。
Redis Cluster采用无中心化的模式(无proxy,客户端与服务端直连),客户端在某个节点访问一个key,如果这个key不在这个节点上,这个节点需要有纠正客户端路由到正确节点的能力(MOVED响应),这就需要节点之间互相交换路由表,每个节点拥有整个集群完整的路由关系。如果存储的都是key与实例的对应关系,节点之间交换信息也会变得非常庞大,消耗过多的网络资源,而且就算交换完成,相当于每个节点都需要额外存储其他节点的路由表,内存占用过大造成资源浪费。
当集群在扩容、缩容、数据均衡时,节点之间会发生数据迁移,迁移时需要修改每个key的映射关系,维护成本高。
而在中间增加一层哈希槽,可以把数据和节点解耦,key通过Hash计算,只需要关心映射到了哪个哈希槽,然后再通过哈希槽和节点的映射表找到节点,相当于消耗了很少的CPU资源,不但让数据分布更均匀,还可以让这个映射表变得很小,利于客户端和服务端保存,节点之间交换信息时也变得轻量。
当集群在扩容、缩容、数据均衡时,节点之间的操作例如数据迁移,都以哈希槽为基本单位进行操作,简化了节点扩容、缩容的难度,便于集群的维护和管理。