集群机构模式如下所示,它有多个Redis节点组组成,每个节点组所服务的数据之间不存在交集。其中,每个节点组都有且仅有一个master节点,如果使用主从复制,那么可以有0到多个slave节点。master节点提供写服务和读服务,而slave节点只提供读服务。
Redis集群架构 - 图1

配置

下面通过docker-compose来搭建集群。首先编写docker-compose.yml文件,内容如下:

  1. version: '3.1'
  2. services:
  3. redis1:
  4. image: daocloud.io/library/redis:5.0.7
  5. restart: always
  6. container_name: redis1
  7. environment:
  8. - TZ=Asia/Shanghai
  9. ports:
  10. - 7001:7001
  11. - 17001:17001
  12. volumes:
  13. - ./conf/redis1.conf:/usr/local/redis/redis.conf
  14. command: ["redis-server", "/usr/local/redis/redis.conf"]
  15. redis2:
  16. image: daocloud.io/library/redis:5.0.7
  17. restart: always
  18. container_name: redis2
  19. environment:
  20. - TZ=Asia/Shanghai
  21. ports:
  22. - 7002:7002
  23. - 17002:17002
  24. volumes:
  25. - ./conf/redis2.conf:/usr/local/redis/redis.conf
  26. command: ["redis-server", "/usr/local/redis/redis.conf"]
  27. redis3:
  28. image: daocloud.io/library/redis:5.0.7
  29. restart: always
  30. container_name: redis3
  31. environment:
  32. - TZ=Asia/Shanghai
  33. ports:
  34. - 7003:7003
  35. - 17003:17003
  36. volumes:
  37. - ./conf/redis3.conf:/usr/local/redis/redis.conf
  38. command: ["redis-server", "/usr/local/redis/redis.conf"]
  39. redis4:
  40. image: daocloud.io/library/redis:5.0.7
  41. restart: always
  42. container_name: redis4
  43. environment:
  44. - TZ=Asia/Shanghai
  45. ports:
  46. - 7004:7004
  47. - 17004:17004
  48. volumes:
  49. - ./conf/redis4.conf:/usr/local/redis/redis.conf
  50. command: ["redis-server", "/usr/local/redis/redis.conf"]
  51. redis5:
  52. image: daocloud.io/library/redis:5.0.7
  53. restart: always
  54. container_name: redis5
  55. environment:
  56. - TZ=Asia/Shanghai
  57. ports:
  58. - 7005:7005
  59. - 17005:17005
  60. volumes:
  61. - ./conf/redis5.conf:/usr/local/redis/redis.conf
  62. command: ["redis-server", "/usr/local/redis/redis.conf"]
  63. redis6:
  64. image: daocloud.io/library/redis:5.0.7
  65. restart: always
  66. container_name: redis6
  67. environment:
  68. - TZ=Asia/Shanghai
  69. ports:
  70. - 7006:7006
  71. - 17006:17006
  72. volumes:
  73. - ./conf/redis6.conf:/usr/local/redis/redis.conf
  74. command: ["redis-server", "/usr/local/redis/redis.conf"]

每个节点的配置文件内容格式如下:

  1. # 指定Redis的端口号
  2. port 7001
  3. # 开启集群架构模式
  4. cluster-enabled yes
  5. # 集群信息的文件
  6. cluster-config-file nodes-7001.conf
  7. # 集群对外ip
  8. cluster-announce-ip 121.199.75.6
  9. # 集群对外端口号
  10. cluster-announce-port 7001
  11. # 集群的总线端口
  12. cluster-announce-bus-port 17001

编写好相应的配置文件后,使用doker-compose up -d启动全部的Redis容器。容器启动后,随便进入到一个容器中,使用如下命令进行集群节点之间的互连:

  1. 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节点执行读写操作。
Redis集群架构 - 图2
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计算得到哈希值后,明确了它在环上的位置,然后从该位置顺时针往下走,找到的第一个节点就是它需要映射的节点。 Redis集群架构 - 图3
    source
    当节点的数量发生变化时,对于数据的影响仅限于附近的数据。以上图为例,如果在 node1node2 之间增加 node5,则只有 node2 中的一部分数据会迁移到 node5;如果去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。
    但是,当节点的数量比较少时,增加或删除节点可能会导致数据倾斜,造成严重的数据不均衡。例如,如果去掉 node2node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其他节点相比负载过高。
  • 带有虚拟节点的一致性哈希分区:即上面所介绍的Redis中哈希槽的思想,每个实际的节点都包含一定数量的哈希槽,每个槽包含的数据都在一定的范围内,它是数据管理和迁移的基本单位,此时节点数量的变化对于系统的影响就很小。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。

问:当集群中增删节点,哈希槽的分配是如何变化的呢?

答:仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15),槽 0-3位于node14-7位于node2;以此类推….如果此时删除 node2,只需要将槽4-7重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2后,数据在其他节点的分布仍然较为均衡。

slot迁移

当新增节点、节点下线或者因负载不均需要充分调整slot分布时,就会触发slot迁移操作。slot的迁移操作由外部的系统完成,Redis只是提供了过程中所需的原语供调用。这些原语主要包含两种:

  • 节点迁移状态设置:源节点或是目标节点
  • key迁移的原子命令:执行具体的迁移工作

slot迁移的过程示意图如下所示:
Redis集群架构 - 图4

假设需要将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个节点。迁移的示意过程如下所示:
Redis集群架构 - 图5