0%

Redis系列(三):Redis Cluster 集群模式


本篇文章来介绍 Redis 的第三种集群模式 Cluster 集群模式,该模式也是 Redis3.x 之后才引入的,在该
模式下解决了主从同步哨兵模式 的不能水平扩容的问题,使得 Redis 集群的性能得到提高,也由此成为了Redis 官方推荐使用的集群方案。本篇文章来介绍如何搭建使用 Redis Cluster 集群并试图去探究 Redis Cluster 集群的实现原理。

Redis主从架构+Sentinel仍存在问题

写并发的压力仍在:

  • Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了 master 节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。

海量数据的存储压力:

  • (内存容量的限制)Redis的最大缺点和局限性就在于内存存储数据,这样子对容量而言会有相当大的限制。
  • (持久化和硬盘的限制)Redis单实例本质上只有一台 Master 作为存储,如果面对海量数据的存储,一台 Redis 的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。

Redis Cluster 设计

首先我们需要了解 Redis Cluster 的设计目标:

  1. 高性能:高性能是 Redis 赖以生存的看家本领,增加集群之后不能对性能产生太大影响,否则会得不尝失。
  2. 水平扩展:之前 Redis 集群不能水平扩展的缺点时常被人诟病,所以必须具备水平扩展。
  3. 高可用:在之前,高可用主要是通过 Redis Sentinel 来保障,Cluster 集群也应该具备 Sentinel 的监控、提醒、自动故障转移等功能。

有了 Cluster 的集群方案,使得 Redis 变成了真正的分布式 NoSql 数据库。Redis Cluster 的集群实现:

  • 分片存储:Cluster 集群实现了分布式存储,对数据进行分片,将数据存储在不同的 master 节点上面,从而解决海量存储问题
  • 指令转换:Cluster 集群采用了去中心化思想,没有中心节点的说法,对于客户端来说,整个集群可以看作是一个整体,可以连接任何一个节点操作,当客户端操作的 key 并没有分配到该 node 上,集群会转向指令,指向正确的节点。
  • 高可用机制:Cluster 集群也内置了高可用机制,支持 N 个 master 节点,每个 master 节点都可以挂载多个 slave 节点,当 master 节点挂掉时,集群会提升它的某个 slave 节点作为新的 master 节点。

数据分片实现

为了使得集群能够水平扩展,首要解决的问题就是 如何将整个数据集按照一定的规则分配到多个节点上,实现数据分片的方案有以下几种:

1. 客户端实现

在客户度实现数据分片方案,在客户端用取模或者一致性哈希对 key 进行分片,查询和修改都先在客户端先通过哈希算法判断 key 的指向的路由。

  • java 中的 Jedis 客户端提供了 Redis Sharding 的方案,并且支持连接池。
  • Sharded 分片的原理?怎么连接到某一个Redis服务:提供了一致性 hash 和 md5 散列两种 hash 算法,默认使用一致性hash算法。
    并且为了使得请求能均匀的落在不同的节点上,Sharded Jedis会使用节点的名称(如果节点没有名称使用默认名称)虚拟化出160个虚拟节点。也可以根据不同节点的weight,虚拟化出160,weight个节点。
  • 当客户端访问 redis 时,首先根据 key 计算出其落在哪个节点上,然后找到节点的 ip 和端口进行连接访问。

2. 中间代理负载

这种方案的实现和第一种类似,对比第一种方案的区别有优势是将 key 哈希逻辑抽象成单独的服务,客户端不需要再感知 key 查找的逻辑,和单个 redis key 操作没有任何区别。代理服务负责 key 哈希的查找和流量的转发。典型的代理分区方案有 Twitter 开源的 Twemproxy 和国内的豌豆荚开源的 Codis。

3. 服务端负载

第三种方式就是纯服务端实现,客户端不需要关心,也不需要代理层,只需要服务端内部实现,这种方式需要服务端内部实现。 redis cluster 的方式就是这种方式。

数据哈希方案

普通哈希分区

普通的哈希分区比较简单,就是根据规定的哈希函数将数据哈希到指定的节点上,例如:现在有 3 个 Redis 结点 node1、node2、node3。我们的哈希函数采用取余法哈希

1
2
3
function hash(key) {
return key % 3
}

当写数据的时候,根据哈希函数写到对应的节点中,读数据的时候先计算出数据在哪个节点,然后再去对应的节点去取。我们发现当节点数固定的时候,该种数据分区的方案没有问题,当增加一个节点或删除一个节点的时候。
取余哈希函数的分母会改变,导致之前已分配的数据分区大量改变,并且造成大量的数据获取不到。所以该种方案很少使用。

一致性哈希分区

为了解决普通的哈希分区的缺点,提出了一致性哈希的概念。一致性哈希的核心原理是:

1
将数据 key 和节点都通过哈希函数映射到 2^32 次方的环上

关于一致性哈希的原理,超出了本文探讨的范围,后续再写专门的文章来详解。
一致性哈希尽最大限度的解决了节点数改变带来的数据不一致的问题。

虚拟槽分区

Cluster 采用的正是这种分区的方式。虚拟槽分区巧妙的使用了哈希空间,使用分散度良好的哈希函数把
所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot),Redis Cluster 的槽范围是 0
~ 16383,槽是集群内的数据管理和迁移的基本单位。每个节点负责一定数量的槽。计算公式:

1
CRC16(key)&16383

每一个节点负责维护一部分槽及槽所映射的键值数据,如下图所示(图片来源于网络):

Redis Cluster Slot 数据分布

采用 哈希虚拟槽分区 的特性:

  • 解耦了数据和节点之间的关系,简化了节点的扩容和收缩的难度
  • 节点自身来维护槽的映射关系,不需要客户端或者代理来维护槽分区的元数据
  • 至此节点、槽、键之间的映射查询。

节点增加和删除

采用 Cluster 的集群方案,当节点增加和删除时,集群又是如何工作来保证服务的高可用?

下图展现一个 5 个节点构成的集群,每个节点平均大约负责 3276 个槽,以及通过计算公式映射到对应节点的对应槽的过程。

Redis Cluster Slot 数据分布(图片来源于网络)

  • 增加节点
    当增加一个节点 Node-6 时,只需要把其他节点的某些哈希槽挪到新的节点就可以了。

  • 移除节点
    移除一个节点 Node-5 时,只需要把该节点上的哈希槽挪到其他的节点上就可以了。

在增加和删除节点,redis 的其他节点都不需要停机。

数据迁移

1
那么如何将槽的数据挪到其他的结点呢?

为了实现节点之间的数据迁移,节点之间必须相互连接。数据迁移分为两部分:

槽的迁移

现在要将 Master A 节点中的编号为 1,2,3 的槽迁移到 Master B 中

数据迁移状态

在迁移的中间状态下,槽 1,2,3 在 MasterA 节点的状态为 MIGRATING(迁移),在 MasterB 节点的状态为 IMPORTING(入口)
IMPORTING(入口) 状态是被迁移的槽在目标节点中出现的一种状态,准备迁移从A到B的时候,被迁移的槽的状态首先变为 IMPORTING(入口)
注意:此时并不刷新 node 的映射关系

键空间的迁移

在满足了槽迁移的条件下,通过相关命令将 slot1, slot2, slot3 中的键空间从 A 迁移到B。迁移过程大概如下:

  1. Master A 节点执行 DUMP 命令,序列化要迁移的 key,并将数据发送给 Master B
  2. Master B 节点接受到要迁移的序列化的 key 之后执行 RESTORE 命令反序列化为 key, 并保存
  3. Master A 节点执行 DEL 命令删除掉已迁移的 key

迁移完成之后,刷新 node 的映射关系

需要注意的是: MIGRATE(迁移) 并不是原子的,如果在 MIGRATE 出现错误的情况可能会导致下面问题:

  • 键空间在两个节点都存在;
  • 键空间只存在第一个节点;

深挖细节

  1. 为什么不用一致性哈希,而用槽哈希分区,原因是什么?
    Redis 使用的是 crc16 的简单算法,Redis 的作者认为 crc16(key) mod 16384 的效果已经不错了,虽然可能没有一致性哈希灵活,但实现比较简单,节点的增加和删除都比较方便

  2. 节点增加和删除的过程中,数据会不会丢失?
    节点在数据迁移的时候数据会有备份,不会丢失

  3. redis cluster 执行 get 命令之后是如何操作的?

  • 执行命令,指令到达任意一个服务端节点,redis 先检查是否是集群状态
  • 如果是非集群状态就直接执行命令返回
  • 如果是集群状态,会根据 key 执行哈希操作计算得到对应的哈希槽位
  • 根据每个节点本地保持的节点和槽位的映射关系,判断计算得到的槽位所在节点
    • 如果是本节点,则直接执行命令查找对应 redis 数据返回
    • 如果不是本节点,则 内部直接转发到对应的正确的节点
  • 返回节点的查找数据

那么节点之间是如何不同步节点的信息的(节点状态、节点和槽位的映射关系、槽位迁移状态)?请看下面的节点通信

  1. redis cluster 执行 mget 命令之后如何操作?
    1
    > MGET k1 k2 k3
    假设 k1,k2,k3 不在一个槽位且存储的数据不在一个一个节点上,mget 命令执行失败会发生异常:
    1
    (error) CROSSSLOT Keys in request don't hash to the same slot

首先,cluster 是不支持 mutlikey 操作的,例如:mget,Redis cluster 集群模式下对于哈希结果算出来的slot不一致时,会执行失败。

如何优化或者支持 mget ?首先必须在原来 cluster 之上包装一层来实现,包装逻辑可采用如下几种方案:

  1. 传统的串行 IO 操作,也就说 n 个 key,分n次串行操作来获取key,复杂度是o(n)。
  2. 将 Mget 操作(n个key),利用已知的hash函数算出key对应的节点,这样就可以得到一个这样的关系:Map<node, somekeys>,也就是每个节点对应的一些keys,这样将之前的o(n)的效率降低到o(node.size())。
  3. 在方案二的基础上将串行取数据改为并行取数据,进一步提高效率。
  4. 在key里面加入{hash tag}即可。Redis 在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。

节点通信

Redis Cluster 中,节点间是如何通信的呢?又是如何保障一致性、可用性的呢?欲知答案,必先了解 Gossip 算法。

Gossip 简介

Gossip 算法源自流行病学的研究,经过不断的发展演化,作为一种分布式一致性协议而得到广泛应用,如 Cassandra、Akka、Redis 都有用到。

Gossip 原理

Gossip 算法如其名,在办公室,只要一个人八卦一下,在有限的时间内所有的人都会知道该八卦的信息,这种方式也与病毒传播类似。实现原理简单解释是,在一个有届网络中,每个节点都随机的与其他节点通信,经过一段时间杂乱无章的通讯,最终各个节点的状态会保持一致。所以 Gossip 协议本质上是一种带冗余的容错算法,更近一步,Gossip 是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在“最终”所有节点一致,“最终”是一个现实中存在,但理论上无法证明的时间点。但 Gossip 的缺点也很明显,冗余通信会对网路带宽、CUP 资源造成很大的负载。总结有如下特点:

  • 每个节点随机选择节点通信,通信存在冗余
  • 同步到所有节点的最终数据一致的状态需要一段时间
  • 需要单独的 tcp 端口进行通信,需要消耗一定的网络带宽

Gossip 消息种类

Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的 Gossip 消息,常用的 Gossip 消息可分为:Ping 消息、Pong 消息、Meet 消息、Fail 消息。

  • Meet 消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,Meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 Ping、Pong 消息交换;
  • Ping 消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其它节点发送 Ping 消息,用于检测节点是否在线和交换彼此状态信息。Ping 消息发送封装了自身节点和部分其它节点的状态数据;
  • Pong 消息:当接收到 Ping、Meet 消息时,作为响应消息回复给发送方确认消息正常通信。Pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的 Pong 消息来通知整个集群对自身状态进行更新;
  • Fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 Fail 消息,其他节点接收到 Fail 消息之后把对应节点更新为下线状态。

通信时机

由于集群内部需要频繁地进行节点信息交换,而 Ping/Pong 消息携带当前节点和部分其它节点的状态数据,势必会加重带宽和计算的负担。Redis 集群内节点通信采用固定频率(定时任务每秒执行10次),因此,节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少则会降低集群内所有节点彼此信息交换的频率,从而影响故障判定、新节点发现等需求的速度。因此 Redis 集群的 Gossip 协议需要兼顾信息交换实时性和成本开销。

主从同步

我们已经了解了 Cluster 的集群的工作方式,那使用 Cluster 模式如何来实现主从同步?

其实主从同步还是使用的 Redis 本身的主从复制模式,将主从同步和 Cluster 模式结合起来的架构如下:

主从同步+Cluster集群(图片来源于网络)

从图中可以看出,将多个 Master 节点作为 Cluster 的节点,每个 Master 的节点又增加多个 Slave 节点。并且数据读写分离。
如果想水平扩展读的并发能力,可以增加多个 Slave, 想水平扩展写的并发能力,可以增加多个 Master, 并且任何一个 Master 和 Slave 宕掉都不会影响服务稳定性。

参考资料

三张图秒懂Redis集群设计原理
Redis的Cluster集群搭建
Cluster (集群) 下的操作的变化
Redis Cluster 模式分析,哈希槽