02-Redis 部署模式03集群(重要)

官网保平安:https://redis.io/

Sentinel Cluster(>=3.0版本)

Redis 是单线程的,如何提高多核 CPU 的利用率?

可以在同一个服务器部署多个 Redis 的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的,所以如果你想使用多个 CPU,你可以考虑一下分片(shard)。也就是集群。

分布式 Redis 是前期做还是后期规模上来了再做好?为什么

既然 Redis 是如此的轻量(单实例只使用 1M 内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让 Redis 以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个 Redis 实例,例如 32 或者 64 个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的 Redis 服务器时,你需要做的就是仅仅将 Redis 实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的 Redis 实例从第一台机器迁移到第二台机器。

为什么需要 Redis Cluster?

高并发场景下,使用 Redis 主要会遇到的两个问题:

  1. 缓存的数据量太大: 实际缓存的数据量可以达到几十 G,甚至是成百上千 G;
  2. 并发量要求太大: 虽然 Redis 号称单机可以支持 10w 并发,但实际项目中,不可靠因素太多,就比如一些复杂的写/读操作就可能会让这个并发量大打折扣。而且,就算真的可以实际支持 10w 并发,达到瓶颈了,可能也没办法满足系统的实际需求。

主从复制 和 哨兵 这两种方案本质都是通过增加 master 的副本数量的方式来提高 Redis 服务的整体可用性和读吞吐量,都不支持横向扩展来缓解写压力以及解决缓存数据量过大的问题。

img

对于这两种方案来说,如果写压力太大或者缓存数据量太大的话,我们可以考虑提高服务器硬件的配置。不过,提高硬件配置成本太高,无法动态扩容缩容,局限性太大。从本质上来说,靠堆硬件配置的方式并没有实质性地解决问题,依然无法满足高并发场景下分布式缓存的要求。

通常情况下,更建议使用 Redis 切片集群 这种方案。

简单来说,Redis 切片集群 就是部署多台 Redis 主节点(master),这些节点之间平等,并没有主从之说,同时对外提供读/写服务。缓存的数据库相对均匀地分布在这些 Redis 实例上,客户端的请求通过路由规则转发到目标 master 上。

img

Redis 切片集群对于横向扩展非常友好,只需要增加 Redis 节点到集群中即可。

在 Redis 3.0 之前,我们通常使用的是 TwemproxyCodis 这类开源分片集群方案。Twemproxy、Codis 就相当于是上面的 Proxy 层,负责维护路由规则,实现负载均衡。

不过,Twemproxy、Codis 虽然未被淘汰,但官方已经没有继续维护了。

img

到了 Redis 3.0 的时候,Redis 官方推出了分片集群解决方案 Redis Cluster 。经过多个版本的持续完善,Redis Cluster 成为 Redis 切片集群的首选方案,满足绝大部分高并发业务场景需求。

Redis Cluster 通过 分片(Sharding) 来进行数据管理,提供 主从复制(Master-Slave Replication)故障转移(Failover) 等开箱即用的功能,可以非常方便地帮助我们解决 Redis 大数据量缓存以及 Redis 服务高可用的问题。

Redis Cluster 这种方案可以很方便地进行 横向拓展(Scale Out),内置了开箱即用的解决方案。当 Redis Cluster 的处理能力达到瓶颈无法满足系统要求的时候,直接动态添加 Redis 节点到集群中即可。根据官方文档中的介绍,Redis Cluster 支持扩展到 1000 个节点。反之,当 Redis Cluster 的处理能力远远满足系统要求,同样可以动态删除集群中 Redis 节点,节省资源。

img

可以说,Redis Cluster 的动态扩容和缩容是其最大的优势

虽说 Redis Cluster 可以扩展到 1000 个节点,但强烈不推荐这样做,应尽量避免集群中的节点过多。这是因为 Redis Cluster 中的各个节点基于 Gossip 协议 来进行通信共享信息,当节点过多时,Gossip 协议的效率会显著下降,通信成本剧增。

特点:

  • 无中心架构(不存在哪个节点影响性能瓶颈),没有 proxy 层。
  • 可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
  • 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  • 高可用性,部分节点不可用时,集群仍可用。通过增加 slave 做备份数据副本。
  • 实现故障自动 failover(内置了 Sentinel 机制,无需单独部署 Sentinel 集群),节点之间通过 gossip 协议交换状态信息,用投票机制完成 slave 到 master 的角色提升。

有什么缺点

  • 不能使用涉及多个 key 的操作。

    • 不能对两个集合求交集,因为他们可能被存储到不同的 Redis 实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
    • 不能同时操作多个 key,如使用 mset、mget 目前只支持具有相同 slot 值的 Key 执行批量操作。
      • 对于映射为不同 slot 值的 Key 由于 Keys 不支持跨 slot 查询,所以执行 mset、mget、sunion 等操作支持不友好。
    • Key 事务操作支持有限,只支持多 key 在同一节点上的事务操作,当多个 Key 分布于不同的节点上时无法使用事务功能。
    • Key 作为数据分区的最小粒度,不能将一个很大的键值对象如 hash、list 等映射到不同的节点。
  • 分区使用的粒度是 key,不能使用一个非常长的排序 key 存储一个数据集。

  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的 Redis 实例和主机同时收集 RDB/AOF 文件。

  • 分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除 Redis 节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。有一种预分片的技术也可以较好的解决这个问题。

  • Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。目前仅 JedisCluster 相对成熟,异常处理部分还不完善,比如常见的“max redirect exception”。

  • 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。

  • 数据通过异步复制,不保证数据的强一致性。

  • 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。

  • slave 在集群中充当“冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 slave 资源的利用率。

  • 不支持多数据库空间,单机下的 Redis 可以支持到 16 个数据库,集群模式下只能使用1个数据库空间,即db 0。

  • 复制结构只支持一层,slave 只能复制 master,不支持嵌套树状复制结构。

  • 避免产生 hot-key,导致主库节点成为系统的短板。

  • 避免产生 big-key,导致网卡撑爆、慢查询等。

  • 重试时间应该大于 cluster-node-time 时间。

  • Redis Cluster 不建议使用 pipeline 和 multi-keys 操作,减少 max redirect 产生的场景。

一个最基本的 Redis Cluster 架构是怎样的?

为了保证高可用,Redis Cluster 至少需要 6 个节点以上(3 主 3 从)。其中 master 提供读写操作,slave 作为备用节点,不提供请求,只作为故障转移使用。

  • 如果 master 只有一个 slave 的话,master 宕机之后就直接使用这个 slave 替代 master 继续提供服务。
    img

  • 如果 master 有多个 slave 的话,Redis Cluster 中的其他节点会从这个 master 的所有 slave 中选出一个替代 master 继续提供服务。Redis Cluster 总是希望数据最完整的 slave 被提升为新的 master。

Redis Cluster 至少要保证宕机的 master 有一个 slave 可用。

如果宕机的 master 无 slave 的话,为了保障集群的完整性,保证所有的哈希槽都指派给了可用的 master,整个集群将不可用。这种情况下,还是想让集群保持可用的话,可以将 cluster-require-full-coverage 这个参数设置成 no,cluster-require-full-coverage 表示需要 16384 个 slot 都正常被分配的时候 Redis Cluster 才可以对外提供服务。

方案说明

  1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽区间的数据,默认分配了 16384 个槽位。
  2. 每份数据分片会存储在多个互为主从的多节点上。
  3. 数据写入先写 master,再同步到 slave(支持配置为阻塞同步)。
  4. 同一分片多个节点间的数据不保持一致性。
  5. 读取数据时,当客户端操作的 key 没有分配在该节点上时,Redis 会返回转向指令,指向正确的节点。
  6. 扩容时需要把旧节点的数据迁移一部分到新节点。

在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是加 1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,采用 gossip 协议,用来进行故障检测、配置更新、故障转移授权。

Redis Cluster 是如何分片的?

类似的问题:

  • Redis Cluster 中的数据是如何分布的?
  • 如何确定给定 key 的应该分布到哪个哈希槽中?

Redis Cluster 采用的是 哈希槽分区,每一个键值对都属于一个 hash slot(哈希槽)。Redis Cluster 通常有 16384 个哈希槽。

当需要在 Redis 集群中放置一个 key-value 时,计算 CRC16(key) mod 16384 的值,到的值即是 key 对应的哈希槽,然后将 key 放到对应的桶中。

  • 创建并初始化 Redis Cluster 的时候,Redis 会自动平均分配这 16384 个哈希槽到各个节点,不需要我们手动分配。
  • 如果想自己调整的话,Redis Cluster 也内置了相关的命令比如 ADDSLOTS、ADDSLOTSRANGE

假设集群有 3 个 Redis 节点组成,每个节点负责整个集群的一部分数据,哈希槽可能是这样分配的(这里只是演示,实际效果可能会有差异):

  • Node1:【0 - 5500】 的 hash slots
  • Node2:【5501 - 11000】 的 hash slots
  • Node3:【11001 - 16383】 的 hash slots

在任意一个 master 节点上执行 CLUSTER SLOTS 命令即可返回哈希槽和节点的映射关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
127.0.0.1:7000>> CLUSTER SLOTS
# 哈希槽的范围
1) 1) (integer) 0
2) (integer) 5500
# master 的 ip 和端口号
3) 1) "127.0.0.1"
2) (integer) 7002
# slave 的 ip 和端口号
4) 1) "127.0.0.1"
2) (integer) 8002
2) 1) (integer) 11001
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7000
4) 1) "127.0.0.1"
2) (integer) 8000
3) 1) (integer) 5501
2) (integer) 11000
3) 1) "127.0.0.1"
2) (integer) 7001
4) 1) "127.0.0.1"
2) (integer) 8001

客户端连接 Redis Cluster 中任意一个 master 节点即可访问 Redis Cluster 的数据,当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标节点。

img

如果哈希槽确实是当前节点负责,那就直接响应客户端的请求返回结果,如果不由当前节点负责,就会返回 -MOVED 重定向错误,告知客户端当前哈希槽是由哪个节点负责,客户端向目标节点发送请求并更新缓存的哈希槽分配信息。

img

这个时候你可能就会疑问:为什么还会存在找错节点的情况呢?根据公式计算难道还会出错?

这是因为 Redis Cluster 内部可能会重新分配哈希槽比如扩容缩容的时候(后文中有介绍到 Redis Cluster 的扩容和缩容问题),这就可能会导致客户端缓存的哈希槽分配信息会有误。

从上面的介绍中,我们可以简单总结出 Redis Cluster 哈希槽分区机制的优点:解耦了数据和节点之间的关系,提升了集群的横向扩展性和容错性。

为什么 Redis Cluster 的哈希槽是 16384 个?

CRC16 算法产生的校验码有 16 位,理论上可以产生 65536(2^16,0 ~ 65535)个值。为什么 Redis Cluster 的哈希槽选择的是 16384(2^14)个呢?

2015 年的时候,在 Redis 项目的 issues 区,已经有人提了类似的问题,地址:https://github.com/redis/redis/issues/2576。Redis 作者 antirez 巨佬本人专门对这个问题进行了回复。

image-20221017215237019

antirez 认为哈希槽是 16384(2 的 14 次方) 个的原因是:

  • 正常的心跳包会携带一个节点的完整配置,它会以幂等的方式更新旧的配置,这意味着心跳包会附带当前节点的负责的哈希槽的信息。假设哈希槽采用 16384 ,则占空间 2k(16384/8)。假设哈希槽采用 65536,则占空间 8k(65536/8),这是令人难以接受的内存占用。
  • 由于其他设计上的权衡,Redis Cluster 不太可能扩展到超过 1000 个主节点。

也就是说,65536 个固然可以确保每个主节点有足够的哈希槽,但其占用的空间太大。而且,Redis Cluster 的主节点通常不会扩展太多,16384 个哈希槽完全足够用了。

cluster.h 文件中定义了消息结构 clusterMsg(源码地址:https://github.com/redis/redis/blob/7.0/src/cluster.h)

1
2
3
4
5
6
7
8
9
10
typedef struct {
// 省略部分字段
// ......
// 本节点负责的哈希槽信息,16384/8 个 char 数组,一共为16384bit
unsigned char myslots[CLUSTER_SLOTS/8];
// 集群的状态
unsigned char state;
// 消息的内容
union clusterMsgData data;
} clusterMsg;

myslots 字段用于存储哈希槽信息, 属于无符号类型的 char 数组,数组长度为 16384/8 = 2048。C 语言中的 char 只占用一个字节,而 Java 语言中 char 占用两个字节,小伙伴们不要搞混了。

这里实际就是通过 bitmap 这种数据结构维护的哈希槽信息,每一个 bit 代表一个哈希槽,每个 bit 只能存储 0/1 。如果该位为 1,表示这个哈希槽是属于这个节点。

img

消息传输过程中,会对 myslots 进行压缩,bitmap 的填充率越低,压缩率越高。bitmap 的填充率的值是 哈希槽总数/节点数 ,如果哈希槽总数太大的话,bitmap 的填充率的值也会比较大。

最后,总结一下 Redis Cluster 的哈希槽的数量选择 16384 而不是 65536 的主要原因:

  • 哈希槽太大会导致心跳包太大,消耗太多带宽;
  • 哈希槽总数越少,对存储哈希槽信息的 bitmap 压缩效果越好;
  • Redis Cluster 的主节点通常不会扩展太多,16384 个哈希槽已经足够用了。

Redis Cluster 如何重新分配哈希槽?

如果你想自己手动调整的话,Redis Cluster 也内置了相关的命令:

  • CLUSTER ADDSLOTS slot [slot …] : 把一组 hash slots 分配给接收命令的节点,时间复杂度为 O(N),其中 N 是 hash slot 的总数;
  • CLUSTER ADDSLOTSRANGE start-slot end-slot [start-slot end-slot …] (Redis 7.0 后新加的命令): 把指定范围的 hash slots 分配给接收命令的节点,类似于 ADDSLOTS 命令,时间复杂度为 O(N) 其中 N 是起始 hash slot 和结束 hash slot 之间的 hash slot 的总数。
  • CLUSTER DELSLOTS slot [slot …] : 从接收命令的节点中删除一组 hash slots;
  • CLUSTER FLUSHSLOTS :移除接受命令的节点中的所有 hash slot;
  • CLUSTER SETSLOT slot MIGRATING node-id: 迁移接受命令的节点的指定 hash slot 到目标节点(node_id 指定)中;
  • CLUSTER SETSLOT slot IMPORTING node-id: 将目标节点(node_id 指定)中的指定 hash slot 迁移到接受命令的节点中;
  • ……

简单演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
# 将 slot 1 2 3 4 5 分配给节点
> CLUSTER ADDSLOTS 1 2 3 4 5
OK
# 可以使用 ADDSLOTSRANGE 命令完成一样的效果
> CLUSTER ADDSLOTSRANGE 1 5
OK
# 从接收命令的节点中删除 hash slot 1000 1001 1002
> CLUSTER DELSLOTS 1000 1001 1002
# 迁移接受命令的节点的 hash slot 1005 到 node_id(一长串字符串)对应的节点中
> CLUSTER SETSLOT 1005 MIGRATING node_id(一长串字符串)
# 将node_id(一长串字符串)对应的节点中的 hash slot 1005 迁移到接受命令的节点中
> CLUSTER SETSLOT 1005 IMPORTING 92fd7c2a7b7b8933d1019e72a852f621f6b4faff

Redis Cluster 中的节点是怎么进行通信的?

Redis Cluster 是一个典型的分布式系统,分布式系统中的各个节点需要互相通信。既然要相互通信就要遵循一致的通信协议,

Redis Cluster 采用 Gossip 协议,每个 Redis 节点都维护了一份集群的状态信息。不同的节点如果出现了元数据的变更之后不断地将元数据发送给其他节点让其他节点进行数据变更。

Redis Cluster 的节点之间会相互发送多种 Gossip 消息:

  • MEET:在 Redis Cluster 中的某个 Redis 节点上执行 CLUSTER MEET ip port 命令,可以向指定的 Redis 节点发送一条 MEET 信息,用于将其添加进 Redis Cluster 成为新的 Redis 节点。
  • PING/PONG:Redis Cluster 中的节点都会定时地向其他节点发送 PING 消息,来交换各个节点状态信息,检查各个节点状态,包括在线状态、疑似下线状态 PFAIL 和已下线状态 FAIL。
  • FAIL:Redis Cluster 中的节点 A 发现 B 节点 PFAIL ,并且在下线报告的有效期限内集群中半数以上的节点将 B 节点标记为 PFAIL,节点 A 就会向集群广播一条 FAIL 消息,通知其他节点将故障节点 B 标记为 FAIL 。

优缺点

  • 好处:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
  • 缺点:元数据更新有延时,可能导致集群的一些操作会有一些滞后。

以下为扩展

cluster.h 文件中定义了所有的消息类型(源码地址:https://github.com/redis/redis/blob/7.0/src/cluster.h)。Redis 3.0 版本的时候只有 9 种消息类型,到了 7.0 版本的时候已经有 11 种消息类型了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 注意,PING 、 PONG 和 MEET 实际上是同一种消息。
// PONG 是对 PING 的回复,它的实际格式也为 PING 消息,
// 而 MEET 则是一种特殊的 PING 消息,用于强制消息的接收者将消息的发送者添加到集群中(如果节点尚未在节点列表中的话)
#define CLUSTERMSG_TYPE_PING 0 /* Ping 消息 */
#define CLUSTERMSG_TYPE_PONG 1 /* Pong 用于回复Ping */
#define CLUSTERMSG_TYPE_MEET 2 /* Meet 请求将某个节点添加到集群中 */
#define CLUSTERMSG_TYPE_FAIL 3 /* Fail 将某个节点标记为 FAIL */
#define CLUSTERMSG_TYPE_PUBLISH 4 /* 通过发布与订阅功能广播消息 */
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 /* 请求进行故障转移操作,要求消息的接收者通过投票来支持消息的发送者 */
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6 /* 消息的接收者同意向消息的发送者投票 */
#define CLUSTERMSG_TYPE_UPDATE 7 /* slots 已经发生变化,消息发送者要求消息接收者进行相应的更新 */
#define CLUSTERMSG_TYPE_MFSTART 8 /* 为了进行手动故障转移,暂停各个客户端 */
#define CLUSTERMSG_TYPE_MODULE 9 /* 模块集群API消息 */
#define CLUSTERMSG_TYPE_PUBLISHSHARD 10 /* 通过发布与订阅功能广播分片消息 */
#define CLUSTERMSG_TYPE_COUNT 11 /* 消息总数 */

cluster.h 文件中定义了消息结构 clusterMsg(源码地址:https://github.com/redis/redis/blob/7.0/src/cluster.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct {
char sig[4]; /* 标志位,"RCmb" (Redis Cluster message bus). */
uint32_t totlen; /* 消息总长度 */
uint16_t ver; /* 消息协议版本 */
uint16_t port; /* 端口 */
uint16_t type; /* 消息类型 */
char sender[CLUSTER_NAMELEN]; /* 消息发送节点的名字(ID) */
// 本节点负责的哈希槽信息,16384/8 个 char 数组,一共为16384bit
unsigned char myslots[CLUSTER_SLOTS/8];
// 如果消息发送者是一个从节点,那么这里记录的是消息发送者正在复制的主节点的名字
// 如果消息发送者是一个主节点,那么这里记录的是 REDIS_NODE_NULL_NAME
// (一个 40 字节长,值全为 0 的字节数组)
char slaveof[CLUSTER_NAMELEN];
// 省略部分属性
// ......
// 集群的状态
unsigned char state;
// 消息的内容
union clusterMsgData data;
} clusterMsg;

clusterMsgData 是一个联合体(union),可以为 PING,MEET,PONG 、FAIL 等消息类型。当消息为 PING、MEET 和 PONG 类型时,都是 ping 字段是被赋值的,这也就解释了为什么我们上面说 PING 、 PONG 和 MEET 实际上是同一种消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
union clusterMsgData {
/* PING, MEET and PONG */
struct {
/* Array of N clusterMsgDataGossip structures */
clusterMsgDataGossip gossip[1];
} ping;

/* FAIL */
struct {
clusterMsgDataFail about;
} fail;

/* PUBLISH */
struct {
clusterMsgDataPublish msg;
} publish;

/* UPDATE */
struct {
clusterMsgDataUpdate nodecfg;
} update;

/* MODULE */
struct {
clusterMsgModule msg;
} module;
};

缩容扩容

一个简单的 Redis Cluster 扩容的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 假设你已经有一个运行的 Redis Cluster,并且你想要添加一个新节点

# 在新节点上安装并运行Redis
wget http://download.redis.io/releases/redis-6.2.6.tar.gz
tar xzf redis-6.2.6.tar.gz
cd redis-6.2.6
make
src/redis-server --cluster-enabled yes --port 6380 --cluster-config-file nodes-6380.conf --cluster-node-timeout 5000 --appendonly yes --appendfilename appendonly-6380.aof --dbfilename dump-6380.rdb --logfile 6380.log

# 将新节点添加到集群
redis-cli --cluster add-node 127.0.0.1:6380 127.0.0.1:6379

# 重新分配 slots
redis-cli --cluster reshard 127.0.0.1:6380

# 将数据迁移到新节点
redis-cli --cluster rebalance 127.0.0.1:6380
  • 缩容时,你需要先将要移除的节点数据迁移到其他节点,然后再将其移除。
    • 这个过程可以通过 redis-cli --cluster reshardredis-cli --cluster forget 命令来完成。

请注意,实际的 Redis Cluster 扩容和缩容操作可能涉及到数据迁移和重分配,这些操作会耗费一定的时间,在此期间,集群仍然能够提供服务,但可能会影响性能。在执行这类操作前,请确保已经做好了充分的备份和测试。

Redis Cluster 扩容缩容期间可以提供服务吗?

类似的问题:

  • 如果客户端访问的 key 所属的槽正在迁移怎么办?
  • 如何确定给定 key 的应该分布到哪个哈希槽中?

Redis Cluster 扩容和缩容本质是进行重新分片,动态迁移哈希槽。

为了保证 Redis Cluster 在扩容和缩容期间依然能够对外正常提供服务,Redis Cluster 提供了重定向机制,两种不同的类型:

  • ASK 重定向
  • MOVED 重定向

从客户端的角度来看,ASK 重定向是下面这样的:

  1. 客户端发送请求命令,如果请求的 key 对应的哈希槽还在当前节点的话,就直接响应客户端的请求。
  2. 如果客户端请求的 key 对应的哈希槽当前正在迁移至新的节点,就会返回 -ASK 重定向错误,告知客户端要将请求发送到哈希槽被迁移到的目标节点。
  3. 客户端收到 -ASK 重定向错误后,将会临时(一次性)重定向,自动向目标节点发送一条 ASKING 命令。也就是说,接收到 ASKING 命令的节点会强制执行一次请求,下次再来需要重新提前发送 ASKING 命令。
  4. 客户端发送真正的请求命令。
  5. ASK 重定向并不会同步更新客户端缓存的哈希槽分配信息,也就是说,客户端对正在迁移的相同哈希槽的请求依然会发送到原节点而不是目标节点。

img

如果客户端请求的 key 对应的哈希槽应该迁移完成的话,就会返回 -MOVED 重定向错误,告知客户端当前哈希槽是由哪个节点负责,客户端向目标节点发送请求并更新缓存的哈希槽分配信息。

Redis cluster 的高可用与主备切换原理

Redis Cluster 相当于是内置了 Sentinel 机制,Redis Cluster 内部的各个 Redis 节点通过 Gossip 协议互相探测健康状态,在故障时可以自动切换。

Redis cluster 的高可用的原理,跟哨兵是类似的。

集群方案比较

  1. Twemproxy:Twemproxy 是 Twtter 开源的一个 Redis 和 Memcache 代理服务器,主要用于管理 Redis 和 Memcached 集群,减少与 Cache 服务器直接连接的数量。他的后端是多台 Redis 或 memcached 所以也可以被称为分布式中间件。
    使用时在本需要连接 Redis 的地方改为连接 twemproxy,它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 Redis,将结果再返回 Twemproxy。
    作用:

    • 通过代理的方式减少缓存服务器的连接数。
    • 自动在多台缓存服务器间共享数据。
    • 通过配置的方式禁用失败的结点。
    • 运行在多个实例上,客户端可以连接到首个可用的代理服务器。
    • 支持请求的流式与批处理,因而能够降低来回的消耗。
      缺点:
    • Twemproxy 自身单端口实例的压力;
    • 使用一致性 hash 后,Redis 节点数量改变的时候相应的计算值也会改变,数据无法自动移动到新的节点。
  2. Codis:豌豆荚技术团队研发的一个分布式 Redis 解决方案。
    对于上层的应用来说,连接到 Codis-Proxy(Redis代理服务)和连接原生的 Redis-Server 没有明显的区别, 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作,所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务。
    基本和 twemproxy 一致的效果,但它支持在节点数量改变情况下,旧节点数据可恢复到新 hash 节点。

  3. Redis cluster3.0:官方提供的集群方案,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置 slave。

生产环境中的 Redis 是怎么部署的

使用 Redis cluster 部署。10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10G 内存,一般线上生产环境,Redis 的内存尽量不要超过 10G,超过 10G 可能会有问题。

5 台机器对外提供读写,一共有 50G 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

参考


02-Redis 部署模式03集群(重要)
https://flepeng.github.io/interview-41-数据库-41-Redis-02-Redis-部署模式03集群(重要)/
作者
Lepeng
发布于
2020年8月8日
许可协议