集群机构模式如下所示,它有多个Redis节点组组成,每个节点组所服务的数据之间不存在交集。其中,每个节点组都有且仅有一个master节点,如果使用主从复制,那么可以有0到多个slave节点。master节点提供写服务和读服务,而slave节点只提供读服务。
配置
下面通过docker-compose来搭建集群。首先编写docker-compose.yml文件,内容如下:
version: '3.1'services:redis1:image: daocloud.io/library/redis:5.0.7restart: alwayscontainer_name: redis1environment:- TZ=Asia/Shanghaiports:- 7001:7001- 17001:17001volumes:- ./conf/redis1.conf:/usr/local/redis/redis.confcommand: ["redis-server", "/usr/local/redis/redis.conf"]redis2:image: daocloud.io/library/redis:5.0.7restart: alwayscontainer_name: redis2environment:- TZ=Asia/Shanghaiports:- 7002:7002- 17002:17002volumes:- ./conf/redis2.conf:/usr/local/redis/redis.confcommand: ["redis-server", "/usr/local/redis/redis.conf"]redis3:image: daocloud.io/library/redis:5.0.7restart: alwayscontainer_name: redis3environment:- TZ=Asia/Shanghaiports:- 7003:7003- 17003:17003volumes:- ./conf/redis3.conf:/usr/local/redis/redis.confcommand: ["redis-server", "/usr/local/redis/redis.conf"]redis4:image: daocloud.io/library/redis:5.0.7restart: alwayscontainer_name: redis4environment:- TZ=Asia/Shanghaiports:- 7004:7004- 17004:17004volumes:- ./conf/redis4.conf:/usr/local/redis/redis.confcommand: ["redis-server", "/usr/local/redis/redis.conf"]redis5:image: daocloud.io/library/redis:5.0.7restart: alwayscontainer_name: redis5environment:- TZ=Asia/Shanghaiports:- 7005:7005- 17005:17005volumes:- ./conf/redis5.conf:/usr/local/redis/redis.confcommand: ["redis-server", "/usr/local/redis/redis.conf"]redis6:image: daocloud.io/library/redis:5.0.7restart: alwayscontainer_name: redis6environment:- TZ=Asia/Shanghaiports:- 7006:7006- 17006:17006volumes:- ./conf/redis6.conf:/usr/local/redis/redis.confcommand: ["redis-server", "/usr/local/redis/redis.conf"]
每个节点的配置文件内容格式如下:
# 指定Redis的端口号port 7001# 开启集群架构模式cluster-enabled yes# 集群信息的文件cluster-config-file nodes-7001.conf# 集群对外ipcluster-announce-ip 121.199.75.6# 集群对外端口号cluster-announce-port 7001# 集群的总线端口cluster-announce-bus-port 17001
编写好相应的配置文件后,使用doker-compose up -d启动全部的Redis容器。容器启动后,随便进入到一个容器中,使用如下命令进行集群节点之间的互连:
redis-cli --cluster create 121.199.75.6:7001 121.199.75.6:7002 121.199.75.6:7003 121.199.75.6:7004 121.199.75.6:7005 121.199.75.6:7006 --cluster-replicas 1
等待一段时间,Redis的集群模式就搭建完毕了。
原理
Redis集群架构可以保证主从+哨兵的基本功能之外,继续提升Redis存储数据和并发响应的能力。Redis集群模式无中心,集群中每个Redis节点之间都是互相两两连接,客户端只需要随机的连接到其中任意一个节点上,就可以对集群中其他的Redis节点执行读写操作。
source
不同的节点之间通过Redis Cluster Bus进行交互,交换的信息有:
- slot槽和节点之间的对应关系
- 集群中每个节点的可用状态
- 当集群结构发生改变时,需要一定的协议对集群内所有的节点的配置信息达成一致
- 发布/订阅功能需要传递的信息
- …
Redis集群中内置了16384个哈希槽,客户端连接到 Redis 集群之后,会同时得到一份关于这个 集群的配置信息。当客户端具体对某一个 key 值进行操作时,会使用CRC16校验后计算出它的Hash值,然后把哈希值对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽。
Redis 会根据节点数量 大致均等 的将哈希槽映射到不同的节点,再结合集群的配置信息就能够知道这个 key 值应该存储在哪一个具体的 Redis 节点中。如果不属于自己管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去连接指定的节点执行读写请求。
cluster不支持跨节点的命令,如果一个请求中涉及到两个节点的key,那么请求将操作失败。Redis为了解决这个问题,引入了HashTag的概念,允许只使用key的某一部分计算哈希值,进而划分到相应的哈希槽。如果两个key某些部分是一致的,那么它们将会落到同一个槽中。
集群相对于前两种模式具有如下的优点:
- 数据分区:集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量大大增加;另一方面,每个master节点都可以对外提供读写服务,大大提高了集群的响应能力
- 高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务
分区方案
常用的分区方案有如下三种:
- 哈希值 % 节点数:这是最简单也是最直接的一种方式,简单的取模进行节点的选择。但是当新增或是删减节点导致节点的数量发生变化时,系统中所有的数据都需要重新计算映射关系,使得数据迁移工作繁重
- 一致性哈希分区:将整个哈希值空间组织为一个虚拟的圆环,范围是$[0 - 2^{32-1}]$。对于每一个数据来说,首先根据key计算得到哈希值后,明确了它在环上的位置,然后从该位置顺时针往下走,找到的第一个节点就是它需要映射的节点。

source
当节点的数量发生变化时,对于数据的影响仅限于附近的数据。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。
但是,当节点的数量比较少时,增加或删除节点可能会导致数据倾斜,造成严重的数据不均衡。例如,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。 - 带有虚拟节点的一致性哈希分区:即上面所介绍的Redis中哈希槽的思想,每个实际的节点都包含一定数量的哈希槽,每个槽包含的数据都在一定的范围内,它是数据管理和迁移的基本单位,此时节点数量的变化对于系统的影响就很小。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。
问:当集群中增删节点,哈希槽的分配是如何变化的呢?
答:仍以上图为例,系统中有
4个实际节点,假设为其分配16个槽(0-15),槽0-3位于node1;4-7位于node2;以此类推….如果此时删除node2,只需要将槽4-7重新分配即可,例如槽4-5分配给node1,槽 6 分配给node3,槽 7 分配给node4;可以看出删除node2后,数据在其他节点的分布仍然较为均衡。
slot迁移
当新增节点、节点下线或者因负载不均需要充分调整slot分布时,就会触发slot迁移操作。slot的迁移操作由外部的系统完成,Redis只是提供了过程中所需的原语供调用。这些原语主要包含两种:
- 节点迁移状态设置:源节点或是目标节点
- key迁移的原子命令:执行具体的迁移工作
slot迁移的过程示意图如下所示:
假设需要将A节点中的id为1的slot迁移到B节点,那么整个迁移的过程为:
- 向B节点发送状态变更命令,将B节点对应的slot设置为
IMPORTING - 向A节点发送状态变更命令,将A节点对应的slot设置为
MIGERTAING - 针对于A的slot中所有的key,分别向A发送
MIGRATE命令,告诉A将对应的key迁移到B上
由于两个节点在迁移的过程中仍然会相应客户端的请求,因此,面对请求两个节点的操作和正常状态下是有区别的:
- 对于A节点来说,如果客户端访问的key仍在A上,则正常处理该请求;如果key已经或者正在迁移到B,那么A会回复客户端
ASK消息,让它跳转到B上执行 - 对于B节点来说,所有非
ASK请求跳转而来的操作都不进行处理,而是回复MOVED命令,让客户端跳转到A执行
这样保证了同一个key在迁移之前总是在源节点执行操作,迁移之后总是在目标节点上执行操作。当slot中所有的key都成功的迁移到了B上,客户端使用CLUSTER SETSLOT命令,这是B的slot信息,让B感知到迁移的slot。
failover
集群的故障迁移需要解决如下的几个问题:
- 如果某个节点发生了故障,它如何让集群中其他的节点感知到
- 如果多个节点感知到了故障信息,那么如何确定感知到的节点一定就是发生了故障
- 如果确定发生了故障,如果将故障节点的一个slave晋升为新的master节点
- 当升级完成后,如何让集群所有的节点感知到变更的发生
集群中节点两两之间通过Redis Cluster Bus来进行PING/PONG通信,当节点A向节点B通信时,如果发送的PING消息没有正常得到PONG消息,那么节点A将节点B置为PFAIL(possible fail)状态。之后,节点A与集群中其他的节点使用gossip协议进行通信时,PFAIL状态信息就会传递到其他的节点。
节点A之后也会收到其他节点的PONG信息,如果其他节点认为节点B确实发生了故障的数量超过了一定值,那么节点A就将节点B设置为FAIL状态。
当节点B被设置为FAIL状态后,需要从它的多个slave节点中选举一个升级为新的master节点。slave节点在晋升前会进行协商优先级,优先级的重要决定因素就是slave节点最后一次同步master信息的时间,越近表示这个slave节点的数据越新,相应的优先级就越高。优先级越高的slave节点更有可能早发起选举,成为新的master节点的可能性越高。
参加选举的slave节点会向其他的master节点发送消息,如果master节点本轮还没有投票,则回复同意,否则拒绝。当slave节点收到超过半数的master节点同意的消息,则它升级为新的master节点。然后它通过PONG消息将最新的epoch进行广播,使得集群中其他的节点可以及时更新拓扑结构,直到集群收敛。
优化手段
如果集群中的节点设置了主从复制,那么客户端对于master节点的读请求会被直接响应,而对于slave节点的读请求,会被MOVED消息转移到master节点,由master节点响应。为了更好的满足读写分离的需求,Redis提供了READONLY命令,如果slave节点收到的是该命令,则直接处理请求,无需再转换到master节点处理。
另外,Redis集群还提供了单点保护功能。假设master节点A只有一个slave节点,当它的slave发生故障时,集群为了继续保证高可用性,会将节点B的slave进行副本迁移后,让它重新成为节点A的slave使用。为了满足这种需求,集群中节点的数量应该保持为2 * master + 1个节点。迁移的示意过程如下所示:
