简述
由于哨兵模式的一些局限性,比如说部署麻烦、不能够支持超大数据量的存储。在 Redis 的 3.0
版本正式推出了 Cluster
作为 Redis 的分布式解决方案。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster
架构方案达到负载均衡的目的。(通过哈希算法达到数据负载均衡的目的)
数据路由规则
路由算法
一般而言,数据路由方式有哈希
、顺序
两种。
路由方式 | 路由算法 | 特点 | 优点 | 缺点 | 代表 |
---|---|---|---|---|---|
哈希 | 取余算法 hash(key) % N、一致性哈希、虚拟槽 | 离散度好; 数据分布业务无关; 不可顺序、范围查询 | 实现简单 | 节点扩容、收缩对业务数据有影响需要重新计算数据路由节点 | Redis Cluster Cassandra |
顺序 | 时间、自增ID等切分 | 离散度易倾斜; 数据分布业务相关; 可顺序、范围查询 | 支持时间、自增ID等切分键范围查找; 扩容增加逻辑分区简单 | 数据由于按照切分键路由,会严重倾斜 | Hbase |
由于Redis中是使用哈希算法
相关内容,这里着重分析哈希取模算法
、一致性哈希算法
和虚拟槽
等。
哈希取模算法
路由方式 | 路由算法 | 特点 | 优点 | 缺点 |
---|---|---|---|---|
哈希 | 取余算法 hash(key) % N | 离散度好 数据分布业务无关 不可顺序、范围查询 | 实现简单 | 节点扩容、收缩对业务数据有影响需要重新计算数据路由节点 |
节点扩容、收缩对哈希取模
路由的数据分布有很大影响,因为路由计算公式hash(key) % N
中N为集群的节点数量发生了变化,因此所有的数据路由需要全部重新计算,因此对历史数据会产生几乎颠覆性的推翻和重算,如下:
一致性哈希算法
通过哈希取模算法可以看到,在集群节点发生扩容、收缩等变更时,对其算法的冲击是较为致命的。对于分布式环境下,业务量增加、大促备战进行扩容或业务调整、物理机异常收缩节点是很常见的操作,因此哈希算法的稳定性、一致性就显得尤为重要,它应该能再满足扩容、收缩需求基础上尽可能少的对数据路由特别是历史数据路由计算产生影响。
良好的分布式cahce系统中的哈希算法应该满足以下几个方面: 一致性哈希算法原理
- 平衡性(Balance)
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
- 单调性(Monotonicity)
单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲区加入到系统中,那么哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲区中去,而不会被映射到旧的缓冲集合中的其他缓冲区。简单的哈希算法往往不能满足单调性的要求,如最简单的线性哈希:x = (ax + b) mod (P),在上式中,P表示全部缓冲的大小。不难看出,当缓冲大小发生变化时(从P1到P2),原来所有的哈希结果均会发生变化,从而不满足单调性的要求。哈希结果的变化意味着当缓冲空间发生变化时,所有的映射关系需要在系统内全部更新。而在P2P系统内,缓冲的变化等价于Peer加入或退出系统,这一情况在P2P系统中会频繁发生,因此会带来极大计算和传输负荷。单调性就是要求哈希算法能够应对这种情况。
- 分散性(Spread)
在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
- 负载(Load)
负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
- 平滑性(Smoothness)
平滑性是指缓存服务器的数目平滑改变和缓存对象的平滑改变是一致的。
根据以上所述,引申出一致性哈希算法
的概念,实现思路如下:
- 先构建一个范围在0~232-1(32位的无符号整数)的哈希环,在0和232-1位置处重合。
- 计算所有集群节点的
hash
值,将集群节点均匀分布在哈希环上
- 业务数据读写时,根据数据键key计算
hash
值,然后顺时针找到第一个大于等于该哈希值且已分布在哈希环上的节点
一致性哈希算法
在扩容、收缩时存在负载不均衡的问题,一般通过扩容、收缩2倍数量节点进行调整。在集群节点较少时也容易由于哈希分布不均匀导致数据倾斜问题。
路由方式 | 路由算法 | 特点 | 优点 | 缺点 |
---|---|---|---|---|
哈希 | 一致性哈希 哈希环取最临近节点 | 哈希环将哈希取模的点对点路由方式扩展成集群节点与数据规则范围匹配 | 集群节点扩容、收缩变更时,只影响变更节点临近数据 | 节点少容易数据倾斜; 节点扩容或收缩会出现负载不均和问题,一般需要扩容或收缩一半 |
虚拟槽
为了进一步解决一致性哈希算法
中负载不均衡、少节点数据倾斜的问题,一般采用虚拟槽
来进行改进,Redis Cluster
便是采用这种方式进行集群节点管理的。
如上图,引入虚拟槽
的概念后,可以不考虑具体实际集群节点
的数量,而可以人为定义虚拟槽
的数量,只需要将集群节点
与虚拟槽
的槽位范围进行映射绑定即可,相当于在业务数据
和集群节点
之间增加了一层代理缓冲映射区,实现了集群节点
和业务数据
的解耦,在扩容和收缩集群节点
时只需要操作这个虚拟槽
的映射关系即可。
以上分别介绍了哈希取模算法
、一致性哈希算法
和虚拟槽
的概念,下面总结下演变过程和对比
分区策略 | 寻址算法 | 寻址图 | 改进项 |
---|---|---|---|
哈希取模 | hash(key) % N match node | - | |
一致性哈希 | hash(key) near match hash(node) | 一对一映射变更为范围寻点映射,减少N变更对全局映射影响 | |
虚拟槽 | hash(key) match (slot <---> node) | 回归一对一映射,但是也增加slot范围对node的多对一映射,slot来双向控制,灵活性更高 |
以上为数据分区路由规则的概述,数据分区是分布式数据库存储的核心,无论是MySQL分库分表、还是Redis Cluster集群或其他数据的分布式存储场景都会使用到。
Redis扩容和缩容操作
redis集群可以在保证对外服务不中断的情况进行灵活的扩容和缩容操作。前面一节讲到的redis数据分区规则,redis节点的数据是按照数据slot区分的,那么redis的扩容和缩容可以理解为数slot的切换和移动。
Redis扩容
在一个redis集群中添加一个新的节点,节点添加成功之后对新增节点分配槽点,其他节点将对应槽点的数据发送到新增节点上。此过程分为两个步骤:
- 准备新加入集群的节点,并将新的节点加入到集群中去
- 为新节点分配槽点并且迁移槽和数据
Redis缩容
- 节点是否有槽,如果有槽则需要进行数据迁移(将节点数据分到其他节点上,数据迁移成功之后通知其他节点,这个节点下线)
- 如果没有就直接下线
集群通信
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据
是指节点负责哪些数据,是否出现故障等状态信息。 常见的元数据维护方式分为:集中式
、P2P
。
通信协议
Redis Cluster采用P2P
的Gossip(流言)协议
, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节 点都会知道集群完整的信息,这种方式类似流言传播。
Redis Cluster节点Gossip协议通信过程:
- 集群中的每个节点都会单独开辟一个
TCP通道
,用于节点之间彼此通信,通信端口号在基础端口上加10000。- 每个节点在固定周期内通过特定规则选择几个节点发送
ping
消息。- 接收到
ping
消息的节点用pong
消息作为响应。
消息类型
如上,一般分为ping消息、pong消息、meet消息、fail消息
四种消息类型。
- meet消息 用于通知新节点加入。消息发送者通知接收者加入到当前集群,
meet消息
通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息
交换。 - ping消息 集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送
ping消息
,用于检测节点是否在线和交换彼此状态信息。ping消息
发送封装了自身节点和部分其他节点的状态数据。 - pong消息 当接收到
ping、meet消息
时,作为响应消息回复给发送方确认消息正常通信。pong消息
内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息
来通知整个集群对自身状态进行更新。 - fail消息 当节点判定集群内另一个节点下线时,会向集群内广播一个
fail消息
,其他节点接收到fail消息
之后把对应节点更新为下线状态。
消息类型 | 功能 | 触发 |
---|---|---|
meet | 通知新节点加入 | 新节点加入时 |
ping | 检测节点在线、交互状态信息 | 每秒 |
pong | 封装自身状态信息 | 回复、广播 |
fail | 广播节点异常信息 | 节点异常时 |
消息格式
消息格式分为消息头
、消息体
。 集群消息结构体如下:
union clusterMsgData {
/* PING, MEET and PONG */
struct {
/* Array of N clusterMsgDataGossip structures */
// 每条消息都包含两个 clusterMsgDataGossip 结构
clusterMsgDataGossip gossip[1];
} ping;
/* FAIL */
struct {
clusterMsgDataFail about;
} fail;
/* 忽略其他消息类型 */
};
复制代码
消息解析流程
消息成本
通过以上Gossip协议的通信方式来看
- 优点 是可以能够保持对集群节点信息的更新采集,能够较快响应异常
- 缺点 是由于是点对点通信,集群中节点间消息通信成本较高,且随着集群规模越大,成本呈指数级增长
针对消息通信成本较高影响网络负载的情况,Redis Cluster
通过预设值进行自适应算法来调整
集群搭建
搭建集群工作需要以下三个步骤:
- [step-1] 准备节点
这一步主要是配置好Redis Cluster
各节点集群配置文件,具体操作流程这里不做赘述,可自行研究。
- [step-2] 节点握手 这一步主要是通过
cluster meet {ip} {port}
命令建立各节点之间的通信开始进行Gossip协议消息传递。此时通过cluster info
命令查看cluster_state:fail
节点集群状态仍然是失败,因为此步仅完成了集群节点通信,还需要进行槽位分配才可以使集群开始工作。这正如上面我们讲的Redis Cluster
通过虚拟槽改进了一致性哈希算法。 - [step-3] 分配槽
Redis Cluster
中一共有16384个槽位,只有全部槽位都分配给节点后,集群才进入在线状态。Redis Cluster
中计算槽位hash
值采用了CRC16
算法。
分配槽,其实就是建立集群节点与槽范围的映射关系,如下是一个分配槽位区间给集群节点的命令集案例:
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462...10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923...16383}
复制代码
为什么有16384(214)个槽位?
CRC16
算法产生的hash值有16bit,该算法可以产生216=65536个值。换句话说,值是分布在0~65535之间。为什么不mod(65536),而选择**mod(16384)**呢
Redis作者回答了该问题 github.com/antirez/red…
关于集群环境,除了搭建,还会有日常维护涉及的节点扩容、收缩等,这些比较偏运维不在此进行赘述,感兴趣可以继续探究,扩容、收缩原理可参考上述介绍。
集群路由
MOVED
在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再 根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED
重定向错误,通知客户端请求正确的节点。这个过程称为MOVED
重定向
注意:
- 使用redis-cli命令时,可以加入
-c参数
支持自动重定向,简化手动发起重定向操作 - 重定向会产生大量网络IO消耗,一般Redis Client会维护一个映射关系表,作为客户端本地副本来进行维护,通过前置查询本地映射副本来减少重定向的情况产生
ASK
Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点的情况
类型 | 场景 | 作用 | 更新客户端缓存 |
---|---|---|---|
ASK | 集群slot迁移中 | 表明当前正在进行slot迁移 | 不更新,因为不知道何时完成和正确的slot |
MOVED | slot错误 | 仅表明slot错误 | 更新,集群会返回正确slot信息 |
故障处理
故障发现
Redis通过ping/pong
消息实现节点间通信用来交互节点间状态信息,故障发现流程也是基于此进行实现,故障判定有一套判定流程,这里主要区分为主观下线和客观下线。
主观下线
主观下线[简单来讲就是,当cluster-note-timeout
时间内某节点无法与另一 个节点顺利完成ping/pong
消息通信时,则将该节点标记为主观下线状态,但是该判断仅仅是某节点自身认为的,由于分布式网络的复杂性还需要其他节点的共同判定才可以。
客观下线
通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时。触发客观下线流程
为什么必须是负责槽的主节点参与故障发现决策? 因为集群模式下 只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只 进行主节点数据和状态信息的复制
为什么半数以上处理槽的主节点? 必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务
故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。Redis Cluster
集群故障异常的选主过程在raft
一致性协议的基础上做了调整,想深入了解可以自行研究,后续也会开篇专门论述。
故障恢复流程步骤如下:
- [step-1]资格检查
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor
,则当前从节点不具备故障转移资格。参数cluster-slave- validity-factor
用于从节点的有效因子,默认为10。
- [step-2]准备选举时间
当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程,这里采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。
- [step-3]发起选举
当从节点定时任务检测到达故障选举时间(failover_auth_time)到达 后,发起选举流程
- 更新配置纪元
配置纪元的主要作用 标示集群内每个主节点的不同版本和当前集群最大的版本。每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加 入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong
消息交换时,如出现slots等关键信息不一致时,以配置纪元更大 的一方为准,防止过时的消息状态污染集群
应用场景 新节点加入、槽节点映射冲突检测、从节点投票选举冲突检测
- 广播选举消息
在集群内广播选举消息(FAILOVER_AUTH_REQUEST
),并记录已发 送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。消息内容如同ping消息只是将type类型变为FAILOVER_AUTH_REQUEST
,当发起过投票后,再收到从节点的投票消息会进行拒绝,防止重复投票。
- [step-4]选举投票
注意 只有持有槽的主节点才会处理故障选举消息,而选举成功的规则是必须满足大于等于N/2+1
张选票才可以选举成功,因此机器部署必须至少满足3台,上图示例是5个Master节点的集群选举过程。
- [step-5]替换主节点
当从节点收集到足够的选票之后,触发替换主节点操作:
- 当前从节点取消复制变为主节点。
- 执行
clusterDelSlot
操作撤销故障主节点负责的槽,并执行clusterAddSlot
把这些槽委派给自己 - 向集群广播自己的
pong
消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息
集群架构
如图,是一个比较常见的Redis集群架构,这里仅供参考。
- client 所有的客户端请求都会根据请求键Key进行
slot
值计算,然后路由到相应负责的分区master
节点上进行请求命令处理 - 分区 或者这里也可以叫做
集群节点
,最少由一个master
和一个slave
构成,一般会选择一个主备在同机房,另外slave
节点部署到另外一个机房,这样异地多机房的部署可以满足灾备要求。 - 集群 Redis集群是由多个集群节点
<master,slave,slave>
构建而成的
集群限制
批量操作支持有限 如mset、mget
,目前只支持具有相同slot
值的key
执行批量操作。对于映射为不同slot
值的key
由于执行mget、mget
等操作可能存在于多个节点上因此不被支持。
对于集群环境下的mset、mget
,一般可以通过pipeline
方式进行实现,即将mset、mget
命令进行批量的set、get
命令转换,这样就打破了slot
值对key
路由限制,批执行后聚合数据再返回,是一种在集群环境下曲线救国的方式
除此之外,这种因slot
槽位不同导致无法执行的问题同样存在于lua
脚本中,当试图通过lua
脚本来原子地执行一系列命令时,对Key的读写也必须遵循这个要求
如果业务场景允许,可以通过给key增加{}
强制指定key的路由节点进行数据存放,如命令set/get {key}1
则强制指定key在第一个节点进行落库,便可进行操作
- 事务操作支持有限 同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能,可参考上述图例。
- 受字典表结构限制 key作为数据分区的唯一决策因子,值对象会根据Key进行数据分区存储,因此无法将一个大的键值对象如
hash
、list
等映射到不同的节点。这部分的数据拆分需要上层应用根据业务场景进行拆分和规划设计,防止大Key产生带来不良影响。 - 不支持多数据库空间 单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
- 复制结构只支持一层 从节点只能复制主节点(主-从),不支持嵌套树状复制结构(主-从-从)。
总结
在互联网分布式服务场景下,我们要确保高并发、高性能、高可用三个指标的SLA,通过之前的文章分析可以看到Redis通过内存管理、数据结构、自适应算法、事件驱动模型等实现确保了它的高并发、高性能,通过主从复制、哨兵机制等实现确保了它的高可用,而本篇的Redis Cluster
核心解决的是Redis分布式部署、服务伸缩等问题。