You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 09 | 切片集群:数据增多了,是该加内存还是加实例?
你好,我是蒋德钧。今天我们来学习切片集群。
我曾遇到过这么一个需求要用Redis保存5000万个键值对每个键值对大约是512B为了能快速部署并对外提供服务我们采用云主机来运行Redis实例那么该如何选择云主机的内存容量呢
我粗略地计算了一下这些键值对所占的内存空间大约是25GB5000万\*512B。所以当时我想到的第一个方案就是选择一台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响应变慢了。
看来第一个方案显然是不可行的我们必须要寻找其他的方案。这个时候我们注意到了Redis的切片集群。虽然组建切片集群比较麻烦但是它可以保存大量数据而且对Redis主线程的阻塞影响较小。
切片集群也叫分片集群就是指启动多个Redis实例组成一个集群然后按照一定的规则把收到的数据划分成多份每一份用一个实例来保存。回到我们刚刚的场景中如果把25GB的数据平均分成5份当然也可以不做均分使用5个实例来保存每个实例只需要保存5GB数据。如下图所示
![](https://static001.geekbang.org/resource/image/79/26/793251ca784yyf6ac37fe46389094b26.jpg "切片集群架构图")
那么在切片集群中实例在为5GB数据生成RDB时数据量就小了很多fork子进程一般不会给主线程带来较长时间的阻塞。采用多个实例保存数据切片后我们既能保存25GB数据又避免了fork子进程阻塞主线程而导致的响应突然变慢。
在实际应用Redis时随着用户或业务规模的扩展保存大量数据的情况通常是无法避免的。而切片集群就是一个非常好的解决方案。这节课我们就来学习一下。
## 如何保存更多数据?
在刚刚的案例里为了保存大量数据我们使用了大内存云主机和切片集群两种方法。实际上这两种方法分别对应着Redis应对数据量增多的两种方案纵向扩展scale up和横向扩展scale out
* **纵向扩展**升级单个Redis实例的资源配置包括增加内存容量、增加磁盘容量、使用更高配置的CPU。就像下图中原来的实例内存是8GB硬盘是50GB纵向扩展后内存增加到24GB磁盘增加到150GB。
* **横向扩展**横向增加当前Redis实例的个数就像下图中原来使用1个8GB内存、50GB磁盘的实例现在使用三个相同配置的实例。
![](https://static001.geekbang.org/resource/image/7a/1a/7a512fec7eba789c6d098b834929701a.jpg "纵向扩展和横向扩展对比图")
那么,这两种方式的优缺点分别是什么呢?
首先,纵向扩展的好处是,**实施起来简单、直接**。不过,这个方案也面临两个潜在的问题。
第一个问题是当使用RDB对数据进行持久化时如果数据量增加需要的内存也会增加主线程fork子进程时就可能会阻塞比如刚刚的例子中的情况。不过如果你不要求持久化保存Redis数据那么纵向扩展会是一个不错的选择。
不过,这时,你还要面对第二个问题:**纵向扩展会受到硬件和成本的限制**。这很容易理解毕竟把内存从32GB扩展到64GB还算容易但是要想扩充到1TB就会面临硬件容量和成本上的限制了。
与纵向扩展相比横向扩展是一个扩展性更好的方案。这是因为要想保存更多的数据采用这种方案的话只用增加Redis的实例个数就行了不用担心单个实例的硬件和成本限制。**在面向百万、千万级别的用户规模时横向扩展的Redis切片集群会是一个非常好的选择**。
不过,在只使用单个实例的时候,数据存在哪儿,客户端访问哪儿,都是非常明确的,但是,切片集群不可避免地涉及到多个实例的分布式管理问题。要想把切片集群用起来,我们就需要解决两大问题:
* 数据切片后,在多个实例之间如何分布?
* 客户端怎么确定想要访问的数据在哪个实例上?
接下来,我们就一个个地解决。
## 数据切片和实例的对应分布关系
在切片集群中数据需要分布在不同实例上那么数据和实例之间如何对应呢这就和接下来我要讲的Redis Cluster方案有关了。不过我们要先弄明白切片集群和Redis Cluster的联系与区别。
实际上切片集群是一种保存大量数据的通用机制这个机制可以有不同的实现方案。在Redis 3.0之前官方并没有针对切片集群提供具体的方案。从3.0开始官方提供了一个名为Redis Cluster的方案用于实现切片集群。Redis Cluster方案中就规定了数据和实例的对应规则。
具体来说Redis Cluster方案采用哈希槽Hash Slot接下来我会直接称之为Slot来处理数据和实例之间的映射关系。在Redis Cluster方案中一个切片集群共有16384个哈希槽这些哈希槽类似于数据分区每个键值对都会根据它的key被映射到一个哈希槽中。
具体的映射过程分为两大步首先根据键值对的key按照[CRC16算法](https://en.wikipedia.org/wiki/Cyclic_redundancy_check)计算一个16 bit的值然后再用这个16bit值对16384取模得到0~16383范围内的模数每个模数代表一个相应编号的哈希槽。关于CRC16算法不是这节课的重点你简单看下链接中的资料就可以了。
那么这些哈希槽又是如何被映射到具体的Redis实例上的呢
我们在部署Redis Cluster方案时可以使用cluster create命令创建集群此时Redis会自动把这些槽平均分布在集群实例上。例如如果集群中有N个实例那么每个实例上的槽个数为16384/N个。
当然, 我们也可以使用cluster meet命令手动建立实例间的连接形成集群再使用cluster addslots命令指定每个实例上的哈希槽个数。
举个例子假设集群中不同Redis实例的内存大小配置不一如果把哈希槽均分在各个实例上在保存相同数量的键值对时和内存大的实例相比内存小的实例就会有更大的容量压力。遇到这种情况时你可以根据不同实例的资源配置情况使用cluster addslots命令手动分配哈希槽。
为了便于你理解,我画一张示意图来解释一下,数据、哈希槽、实例这三者的映射分布情况。
![](https://static001.geekbang.org/resource/image/7d/ab/7d070c8b19730b308bfaabbe82c2f1ab.jpg)
示意图中的切片集群一共有3个实例同时假设有5个哈希槽我们首先可以通过下面的命令手动分配哈希槽实例1保存哈希槽0和1实例2保存哈希槽2和3实例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
```
在集群运行的过程中key1和key2计算完CRC16值后对哈希槽总个数5取模再根据各自的模数结果就可以被映射到对应的实例1和实例3上了。
另外,我再给你一个小提醒,**在手动分配哈希槽时需要把16384个槽都分配完否则Redis集群无法正常工作**。
好了,通过哈希槽,切片集群就实现了数据到哈希槽、哈希槽再到实例的分配。但是,即使实例有了哈希槽的映射信息,客户端又是怎么知道要访问的数据在哪个实例上呢?接下来,我就来和你聊聊。
## 客户端如何定位数据?
在定位键值对数据时,它所处的哈希槽是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。但是,要进一步定位到实例,还需要知道哈希槽分布在哪个实例上。
一般来说,客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的。
那么客户端为什么可以在访问任何一个实例时都能获得所有的哈希槽信息呢这是因为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连接,并发送操作请求了。
我画一张图来说明一下MOVED重定向命令的使用方法。可以看到由于负载均衡Slot 2中的数据已经从实例2迁移到了实例3但是客户端缓存仍然记录着“Slot 2在实例2”的信息所以会给实例2发送命令。实例2给客户端返回一条MOVED命令把Slot 2的最新位置也就是在实例3上返回给客户端客户端就会再次向实例3发送请求同时还会更新本地缓存把Slot 2与实例的对应关系更新过来。
![](https://static001.geekbang.org/resource/image/35/09/350abedefcdbc39d6a8a8f1874eb0809.jpg "客户端MOVED重定向命令")
需要注意的是在上图中当客户端给实例2发送命令时Slot 2中的数据已经全部迁移到了实例3。在实际应用时如果Slot 2中的数据比较多就可能会出现一种情况客户端向实例2发送请求但此时Slot 2中的数据只有一部分迁移到了实例3还有部分数据没有迁移。在这种迁移部分完成的情况下客户端就会收到一条ASK报错信息如下所示
```
GET hello:key
(error) ASK 13320 172.16.19.5:6379
```
这个结果中的ASK命令就表示客户端请求的键值对所在的哈希槽13320在172.16.19.5这个实例上但是这个哈希槽正在迁移。此时客户端需要先给172.16.19.5这个实例发送一个ASKING命令。这个命令的意思是让这个实例允许执行客户端接下来发送的命令。然后客户端再向这个实例发送GET命令以读取数据。
看起来好像有点复杂,我再借助图片来解释一下。
在下图中Slot 2正在从实例2往实例3迁移key1和key2已经迁移过去key3和key4还在实例2。客户端向实例2请求key2后就会收到实例2返回的ASK命令。
ASK命令表示两层含义第一表明Slot数据还在迁移中第二ASK命令把客户端所请求数据的最新实例地址返回给客户端此时客户端需要给实例3发送ASKING命令然后再发送操作命令。
![](https://static001.geekbang.org/resource/image/e9/b0/e93ae7f4edf30724d58bf68yy714eeb0.jpg "客户端ASK重定向命令")
和MOVED命令不同**ASK命令并不会更新客户端缓存的哈希槽分配信息**。所以在上图中如果客户端再次请求Slot 2中的数据它还是会给实例2发送请求。这也就是说ASK命令的作用只是让客户端能给新实例发送一次请求而不像MOVED命令那样会更改本地缓存让后续所有命令都发往新实例。
## 小结
这节课,我们学习了切片集群在保存大量数据方面的优势,以及基于哈希槽的数据分布机制和客户端定位键值对的方法。
在应对数据量扩容时虽然增加内存这种纵向扩展的方法简单直接但是会造成数据库的内存过大导致性能变慢。Redis切片集群提供了横向扩展的模式也就是使用多个实例并给每个实例配置一定数量的哈希槽数据可以通过键的哈希值映射到哈希槽再通过哈希槽分散保存到不同的实例上。这样做的好处是扩展性好不管有多少数据切片集群都能应对。
另外集群的实例增减或者是为了实现负载均衡而进行的数据重新分布会导致哈希槽和实例的映射关系发生变化客户端发送请求时会收到命令执行报错信息。了解了MOVED和ASK命令你就不会为这类报错而头疼了。
我刚刚说过在Redis 3.0 之前Redis官方并没有提供切片集群方案但是其实当时业界已经有了一些切片集群的方案例如基于客户端分区的ShardedJedis基于代理的Codis、Twemproxy等。这些方案的应用早于Redis Cluster方案在支撑的集群实例规模、集群稳定性、客户端友好性方面也都有着各自的优势我会在后面的课程中专门和你聊聊这些方案的实现机制以及实践经验。这样一来当你再碰到业务发展带来的数据量巨大的难题时就可以根据这些方案的特点选择合适的方案实现切片集群以应对业务需求了。
## 每课一问
按照惯例给你提一个小问题Redis Cluster方案通过哈希槽的方式把键值对分配到不同的实例上这个过程需要对键值对的key做CRC计算然后再和哈希槽做映射这样做有什么好处吗如果用一个表直接把键值对和实例的对应关系记录下来例如键值对1在实例2上键值对2在实例1上这样就不用计算key和哈希槽的对应关系了只用查表就行了Redis为什么不这么做呢
欢迎你在留言区畅所欲言,如果你觉得有收获,也希望你能帮我把今天的内容分享给你的朋友,帮助更多人解决切片集群的问题。