Redis中切片集群详解
目录
- 一.切片集群
- 1.什么是切片集群?
- 2.如何保存更多的数据?
- 2.1横向扩展与纵向扩展
- 2.2横向扩展和纵向扩展的优缺点
- 3.切片集群面临两大问题:
- 3.1横向扩展:数据切片和实例的对应分布关系
- 3.2客户端如何定位数据?
- 3.切片集群总结
- 二:切片集群方案:Codis \ Redis Cluster
- 1.Codis 的整体架构和基本流程
- 1.1Codis 是如何处理请求的
- 2.Codis的关键技术原理
- 2.1数据如何在集群里分布
- 2.2例子
- 2.3Codis 和 Redis Cluster数据分布上的区别
- 3.集群扩容和数据迁移
- 3.1增加codis server
- 3.2增加codis proxy
- 4编程客栈:客户端能否与集群直接交互
- 5.怎么保证集群可靠性?
- 5.1codis server保证可靠性方法
- 5.2codis proxy 和 Zookeeper可靠性
- 5.3codis dashboard 和 codis fe可靠性
- 6.切片集群方案选择建议
- 6.1Codis 和 Redis Cluster 区别
- 6.2实际应用中的两种方案
- 7.Codis 和 Redis Cluster总结
- 三.通信开销:限制Redis Cluster规模的关键因素
- 1:为什么要限定集群规模呢?
- 2:实例通信方法和对集群规模的影响
- 2.1Gossip 协议
- 3.通信的影响
- 3.1Gossip 消息大小
- 3.2实例间通信频率
- 4.如何降低实例间的通信开销?
- 4.1减小实例传输的消息大小
- 4.2降低实例间发送消息的频率:
- 5.通信开销总结
一.切片集群
Redis中,数据增多了,是该加内存还是加实例?
采用云主机来运行 Redis 实例,那么,该如何选择云主机的内存容量呢?
用 Redis 保存 5000 万个键值对,每个键值对大约是 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 主线程的阻塞影响较小。
如果把 25GB 的数据平均分成 5 份(当然,也可以不做均分),使用 5 个实例来保存,每个实例只需要保存 5GB 数据。如下图所示:
那么,在切片集群中,实例在为 5GB 数据生成 RDB 时,数据量就小了很多,fork 子进程一般不会给主线程带来较长时间的阻塞。采用多个实例保存数据切片后,我们既能保存 25GB 数据,又避免了 fork 子进程阻塞主线程而导致的响应突然变慢。
1.什么是切片集群?
切片集群,也叫分片集群,就是指启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。
2.如何保存更多的数据?
2.1横向扩展与纵向扩展
上边案例中使用了大内存云片机和切片集群的方法。这两种方法分别对应着Redis应对的数据量增多的两种方案:纵向扩展(sclae up)和横向扩展(scale out).
- 纵向扩展:升级单个 Redis 实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的 CPU。就像下图中,原来的实例内存是 8GB,硬盘是 50GB,纵向扩展后,内存增加到 24GB,磁盘增加到 150GB。
- 横向扩展:横向增加当前 Redis 实例的个数,就像下图中,原来使用 1 个 8GB 内存、50GB 磁盘的实例,现在使用三个相同配置的实例。
2.2横向扩展和纵向扩展的优缺点
2.2.1纵向扩展
好处:实施起来简单、直接。
潜在问题:
第一个问题是,当使用 RDB 对数据进行持久化时,如果数据量增加,需要的内存也会增加,主线程 fork 子进程时就可能会阻塞(比如刚刚的例子中的情况)。不过,如果你不要求持久化、保存 Redis 数据,那么,纵向扩展会是一个不错的选择。
第二个问题:纵向扩展会受到硬件和成本的限制。这很容易理解,毕竟,把内存从 32GB 扩展到 64GB 还算容易,但是,要想扩充到 1TB,就会面临硬件容量和成本上的限制了。
2.2.2横向扩展
横向扩展是一个扩展性更好的方案。要想保存更多的数据,采用这种方案的话,只用增加 Redis 的实例个数就行了,不用担心单个实例的硬件和成本限制。在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。
3.切片集群面临两大问题:
数据切片后,在多个实例之间如何分布?
客户端怎么确定想要访问的数据在哪个实例上?
3.1横向扩展:数据切片和实例的对应分布关系
在切片集群中,数据需要分布在不同实例上,数据和实例之间如何对应呢?
Redis Cluster 方案
在 Redis 3.0 之前,官方并没有针对切片集群提供具体的方案。从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群。Redis Cluster 方案中就规定了数据和实例的对应规则。
3.1.1什么是Redis Cluster?
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
具体的映射过程分为两大步:首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;然后,再用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
3.1.2哈希槽又是如何被映射到具体的 Redis 实例?
在部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么,每个实例上的槽个数为 16384/N 个。
也可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数
举个例子,假设集群中不同 Redis 实例的内存大小配置不一,如果把哈希槽均分在各个实例上,在保存相同数量的键值对时,和内存大的实例相比,内存小的实例就会有更大的容量压力。遇到这种情况时,你可以根据不同实例的资源配置情况,使用 cluster addslots 命令手动分配哈希槽。
示意图中的切片集群一共有 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 集群无法正常工作。
切片集群就实现了数据到哈希槽、哈希槽再到实例的分配
即使实例有了哈希槽的映射信息,客户端又是怎么知道要访问的数据在哪个实例上呢?
3.2客户端如何定位数据?
在定位键值对数据时,它所处的哈希槽slots是可以通过计算得到的,这个计算可以在客户端发送请求时来执行。但是,要进一步定位到实例,还需要知道哈希槽分布在哪个实例上。
一般来说,客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端。但是,在集群刚刚创建的时候,每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息。
客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?
Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。
在集群中,实例和哈希槽的对应关系并不是一成不变的,最常见的变化有两个:
- 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽;
- 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍。
实例之间还可以通过相互传递消息,获得最新的哈希槽分配信息
客户端是无法主动感知这些变化的。这就会导致,它缓存的分配信息和最新的分配信息就不一致了,那该怎么办呢?
Redis Cluster 方案提供了一种重定向机制,就是指,客户端给一个实例发送数据读写操作时,这个实例上并没有相应的数据,客户端要再给一个新实例发送操作命令。
3.2.1MOVED 重定向命令的使用方法
客户端怎么知道重定向时新实例访问地址?客户端端请求到了一个不包含 key 对应的哈希槽,集群将做何响应?
当客户端把一个键值对的操作请求发送给一个实例,这个实例上没有键值对映射的哈希槽,这个实例就会给客户端返回MOVED命令响应结果,结果中就包含了新实例的访问地址。
get hello:key (error)MOVED 13320 172.16.19.5:6379
MOVED 命令表示,客户端请求的键值对所在的哈希槽 13320,实际是在 172.16.19.5 这个实例上。通过返回的 MOVED 命令,就相当于把哈希槽所在的新实例的信息告诉给客户端了。这样一来,客户端就可以直接和 172.16.19.5 连接,并发送操作请求了。
由于负载均衡,Slot 2 中的数据已经从实例 2 迁移到了实例 3,但是,客户端缓存仍然记录着“Slot 2 在实例 2”的信息,所以会给实例 2 发送命令。实例 2 给客户端返回一条 MOVED 命令,把 Slot 2 的最新位置(也就是在实例 3 上),返回给客户端,客户端就会再次向实例 3 发送请求,同时还会更新本地缓存,把 Slot 2 与实例的对应关系更新过来
3.2.2ASK命令使用方法
可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
例如:客户端向实例 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 命令,以读取数据.
ASK 命令表示两层含义:第一,表明 Slot 数据还在迁移中;第二,ASK 命令把客户端所请求数据的最新实例地址返回给客户端,此时,客户端需要给实例 3 发送 ASKING 命令,然后再发送操作命令
3.2.3MOVED命令和ASK命令区别
- MOVED命令会更新客户端缓存的哈希槽分配信息,ASK不会更新客户端缓存。如果客户端再次请求 Slot 2 中的数据,它还是会给实例 2 发送请求。
- ASK命令作用只是让客户端能给新实例发送一次请求,而MOVED命令修改本地缓存,让后续命令发往新实例。
3.切片集群总结
本篇主要讲述了,切片集群在保存大量数据方面的优势,以及基于哈希槽的数据分布机制和客户端定位键值对的方法。
- 在应对数据量大的数据,数据扩容时,虽然增加内存这种纵向扩展的方式简单直接,但是会造成内存过大,导致性能变慢。同事也受到硬件和成本的限制。
- Redis切片集群提供了横向扩展的模式,也就是使用多个实例,并给每个实例分配一定的哈希槽,数据可以通过键的哈希值映射到哈希槽,在通过哈希槽分散分布在不同的实例上。扩展性好,通过增加实例可以存储大量数据。
- 集群是实例的增减和为了实现负载均衡而进行的数据重新分布,导致哈希槽和实例映射关系的变化,客户端请求时,会收到命令执行报错信息。MOVED 和 ASK 命令,让客户端获取最新信息。
- 在 Redis 3.0 之前,Redis 官方并没有提供切片集群方案,但是,其实当时业界已经有了一些切片集群的方案,例如基于客户端分区的 ShardedJedis,基于代理的 Codis、Twemproxy 等。这些方案的应用早于 Redis Cluster 方案
Redis Cluster 方案通过哈希槽的方式把键值对分配到不同的实例上,这个过程需要对键值对的 key 做 CRC 计算,然后再和哈希槽做映射,这样做有什么好处吗?如果用一个表直接把键值对和实例的对应关系记录下来(例如键值对 1 在实例 2 上,键值对 2 在实例 1 上),这样就不用计算 key 和哈希槽的对应关系了,只用查表就行了,Redis 为什么不这么做呢?
1、整个集群存储key的数量是无法预估的,key的数量非常多时,直接记录每个key对应的实例映射关系,这个映射表会非常庞大,这个映射表无论是存储在服务端还是客户端都占用了非常大的内存空间。
2、Redis Cluster采用无中心化的模式(无proxy,客户端与服务端直连),客户端在某个节点访问一个key,如果这个key不在这个节点上,这个节点需要有纠正客户端路由到正确节点的能力(MOVED响应),这就需要节点之间互相交换路由表,每个节点拥有整个集群完整的路由关系。如果存储的都是key与实例的对应关系,节点之间交换信息也会变得非常庞大,消耗过多的网络资源,而且就算交换完成,相当于每个节点都需要额外存储其他节点的路由表,内存占用过大造成资源浪费。
3、当集群在扩容、缩容、数据均衡时,节点之间会发生数据迁移,迁移时需要修改每个key的映射关系,维护成本高。
4、而在中间增加一层哈希槽,可以把数据和节点解耦,key通过Hash计算,只需要关心映射到了哪个哈希槽,然后再通过哈希槽和节点的映射表找到节点,相当于消耗了很少的CPU资源,不但让数据分布更均匀,还可以让这个映射表变得很小,利于客户端和服务端保存,节点之间交换信息时也变得轻量。
5、当集群在扩容、缩容、数据均衡时,节点之间的操作例如数据迁移,都以哈希槽为基本单位进行操作,简化了节点扩容、缩容的难度,便于集群的维护和管理。
请求路由、数据迁移
Redis使用集群www.devze.com方案就是为了解决单个节点数据量大、写入量大产生的性能瓶颈的问题。多个节点组成一个集群,可以提高集群的性能和可靠性,但随之而来的就是集群的管理问题,最核心问题有2个:请求路由、数据迁移(扩容/缩容/数据平衡)。
1、请求路由:一般都是采用哈希槽的映射关系表找到指定节点,然后在这个节点上操作的方案。
Redis Cluster在每个节点记录完整的映射关系(便于纠正客户端的错误路由请求),同时也发给客户端让客户端缓存一份,便于客户端直接找到指定节点,客户端与服务端配合完成数据的路由,这需要业务在使用Redis Cluster时,必须升级为集群版的SDK才支持客户端和服务端的协议交互。
其他Redis集群化方案例如Twemproxy、Codis都是中心化模式(增加Proxy层),客户端通过Proxy对整个集群进行操作,Proxy后面可以挂N多个Redis实例,Proxy层维护了路由的转发逻辑。操作Proxy就像是操作一个普通Redis一样,客户端也不需要更换SDK,而Redis Cluster是把这些路由逻辑做在了SDK中。当然,增加一层Proxy也会带来一定的性能损耗。
2、数据迁移:当集群节点不足以支撑业务需求时,就需要扩容节点,扩容就意味着节点之间的数据需要做迁移,而迁移过程中是否会影响到业务,这也是判定一个集群方案是否成熟的标准。
Twemproxy不支持在线扩容,它只解决了请求路由的问题,扩容时需要停机做数据重新分配。而Redis Cluster和Codis都做到了在线扩容(不影响业务或对业务的影响非常小),重点就是在数据迁移过程中,客户端对于正在迁移的key进行操作时,集群如何处理?还要保证响应正确的结果?
Redis Cluster和Codis都需要服务端和客户端/Proxy层互相配合,迁移过程中,服务端针对正在迁移的key,需要让客户端或Proxy去新节点访问(重定向),这个过程就是为了保证业务在访问这些key时依旧不受影响,而且可以得到正确的结果。由于重定向的存在,所以这个期间的访问延迟会变大。等迁移完成之后,Redis Cluster每个节点会更新路由映射表,同时也会让客户端感知到,更新客户端缓存。Codis会在Proxy层更新路由表,客户端在整个过程中无感知。
除了访问正确的节点之外,数据迁移过程中还需要解决异常情况(迁移超时、迁移失败)、性能问题(如何让数据迁移更快、bigkey如何处理),这个过程中的细节也很多。
Redis Cluster的数据迁移是同步的,迁移一个key会同时阻塞源节点和目标节点,迁移过程中会有性能问题。而Codis提供了异步迁移数据的方案,迁移速度更快,对性能影响最小,当然,实现方案也比较复杂。
二:切片集群方案:Codis \ Redis Cluster
Redis 官方提供的切片集群方案 Redis Cluster。但是Redis Cluster 方案正式发布前,业界已经广泛使用的 Codis值得关注
1.Codis 的整体架构和基本流程
Codis 集群中包含了 4 类关键组件。
- codis server:这是进行了二次开发的 Redis 实例,其中增加了额外的数据结构,支持数据迁移操作,主要负责处理具体的数据读写请求。
- codis proxy:接收客户端请求,并把请求转发给 codis server。
- Zookeeper 集群:保存集群元数据,例如数据位置信息和 codis proxy 信息。
- codis dashboard 和 codis fe:共同组成了集群管理工具。其中,codis dashboard 负责执行集群管理工作,包括增删 codis server、codis proxy 和进行数据迁移。而 codis fe 负责提供 dashboard 的 Web 操作界面,便于我们直接在 Web 界面上进行集群管理。
1.1Codis 是如何处理请求的
首先--》为了让集群能接收并处理请求:先使用 codis dashboard 设置 codis server 和 codis proxy 的访问地址,完成设置后,codis server 和 codis proxy 才会开始接收连接。
其次--》当客户端要读写数据时,客户端直接和 codis proxy 建立连接.codis proxy 本身支持 Redis 的 RESP 交互协议.客户端访问 codis proxy 时,和访问原生的 Redis 实例没有什么区别。原本连接单实例的客户端就可以轻松地和 Codis 集群建立起连接。
最后--》codis proxy 接收到请求,就会查询请求数据和 codis server 的映射关系,并把请求转发给相应的 codis server 进行处理。当 codis server 处理完请求后,会把结果返回给 codis proxy,proxy 再把数据返回给客户端。
2.Codis的关键技术原理
2.1数据如何在集群里分布
在 Codis 集群中,一个数据应该保存在哪个 codis server 上,这是通过逻辑槽(Slot)映射来完成的,具体来说,总共分成两步。
第一步,Codis 集群一共有 1024 个 Slot,编号依次是 0 到 1023。把这些 Slot 手动分配给 codis server,每个 server 上包含一部分 Slot。也可以让 codis dashboard 进行自动分配,例如,dashboard 把 1024 个 Slot 在所有 server 上均分。
第二步,当客户端要读写数据时,会使用 CRC32 算法计算数据 key 的哈希值,并把这个哈希值对 1024 取模。而取模后的值,则对应 Slot 的编号。此时,根据第一步分配的 Slot 和 server 对应关系,可以知道数据保存在哪个 server 上了。
2.2例子
下图显示的就是数据、Slot 和 codis server 的映射保存关系。其中,Slot 0 和 1 被分配到了 server1,Slot 2 分配到 server2,Slot 1022 和 1023 被分配到 server8。当客户端访问 key 1 和 key 2 时,这两个数据的 CRC32 值对 1024 取模后,分别是 1 和 1022。因此,它们会被保存在 Slot 1 和 Slot 1022 上,而 Slot 1 和 Slot 1022 已经被分配到 codis server 1 和 8 上了。key 1 和 key 2 的保存位置就很清楚。
数据 key 和 Slot 的映射关系是客户端在读写数据前直接通过 CRC32 计算得到的,而 Slot 和 codis server 的映射关系是通过分配完成的,所以就需要用一个存储系统保存下来,否则,如果集群有故障了,映射关系就会丢失。
把 Slot 和 codis server 的映射关系称为数据路由表(简称路由表)。我们在 codis dashboard 上分配好路由表后,dashboard 会把路由表发送给 codis proxy,同时,dashboard 也会把路由表保存在 Zookeeper 中。codis-proxy 会把路由表缓存在本地,当它接收到客户端请求后,直接查询本地的路由表,就可以完成正确的请求转发了
在数据分布的实现方法上,Codis 和 Redis Cluster 很相似,都采用了 key 映射到 Slot、Slot 再分配到实例上的机制
2.3Codis 和 Redis Cluster数据分布上的区别
- Codis 中的路由表:通过 codis dashboard 分配和修改的,并被保存在 Zookeeper 集群中。一旦数据位置发生变化(例如有实例增减),路由表被修改了,codis dashbaord 就会把修改后的路由表发送给 codis proxy,proxy 就可以根据最新的路由信息转发请求了。
- 在 Redis Cluster 中,数据路由表是通过每个实例相互间的通信传递的,最后会在每个实例上保存一份。当数据路由信息发生变化时,就需要在所有实例间通过网络消息进行传递。所以,如果实例数量较多的话,就会消耗较多的集群网络资源。
3.集群扩容和数据迁移
Codis 集群扩容包括了两方面:增加 codis server 和增加 codis proxy。
3.1增加codis server
两步操作:
- 启动新的 codis server,将它加入集群;
- 把部分数据迁移到新的 server。
3.1.1数据迁移的基本流程
Codis 集群按照 Slot 的粒度进行数据迁移,数据迁移是一个重要的机制
- 在源 server 上,Codis 从要迁移的 Slot 中随机选择一个数据,发送给目的 server。
- 目的 server 确认收到数据后,会给源 server 返回确认消息。这时,源 server 会在本地将刚才迁移的数据删除。
- 第一步和第二步就是单个数据的迁移过程。Codis 会不断重复这个迁移过程,直到要迁移的 Slot 中的数据全部迁移完成。
Codis 实现了两种迁移模式,分别是同步迁移和异步迁移
3.1.2同步迁移
同步迁移是指,在数据从源 server 发送给目的 server 的过程中,源 server 是阻塞的,无法处理新的请求操作。这种模式很容易实现,但是迁移过程中会涉及多个操作(包括数据在源 server 序列化、网络传输、在目的 server 反序列化,以及在源 server 删除),如果迁移的数据是一个 bigkey,源 server 就会阻塞较长时间,无法及时处理用户请求。
3.1.3异步迁移
为了避免数据迁移阻塞源 server,Codis 实现的第二种迁移模式就是异步迁移
异步迁移的两个关键特点
第一个特点是:
- 当源 server 把数据发送给目的 server 后,就可以处理其他请求操作了,不用等到目的 server 的命令执行完。而目的 server 会在收到数据并反序列化保存到本地后,给源 server 发送一个 ACK 消息,表明迁移完成。此时,源 server 在本地把刚才迁移的数据删除。
- 在这个过程中,迁移的数据会被设置为只读,所以,源 server 上的数据不会被修改,自然也就不会出现“和目的 server 上的数据不一致”的问题了
第二个特点是:
- 于 bigkey,异步迁移采用了拆分指令的方式进行迁移。具体来说就是,对 bigkey 中每个元素,用一条指令进行迁移,而不是把整个 bigkey 进行序列化后再整体传输。这种化整为零的方式,就避免了 bigkey 迁移时,因为要序列化大量数据而阻塞源 server 的问题。
- 当 bigkey 迁移了一部分数据后,如果 Codis 发生故障,就会导致 bigkey 的一部分元素在源 server,而另一部分元素在目的 server,这就破坏了迁移的原子性。所以,Codis 会在目标 server 上,给 bigkey 的元素设置一个临时过期时间。如果迁移过程中发生故障,那么,目标 server 上的 key 会在过期后被删除,不会影响迁移的原子性。当正常完成迁移后,bigkey 元素的临时过期时间会被删除。
第二个特点例子:
- 假如要迁移一个有 1 万个元素的 List 类型数据,当使用异步迁移时,源 server 就会给目的 server 传输 1 万条 RPUSH 命令,每条命令对应了 List 中一个元素的插入。在目的 server 上,这 1 万条命令再被依次执行,就可以完成数据迁移。
- 为了提升迁移的效率,Codis 在异步迁移 Slot 时,允许每次迁移多个 key。可以通过异步迁移命令 SLOTSMGRTTAGSLOT-ASYNC 的参数 numkeys 设置每次迁移的 key 数量
3.2增加codis proxy
Codis 集群中,客户端是和 codis proxy 直接连接的,所以,当客户端增加时,一个 proxy 无法支撑大量的请求操作,就需要增加 proxy。增加 proxy 比较容易,直接启动 proxy,再通过 codis dashboard 把 proxy 加入集群就行。
此时,codis proxy 的访问连接信息都会保存在 Zookeeper 上。所以,当新增了 proxy 后,Zookeeper 上会有最新的访问列表,客户端也就可以从 Zookeeper 上读取 proxy 访问列表,把请求发送给新增的 proxy。这样一来,客户端的访问压力就可以在多个 proxy 上分担处理了,如下图所示
4:客户端能否与集群直接交互
使用 Redis 单实例时,客户端只要符合 RESP 协议,就可以和实例进行交互和读写数据。但是,在使用切片集群时,有些功能是和单实例不一样的,比如集群中的数据迁移操作,在单实例上是没有的,而且迁移过程中,数据访问请求可能要被重定向(例如 Redis Cluster 中的 MOVE 命令)。
客户端需要增加和集群功能相关的命令操作的支持。如果原来使用单实例客户端,想要扩容使用集群,就需要使用新客户端,这对于业务应用的兼容性来说,并不是特别友好。
Codis 集群在设计时,就充分考虑了对现有单实例客户端的兼容性。
Codis 使用 codis proxy 直接和客户端连接,codis proxy 是和单实例客户端兼容的。而和集群相关的管理工作(例如请求转发、数据迁移等),都由 codis proxy、codis dashboard 这些组件来完成,不需要客户端参与。
业务应用使用 Codis 集群时,就不用修改客户端了,可以复用和单实例连接的客户端,既能利用集群读写大容量数据,又避免了修改客户端增加复杂的操作逻辑,保证了业务代码的稳定性和兼容性。
5.怎么保证集群可靠性?
可靠性是实际业务应用的一个核心要求。对于一个分布式系统来说,它的可靠性和系统中的组件个数有关:组件越多,潜在的风险点也就越多。
和 Redis Cluster 只包含 Redis 实例不一样,Codis 集群包含的组件有 4 类
Codis 不同组件的可靠性保证方法。
5.1codis server保证可靠性方法
- codis server 其实就是 Redis 实例,只不过增加了和集群操作相关的命令。Redis 的主从复制机制和哨兵机制在 codis server 上都是可以使用的,所以,Codis 就使用主从集群来保证 codis server 的可靠性。简单来说就是,Codis 给每个 server 配置从库,并使用哨兵机制进行监控,当发生故障时,主从库可以进行切换,从而保证了 server 的可靠性。
- 在这种配置情况下,每个 server 就成为了一个 server group,每个 group 中是一主多从的 server。数据分布使用的 Slot,也是按照 group 的粒度进行分配的。同时,codis proxy 在转发请求时,也是按照数据所在的 Slot 和 group 的对应关系,把写请求发到相应 group 的主库,读请求发到 group 中的主库或从库上。
下图展示的是配置了 server group 的 Codis 集群架构。在 Codis 集群中,我们通过部署 server group 和哨兵集群,实现 codis server 的主从切换,提升集群可靠性。
5.2codis proxy 和 Zookeeper可靠性
- 在 Codis 集群设计时,proxy 上的信息源头都是来自 Zookeeper(例如路由表)。而 Zookeeper 集群使用多个实例来保存数据,只要有超过半数的 Zookeeper 实例可以正常工作, Zookeeper 集群就可以提供服务,也可以保证这些数据的可靠性。
- 所以,codis proxy 使用 Zookeeper 集群保存路由表,可以充分利用 Zookeeper 的高可靠性保证来确保 codis proxy 的可靠性,不用再做额外的工作了。当 codis proxy 发生故障后,直接重启 proxy 就行。重启后的 proxy,可以通过 codis dashboard 从 Zookeeper 集群上获取路由表,然后,就可以接收客户端请求进行转发了。这样的设计,也降低了 Codis 集群本身的开发复杂度。
5.3codis dashboard 和 codis fe可靠性
它们主要提供配置管理和管理员手工操作,负载压力不大,所以,它们的可靠性可以不用额外进行保证了
6.切片集群方案选择建议
6.1Codis 和 Redis Cluster 区别
6.2实际应用中的两种方案
- 从稳定性和成熟度来看,Codis 应用得比较早,在业界已经有了成熟的生产部署。虽然 Codis 引入了 proxy 和 Zookeeper,增加了集群复杂度,但是,proxy 的无状态设计和 Zookeeper 自身的稳定性,也给 Codis 的稳定使用提供了保证。而 Redis Cluster 的推出时间晚于 Codis,相对来说,成熟度要弱于 Codis,如果你想选择一个成熟稳定的方案,Codis 更加合适些。
- 从业务应用客户端兼容性来看,连接单实例的客户www.devze.com端可以直接连接 codis proxy,而原本连接单实例的客户端要想连接 Redis Cluster 的话,就需要开发新功能。所以,如果你的业务应用中大量使用了单实例的客户端,而现在想应用切片集群的话,建议你选择 Codis,这样可以避免修改业务应用中的客户端。
- 从使用 Redis 新命令和新特性来看,Codis server 是基于开源的 Redis 3.2.8 开发的,所以,Codis 并不支持 Redis 后续的开源版本中的新增命令和数据类型。另外,Codis 并没有实现开源 Redis 版本的所有命令,比如 BITOP、BLPOP、BRPOP,以及和与事务相关的 MUTLI、EXEC 等命令。Codis 官网上列出了不被支持的命令列表,你在使用时记得去核查一下。所以,如果你想使用开源 Redis 版本的新特性,Redis Cluster 是一个合适的选择。
- 从数据迁移性能维度来看,Codis 能支持异步迁移,异步迁移对集群处理正常请求的性能影响要比使用同步迁移的小。所以,如果你在应用集群时,数据迁移比较频繁的话,Codis 是个更合适的选择。
7.Codis 和 Redis Cluster总结
Codis 集群包含 codis server、codis proxy、Zookeeper、codis dashboard 和 codis fe 这四大类组件。
- codis proxy 和 codis server 负责处理数据读写请求,其中,codis proxy 和客户端连接,接收请求,并转发请求给 codis server,而 codis server 负责具体处理请求。
- codis dashboard 和 codis fe 负责集群管理,其中,codis dashboard 执行管理操作,而 codis fe 提供 Web 管理界面。
- Zookeeper 集群负责保存集群的所有元数据信息,包括路由表、proxy 实例信息等。这里,有个地方需要你注意,除了使用 Zookeeper,Codis 还可以使用 etcd 或本地文件系统保存元数据信息。
Codis 使用上的小建议:当你有多条业务线要使用 Codis 时,可以启动多个 codis dashboard,每个 dashboard 管理一部分 codis server,同时,再用一个 dashboard 对应负责一个业务线的集群管理,这样,就可以做到用一个 Codis 集群实现多条业务线的隔离管理了。
假设 Codis 集群中保存的 80% 的键值对都是 Hash 类型,每个 Hash 集合的元素数量在 10 万~20 万个,每个集合元素的大小是 2KB。你觉得,迁移一个这样的 Hash 集合数据,会对 Codis 的性能造成影响吗?
Codis 在迁移数据时,设计的方案可以保证迁移性能不受影响。
- 1、异步迁移:源节点把迁移的数据发送给目标节点后就返回,之后接着处理客户端请求,这个阶段不会长时间阻塞源节点。目标节点加载迁移的数据成功后,向源节点发送 ACK 命令,告知其迁移成功。
- 2、源节点异步释放 key:源节点收到目标节点 ACK 后,在源实例删除这个 key,释放 key 内存的操作,会放到后台线程中执行,不会阻塞源实例。(没错,Codis 比 Redis 更早地支持了 lazy-free,只不过只用在了数据迁移中)。
- 3、小对象序列化传输:小对象依旧采用序列化方式迁移,节省网络流量。
- 4、bigkey 分批迁移:bigkey 拆分成一条条命令,打包分批迁移(利用了 Pipeline 的优势),提升迁移速度。
- 5、一次迁移多个 key:一次发送多个 key 进行迁移,提升迁移效率。
- 6、迁移流量控制:迁移时会控制缓冲区大小,避免占满网络带宽。
- 7、bigkey 迁移原子性保证(兼容迁移失败情况):迁移前先发一个 DEL 命令到目标节点(重试可保证幂等性),然后把 bigkey 拆分成一条条命令,并设置一个临时过期时间(防止迁移失败在目标节点遗留垃圾数据),迁移成功后在目标节点设置真实的过期时间。 Codis 在数据迁移方面要比 Redis Cluster 做得更优秀,而且 Codis 还带了一个非常友好的运维界面,方便 DBA 执行增删节点、主从切换、数据迁移等操作。
三.通信开销:限制Redis Cluster规模的关键因素
1:为什么要限定集群规模呢?
Redis Cluster 能保存的数据量以及支撑的吞吐量,跟集群的实例规模密切相关。Redis 官方给出了 Redis Cluster 的规模上限,就是一个集群运行 1000 个实例
这里的一个关键因素就是,实例间的通信开销会随着实例规模增加而增大,在集群超过一定规模时(比如 800 节点),集群吞吐量反而会下降。所以,集群的实际规模会受到限制。
2:实例通信方法和对集群规模的影响
Redis Cluster 在运行时,每个实例上都会保存 Slot 和实例的对应关系(也就是 Slot 映射表),以及自身的状态信息。
为了让集群中的每个实例都知道其它所有实例的状态信息,实例之间会按照一定的规则进行通信。这个规则就是 Gossip 协议
2.1Gossip 协议
Gossip 协议的工作原理可以概括成两点。:检测实例时候在线\给发送PING命令实例返回PONG消息
- 一是,每个实例之间会按照一定的频率,从集群中随机挑选一些实例,把 PING 消息发送给挑选出来的实例,用来检测这些实例是否在线,并交换彼此的状态信息。PING 消息中封装了发送消息的实例自身的状态信息、部分其它实例的状态信息,以及 Slot 映射表。
- 二是,一个实例在接收到 PING 消息后,会给发送 PING 消息的实例,发送一个 PONG 消息。PONG 消息包含的内容和 PING 消息一样。
Gossip 协议可以保证在一段时间后,集群中的每一个实例都能获得其它所有实例的状态信息。
这样一来,即使有新节点加入、节点故障、Slot 变更等事件发生,实例间也可以通过 PING、PONG 消息的传递,完成集群状态在每个实例上的同步
3.通信的影响
实例间使用 Gossip 协议进行通信时,通信开销受到通信消息大小和通信频率这两方面的影响。消息越大、频率越高,相应的通信开销也就越大。如果想要实现高效的通信,可以从这两方面入手去调优
3.1Gossip 消息大小
Redis 实例发送的 PING 消息的消息体是由 clusterMsgDataGossip 结构体组成的,这个结构体的定义如下所示:
typedef struct { char nodename[CLUSTER_NAMELEN]; //40字节 uint32_t ping_sent; //4字节 uint32_t pong_received; //4字节 char ip[NET_IP_STR_LEN]; //46字节 uint16_t port; //2字节 uint16_t cport; //2字节 uint16_t flags; //2字节 uint32_t notused1; //4字节 } clusterMsgDataGossip;
其中,CLUSTER_NAMELEN 和 NET_IP_STR_LEN 的值分别是 40 和 46,分别表示,nodename 和 ip 这两个字节数组的长度是 40 字节和 46 字节,我们再把结构体中其它信息的大小加起来,就可以得到一个 Gossip 消息的大小了,即 104 字节。
每个实例在发送一个 Gossip 消息时,除了会传递自身的状态信息,默认还会传递集群十分之一实例的状态信息。
例子:
所以,对于一个包含了 1000 个实例的集群来说,每个实例发送一个 PING 消息时,会包含 100 个实例的状态信息,总的数据量是 10400 字节,再加上发送实例自身的信息,一个 Gossip 消息大约是 10KB。为了让 Slot 映射表能够在不同实例间传播,PING 消息中还带有一个长度为 16,384 bit 的 Bitmap,这个 Bitmap 的每一位对应了一个 Slot,如果某一位为 1,就表示这个 Slot 属于当前实例。这个 Bitmap 大小换算成字节后,是 2KB实例状态信息和 Slot 分配信息相加,就可以得到一个 PING 消息的大小了,大约是 12KB。
PONG 消息和 PING 消息的内容一样,它的大小大约是 12KB。每个实例发送了 PING 消息后,还会收到返回的 PONG 消息,两个消息加起来有 24KB。
从绝对值上来看,24KB 并不算很大,但是,如果实例正常处理的单个请求只有几 KB 的话,那么,实例为了维护集群状态一致传输的 PING/PONG 消息,就要比单个业务请求大了。而且,每个实例都会给其它实例发送 PING/PONG 消息。随着集群规模增加,这些心跳消息的数量也会越多,会占据一部分集群的网络通信带宽,进而会降低集群服务正常客户端请求的吞吐量。
3.2实例间通信频率
Redis Cluster 的实例启动后,默认会每秒从本地的实例列表中随机选出 5 个实例,再从这 5 个实例中找出一个最久没有通信的实例,把 PING 消息发送给该实例。这是实例周期性发送 PING 消息的基本做法
这里有一个问题:实例选出来的这个最久没有通信的实例,毕竟是从随机选出的 5 个实例中挑选的,这并不能保证这个实例就一定是整个集群中最久没有通信的实例。可能会出现,有些实例一直没有被发送 PING 消息,导致它们维护的集群状态已经过期。
为了避免这种情况,Redis Cluster 的实例会按照每 100ms 一次的频率,扫描本地的实例列表,如果发现有实例最近一次接收 PONG 消息的时间,已经大于配置项 cluster-node-timeout 的一半了(cluster-node-timeout/2),就会立刻给该实例发送 PING 消息,更新这个实例上的集群状态信息
当集群规模扩大之后,因为网络拥塞或是不同服务器间的流量竞争,会导致实例间的网络通信延迟增加。如果有部分实例无法收到其它实例发送的 PONG 消息,就会引起实例之间频繁地发送 PING 消息,这又会对集群网php络通信带来额外的开销
总结下单实例每秒会发送的 PING 消息数量,如下所示:
PING 消息发送数量 = 1 + 10 * 实例数(最近一次接收 PONG 消息的时间超出 cluster-node-timeout/2)
其中,1 是指单实例常规按照每 1 秒发送一个 PING 消息,10 是指每 1 秒内实例会执行 10 次检查,每次检查后会给 PONG 消息超时的实例发送消息。
例子:
假设单个实例检测发现,每 100 毫秒有 10 个实例的 PONG 消息接收超时,那么,这个实例每秒就会发送 101 个 PING 消息,约占 1.2MB/s 带宽。如果集群中有 30 个实例按照这种频率发送消息,就会占用 36MB/s 带宽,这就会挤占集群中用于服务正常请求的带宽
4.如何降低实例间的通信开销?
4.1减小实例传输的消息大小
为了降低实例间的通信开销,从原理上说,可以减小实例传输的消息大小(PING/PONG 消息、Slot 分配信息),但是,因为集群实例依赖 PING、PONG 消息和 Slot 分配信息,来维持集群状态的统一,一旦减小了传递的消息大小,就会导致实例间的通信信息减少,不利于集群维护,所以,减小实例传输的消息大小不能采用这种方式。
4.2降低实例间发送消息的频率:
实例间发送消息的频率有两个。
- 每个实例每 1 秒发送一条 PING 消息。这个频率不算高,如果再降低该频率的话,集群中各实例的状态可能就没办法及时传播了。
- 每个实例每 100 毫秒会做一次检测,给 PONG 消息接收超过 cluster-node-timeout/2 的节点发送 PING 消息。实例按照每 100 毫秒进行检测的频率,是 Redis 实例默认的周期性检查www.devze.com任务的统一频率,我们一般不需要修改它。
就只有 cluster-node-timeout 这个配置项可以修改
配置项 cluster-node-timeout 定义了集群实例被判断为故障的心跳超时时间,默认是 15 秒。如果 cluster-node-timeout 值比较小,那么,在大规模集群中,就会比较频繁地出现 PONG 消息接收超时的情况,从而导致实例每秒要执行 10 次“给 PONG 消息超时的实例发送 PING 消息”这个操作
所以,为了避免过多的心跳消息挤占集群带宽,可以调大 cluster-node-timeout 值,比如说调大到 20 秒或 25 秒。这样一来, PONG 消息接收超时的情况就会有所缓解,单实例也不用频繁地每秒执行 10 次心跳发送操作了。
也不要把 cluster-node-timeout 调得太大,否则,如果实例真的发生了故障,需要等待 cluster-node-timeout 时长后,才能检测出这个故障,这又会导致实际的故障恢复时间被延长,会影响到集群服务的正常使用
为了验证调整 cluster-node-timeout 值后,是否能减少心跳消息占用的集群网络带宽,建议:可以在调整 cluster-node-timeout 值的前后,使用 tcpdump 命令抓取实例发送心跳信息网络包的情况。
执行下面的命令后,可以抓取到 192.168.10.3 机器上的实例从 16379 端口发送的心跳网络包,并把网络包的内容保存到 r1.cap 文件中:
tcpdump host 192.168.10.3 port 16379 -i 网卡名 -w /tmp/r1.cap
通过分析网络包的数量和大小,就可以判断调整 cluster-node-timeout 值前后,心跳消息占用的带宽情况了。
5.通信开销总结
Redis Cluster 实例间以 Gossip 协议进行通信的机制。Redis Cluster 运行时,各实例间需要通过 PING、PONG 消息进行信息交换,这些心跳消息包含了当前实例和部分其它实例的状态信息,以及 Slot 分配信息。这种通信机制有助于 Redis Cluster 中的所有实例都拥有完整的集群状态信息。
但是,随着集群规模的增加,实例间的通信量也会增加。如果盲目地对 Redis Cluster 进行扩容,就可能会遇到集群性能变慢的情况。这是因为,集群中大规模的实例间心跳消息会挤占集群处理正常请求的带宽。而且,有些实例可能因为网络拥塞导致无法及时收到 PONG 消息,每个实例在运行时会周期性地(每秒 10 次)检测是否有这种情况发生,一旦发生,就会立即给这些 PONG 消息超时的实例发送心跳消息。
集群规模越大,网络拥塞的概率就越高,相应的,PONG 消息超时的发生概率就越高,这就会导致集群中有大量的心跳消息,影响集群服务正常请求。可以通过调整 cluster-node-timeout 配置项减少心跳消息的占用带宽情况,但是,在实际应用中,如果不是特别需要大容量集群,建议把 Redis Cluster 的规模控制在 400~500 个实例。
设单个实例每秒能支撑 8 万请求操作(8 万 QPS),每个主实例配置 1 个从实例,那么,400~ 500 个实例可支持 1600 万~2000 万 QPS(200/250 个主实例 *8 万 QPS=1600/2000 万 QPS),这个吞吐量性能可以满足不少业务应用的需求
如果我们采用跟 Codis 保存 Slot 分配信息相类似的方法,把集群实例状态信息和 Slot 分配信息保存在第三方的存储系统上(例如 Zookeeper),这种方法会对集群规模产生什么影响吗?
答案:假设我们将 Zookeeper 作为第三方存储系统,保存集群实例状态信息和 Slot 分配信息,那么,实例只需要和 Zookeeper 通信交互信息,实例之间就不需要发送大量的心跳消息来同步集群状态了。这种做法可以减少实例之间用于心跳的网络通信量,有助于实现大规模集群。
而且,网络带宽可以集中用在服务客户端请求上。不过,在这种情况下,实例获取或更新集群状态信息时,都需要和 Zookeeper 交互,Zookeeper 的网络通信带宽需求会增加。所以,采用这种方法的时候,需要给 Zookeeper 保证一定的网络带宽,避免 Zookeeper 受限于带宽而无法和实例快速通信。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论