Redis11-集群

  • Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。
  • 3.0之前,Redis分布式方案一般有两种:
    • 客户端分区方案,优点是分区逻辑可控,缺点是需要自己处理数据路由、高可用、故障转移等问题。
    • 代理方案,优点是简化客户端分布式逻辑和升级维护便利,缺点是加重架构部署复杂度和性能损耗。
  • 现在官方为我们提供了专有的集群方案:Redis Cluster,它非常优雅地解决了Redis集群方面的问题,同时它也是学习分布式存储的绝佳案例。
  • 从数据分布、搭建集群、节点通信、集群伸缩、请求路由、故障转移、集群运维几个方面介绍Redis Cluster。
  • cluster命令
127.0.0.1:6379> cluster help
CLUSTER INFO                               #返回关于集群的信息。
CLUSTER NODES                              #返回节点所看到的集群配置。输出格式:
    <id> <ip:port> <flags> <master> <pings> <pongs> <epoch> <link> <slot> ... <slot>
CLUSTER MYID                               #返回节点id。
CLUSTER REPLICAS <node-id>                 #返回<node-id>从节点。
CLUSTER COUNT-failure-reports <node-id>    #返回<node-id>的失败报告数。
CLUSTER SLOTS                              #返回关于插槽范围映射的信息。每个系列由:
    start, end, master and replicas IP addresses, ports and ids
CLUSTER COUNTKEYSINSLOT <slot>             #返回<slot>中键的数量。
CLUSTER GETKEYSINSLOT <slot> <count>       #返回槽中当前节点存储的键名。
CLUSTER KEYSLOT <key>                      #返回<key>的散列槽。

CLUSTER MEET <ip> <port> [bus-port]        #将新节点加入到集群中。 
CLUSTER FORGET <node-id>                   #从集群中移除一个节点。
CLUSTER FAILOVER [force|takeover]          #将当前从节点提升为主节点。
CLUSTER REPLICATE <node-id>                #配置当前节点为<node-id>的从节点。
CLUSTER RESET [hard|soft]                  #重置当前节点(default: soft).

CLUSTER SETSLOT <slot> (importing|migrating|stable|node <node-id>)    #设置槽状态。
CLUSTER ADDSLOTS <slot> [slot ...]         #为当前节点分配槽位。
CLUSTER FLUSHSLOTS                         #删除当前节点所属槽位信息。
CLUSTER DELSLOTS <slot> [slot ...]         #删除当前节点的槽位信息。

CLUSTER BUMPEPOCH                          #推进集群配置时代。
CLUSTER SET-config-epoch <epoch>           #设置当前节点的配置epoch。

CLUSTER SAVECONFIG                         #强制将集群配置保存在磁盘上。 
  • redis-cli --cluster的相关命令
]# redis-cli --cluster help
  create         host1:port1 ... hostN:portN         #创建集群
                 --cluster-replicas <arg>            #每个主节点的从节点个数
  check          host:port                           #检查集群
                 --cluster-search-multiple-owners    #检查是否有槽同时被分配给了多个节点
  info           host:port                           #查看集群状态
  fix            host:port                           #修复集群
                 --cluster-search-multiple-owners    #修复槽的重复分配问题
                 --cluster-fix-with-unreachable-masters
  reshard        host:port                           #指定集群的任意一节点进行迁移slot,重新分slots
                 --cluster-from <arg>                #需要从哪些源节点上迁移slot,可从多个源节点完成迁移,以逗号隔开,传递的是节点的node id,还可以直接传递--from all,这样源节点就是集群的所有节点,不传递该参数的话,则会在迁移过程中提示用户输入
                 --cluster-to <arg>                  #slot需要迁移的目的节点的node id,目的节点只能填写一个,不传递该参数的话,则会在迁移过程中提示用户输入
                 --cluster-slots <arg>               #需要迁移的slot数量,不传递该参数的话,则会在迁移过程中提示用户输入。
                 --cluster-yes                       #指定迁移时的确认输入
                 --cluster-timeout <arg>             #设置migrate命令的超时时间
                 --cluster-pipeline <arg>            #定义cluster getkeysinslot命令一次取出的key数量,不传的话使用默认值为10
                 --cluster-replace                   #是否直接replace到目标节点

  rebalance      host:port                                      #指定集群的任意一节点进行平衡集群节点slot数量
                 --cluster-weight <node1=w1...nodeN=wN>         #指定集群节点的权重
                 --cluster-use-empty-masters                    #设置可以让没有分配slot的主节点参与,默认不允许
                 --cluster-timeout <arg>                        #设置migrate命令的超时时间
                 --cluster-simulate                             #模拟rebalance操作,不会真正执行迁移操作
                 --cluster-pipeline <arg>                       #定义cluster getkeysinslot命令一次取出的key数量,默认值为10
                 --cluster-threshold <arg>                      #迁移的slot阈值超过threshold,执行rebalance操作
                 --cluster-replace                              #是否直接replace到目标节点

  add-node       new_host:new_port existing_host:existing_port  #添加节点,把新节点加入到指定的集群,默认添加主节点
                 --cluster-slave                                #新节点作为从节点,默认随机一个主节点
                 --cluster-master-id <arg>                      #给新节点指定主节点
  del-node       host:port node_id                              #删除给定的一个节点,成功后关闭该节点服务
  call           host:port command arg arg .. arg               #在集群的所有节点执行相关命令
                 --cluster-only-masters
                 --cluster-only-replicas
  set-timeout    host:port milliseconds                         #设置cluster-node-timeout
  import         host:port                                      #将外部redis数据导入集群
                 --cluster-from <arg>                           #将指定实例的数据导入到集群
                 --cluster-from-user <arg>
                 --cluster-from-pass <arg>
                 --cluster-from-askpass
                 --cluster-copy                                 #migrate时指定copy
                 --cluster-replace                              #migrate时指定replace
  backup         host:port backup_directory

1、数据分布

1.1、数据分布理论

  • 分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集,如图10-1所示。

  • 需要重点关注的是数据分区规则。常见的分区规则有哈希分区和顺序分区两种,表10-1对这两种分区规则进行了对比。

  • Redis Cluster采用哈希分区规则,常见的哈希分区规则有几种。

1、节点取余分区

  • 使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:
    • hash(key) % N计算出哈希值,用来决定数据映射到哪一个节点上。
  • 这种方案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
  • 这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,比如划分为512或1024张表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。
  • 扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况,如图10-2所示。

 2、一致性哈希分区

  • 一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个token,范围一般在0~2^32,这些token构成一个哈希环。进行数据读写操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点,如图10-3所示。

  • 这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:
    • 加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景
    • 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
    • 普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
  • 正因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致性哈希进行改进,比如Dynamo系统。

3、虚拟槽分区

  • 虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,每个整数都是一个槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。
    • 槽是集群内数据管理和迁移的基本单位
    • 采用大范围槽的主要目的是为了方便数据拆分和集群扩展。
    • 每个节点会负责一定数量的槽,如图10-4所示。

  • 当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区。
  • Redis Cluster就是采用虚拟槽分区

1.2、Redis数据分区

  • Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。
  • 每一个节点负责维护一部分槽以及槽所映射的键值数据,如图10-5所示。

  • Redis虚拟槽分区的特点:
    • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
    • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
    • 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。
  • 数据分区是分布式存储的核心,理解和灵活运用数据分区规则对于掌握Redis Cluster非常有帮助。

1.3、集群功能限制

  • Redis集群相对单机在功能上存在一些限制,需要开发人员提前了解,在使用时做好规避。限制如下:
    • (1)key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作,对于映射为不同slot值的key存在于多个节点上因此不被支持。
    • (2)key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
    • (3)key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
    • (4)不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
    • (5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

2、搭建集群

  • 要搭建的集群架构:

  • 搭建集群工作需要以下三个步骤:
    • (1)准备节点。
    • (2)节点握手。
    • (3)分配槽。

2.1、准备节点

  • Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。
    • 每个节点需要开启配置cluster-enabled yes,让Redis运行在集群模式下。
    • 建议为集群内所有节点统一目录,一般划分三个目录:conf、data、log,分别存放配置、数据和日志相关文件。

1、配置文件

  • 配置文件,集群相关的配置。(Redis 6.0.16)
]# vim /apps/redis/redis_6379.conf
port 6379
bind 127.0.0.1
requirepass hengha123
daemonize yes
logfile "/apps/redis/logs/6379.log"
dir /apps/redis/data_6379/
tcp-keepalive 0
appendonly yes

cluster-enabled yes                    #开启集群模式
#cluster-node-timeout 15000            #节点超时时间,单位毫秒
cluster-config-file nodes-6379.conf    #集群内部配置文件
masterauth hengha123

2、启动所有节点

  • 配置文件命名是redis-{port}.conf,准备好配置后启动所有节点,命令如下:
]# /apps/redis/bin/redis-server /apps/redis/redis_6379.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6380.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6381.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6382.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6383.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6384.conf
  • 集群模式的Redis除了原有的配置文件之外又加了一份集群配置文件
    • 当集群内节点信息发生变化,如添加节点、节点下线、故障转移等。节点会自动保存集群状态到配置文件中。
    • 注意,Redis自动维护集群配置文件,不要手动修改,防止节点重启时产生集群信息错乱。
  • 第一次启动时如果没有集群配置文件,它会自动创建一份,文件名称使用cluster-config-file参数的值,建议采用node-{port}.conf格式定义,通过使用端口号区分不同节点,防止同一机器下多个节点彼此覆盖,造成集群信息异常。如果启动时存在集群配置文件,节点会使用配置文件内容初始化集群信息。启动过程如图10-6所示。

  • 如节点6379首次启动后生成集群配置如下:
]# cat /apps/redis/data_6379/nodes-6379.conf
d4ef31621267b6d30b29337df0b435a62ca1eea6 :0@0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
  • 文件内容记录了集群初始状态,这里最重要的是节点ID,它是一个40位16进制字符串,用于唯一标识集群内一个节点,之后很多集群操作都要借助于节点ID来完成。
    • 注意,节点ID不同于运行ID。节点ID在集群初始化时只创建一次,节点重启时会加载集群配置文件进行重用,而Redis的运行ID每次重启都会变化。
  • 在节点6379执行cluster nodes命令获取集群节点状态:
127.0.0.1:6379> cluster nodes
d4ef31621267b6d30b29337df0b435a62ca1eea6 :6379@16379 myself,master - 0 0 0 connected
  • 每个节点目前只能识别出自己的节点信息。我们启动6个节点,但每个节点彼此并不知道对方的存在,下面通过节点握手让6个节点彼此建立联系从而组成一个集群。

2.2、节点握手

  • 节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet <ip> <port>,如图10-7所示。

  • cluster meet命令是一个异步命令,执行之后立刻返回。内部发起与目标节点进行握手通信,如图10-8所示。
    • (1)节点6379本地创建6380节点信息对象,并发送meet消息。
    • (2)节点6380接受到meet消息后,保存6379节点信息并回复pong消息。
    • (3)之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信。
  • 这里的meet、ping、pong消息是Gossip协议通信的载体,它的主要作用是节点彼此交换状态数据信息。
  • 6379和6380节点通过meet命令建立通信之后,它们就可以彼此已经感知到对方的存在了。
//通过meet命令,让节点6379和6380节点进行握手通信
127.0.0.1:6379> cluster meet 127.0.0.1 6379
OK

//查看集群节点
127.0.0.1:6379> cluster nodes
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661412913823 1 connected
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 0 0 connected
  • 可以在集群内的任意一个节点上执行cluster meet命令加入新节点,握手状态会通过消息在集群内传播,这样其他节点会自动发现新节点并发起握手流程。最后执行cluster nodes命令确认6个节点都彼此感知并组成集群。
127.0.0.1:6379> cluster meet 127.0.0.1 6381
127.0.0.1:6379> cluster meet 127.0.0.1 6382
127.0.0.1:6379> cluster meet 127.0.0.1 6383
127.0.0.1:6379> cluster meet 127.0.0.1 6384

127.0.0.1:6379> cluster nodes
c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 master - 0 1661412961407 5 connected
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661412962000 1 connected
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 1661412960000 2 connected
da43e740a1a560a06fcebd87a8ea70ff2d82f670 127.0.0.1:6383@16383 master - 0 1661412961000 4 connected
633fc67cbc9f4697bb676fb5c939fbabdfc030e9 127.0.0.1:6382@16382 master - 0 1661412962421 3 connected
ef8838d1768ae25c8d9f626cb14ff933f486b873 127.0.0.1:6381@16381 master - 0 1661412960399 0 connected
  • 节点建立握手之后集群还不能正常工作,这时集群处于下线状态,所有的数据读写都被禁止。
//通过cluster info命令可以获取集群当前状态
127.0.0.1:6379> cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:0
...

//此时集群禁止写操作
127.0.0.1:6379> set hello redis
(error) CLUSTERDOWN Hash slot not served
  • 从输出内容可以看到,被分配的槽(cluster_slots_assigned)是0,由于目前所有的槽没有分配到节点,因此集群无法完成槽到节点的映射。只有当16384个槽全部分配给节点后,集群才进入在线状态

2.3、分配槽

  • Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过cluster addslots命令为节点分配槽,这里利用bash特性批量设置槽(slots),命令如下:
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 cluster addslots {0..5461}
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6380 -a hengha123 cluster addslots {5462..10922}
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6381 -a hengha123 cluster addslots {10923..16383}
  • 所有的槽都已经分配给节点后,查看看节点和槽的分配关系、集群状态
//查看节点和槽的分配关系
127.0.0.1:6379> cluster nodes
c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 master - 0 1661414041000 5 connected
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661414041015 1 connected 5462-10922
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 1661414041000 2 connected 0-5461
da43e740a1a560a06fcebd87a8ea70ff2d82f670 127.0.0.1:6383@16383 master - 0 1661414043031 4 connected
633fc67cbc9f4697bb676fb5c939fbabdfc030e9 127.0.0.1:6382@16382 master - 0 1661414040000 3 connected
ef8838d1768ae25c8d9f626cb14ff933f486b873 127.0.0.1:6381@16381 master - 0 1661414042024 0 connected 10923-16383

//当所有槽分配给节点后,集群进入在线状态。
127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
...

2.4、为主节点设置从节点

  • 目前还有三个节点没有使用,作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。
  • 集群模式下,Reids节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。
  • 使用cluster replicate <node-id>命令让一个节点成为从节点。其中命令执行必须在相应的从节点上执行,<node-id>是要复制主节点的节点ID,命令如下:
//主节点的节点ID
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 cluster nodes
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 master - 0 1661414994083 5 connected
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661414995093 1 connected 5462-10922
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 1661414992000 2 connected 0-5461
da43e740a1a560a06fcebd87a8ea70ff2d82f670 127.0.0.1:6383@16383 master - 0 1661414993000 4 connected
633fc67cbc9f4697bb676fb5c939fbabdfc030e9 127.0.0.1:6382@16382 master - 0 1661414994000 3 connected
ef8838d1768ae25c8d9f626cb14ff933f486b873 127.0.0.1:6381@16381 master - 0 1661414993075 0 connected 10923-16383

//6382是6379的从,6383是6380的从,6384是6381的从
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6382 -a hengha123 cluster replicate d4ef31621267b6d30b29337df0b435a62ca1eea6
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6383 -a hengha123 cluster replicate 577ae0053d0ec1e74a67ad46670e9ca445083b30
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6384 -a hengha123 cluster replicate ef8838d1768ae25c8d9f626cb14ff933f486b873
  • 通过cluster nodes命令查看集群状态和复制关系,如下所示:
127.0.0.1:6379> cluster nodes
c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 slave ef8838d1768ae25c8d9f626cb14ff933f486b873 0 1661415226000 0 connected
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661415226330 1 connected 5462-10922
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 1661415225000 2 connected 0-5461
da43e740a1a560a06fcebd87a8ea70ff2d82f670 127.0.0.1:6383@16383 slave 577ae0053d0ec1e74a67ad46670e9ca445083b30 0 1661415227000 1 connected
633fc67cbc9f4697bb676fb5c939fbabdfc030e9 127.0.0.1:6382@16382 slave d4ef31621267b6d30b29337df0b435a62ca1eea6 0 1661415227342 2 connected
ef8838d1768ae25c8d9f626cb14ff933f486b873 127.0.0.1:6381@16381 master - 0 1661415226000 0 connected 10923-16383
  • 目前为止,我们依照Redis协议手动建立一个集群。它由6个节点构成,3个主节点负责处理槽和相关数据,3个从节点负责故障转移。
  • 手动搭建集群便于理解集群建立的流程和细节,不过读者也从中发现集群搭建需要很多步骤,当集群节点众多时,必然会加大搭建集群的复杂度和运维成本。因此Redis官方提供了redis-cli --cluster工具方便我们快速搭建集群。

3、节点通信

3.1、通信流程

  • 在分布式存储中需要提供维护节点元数据信息的机制,元数据是指:节点负责哪些数据,是否出现故障等状态信息。
  • 常见的元数据维护方式分为:集中式和P2P方式。
  • Redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播,如图10-12所示。

  • 通信过程说明:
    • (1)集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
    • (2)每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
    • (3)接收到ping消息的节点用pong消息作为响应。
  • 集群中每个节点通过规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。

3.2、Gossip消息

  • Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息。
  • 常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等,它们的通信模式如图10-13所示。

  • meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据
  • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
  • 所有的消息格式划分为:消息头和消息体。
  • 消息头包含发送节点自身状态数据,接收节点根据消息头就可以获取到发送节点的相关数据,结构如下:
typedef struct {
    char sig[4];                               /* 信号标示 */
    uint32_t totlen;                           /* 消息总长度 */
    uint16_t ver;                              /* 协议版本*/
    uint16_t type;                             /* 消息类型,用于区分meet,ping,pong等消息 */
    uint16_t count;                            /* 消息体包含的节点数量,仅用于meet,ping,ping消息类型*/
    uint64_t currentEpoch;                     /* 当前发送节点的配置纪元 */
    uint64_t configEpoch;                      /* 主节点/从节点的主节点配置纪元 */
    uint64_t offset;                           /* 复制偏移量 */
    char sender[CLUSTER_NAMELEN];              /* 发送节点的nodeId */
    unsigned char myslots[CLUSTER_SLOTS/8];    /* 发送节点负责的槽信息 */
    char slaveof[CLUSTER_NAMELEN];             /* 如果发送节点是从节点,记录对应主节点的nodeId */
    uint16_t port;                             /* 端口号 */
    uint16_t flags;                            /* 发送节点标识,区分主从角色,是否下线等 */
    unsigned char state;                       /* 发送节点所处的集群状态 */
    unsigned char mflags[3];                   /* 消息标识 */
    union clusterMsgData data                  /* 消息正文 */;
} clusterMsg;
  • 集群内所有的消息都采用相同的消息头结构clusterMsg,它包含了发送节点关键信息,如节点id、槽映射、节点标识(主从角色,是否下线)等。
  • 消息体在Redis内部采用clusterMsgData结构声明,结构如下:
union clusterMsgData {
    /* ping,meet,pong消息体*/
    struct {
        /* gossip消息结构数组 */
        clusterMsgDataGossip gossip[1];
    } ping;
    /* FAIL 消息体 */
    struct {
        clusterMsgDataFail about;
    } fail;
// ...
};
  • 消息体clusterMsgData定义发送消息的数据,其中ping、meet、pong都采用cluster MsgDataGossip数组作为消息体数据,实际消息类型使用消息头的type属性区分。每个消息体包含该节点的多个clusterMsgDataGossip结构数据,用于信息交换,结构如下:
typedef struct {
    char nodename[CLUSTER_NAMELEN];    /* 节点的nodeId */
    uint32_t ping_sent;                /* 最后一次向该节点发送ping消息时间 */
    uint32_t pong_received;            /* 最后一次接收该节点pong消息时间 */
    char ip[NET_IP_STR_LEN];           /* IP */
    uint16_t port;                     /* port*/
    uint16_t flags;                    /* 该节点标识, */
} clusterMsgDataGossip;
  • 当接收到ping、meet消息时,接收节点会解析消息内容并根据自身的识别情况做出相应处理,对应流程如图10-14所示。

  • 接收节点收到ping/meet消息时,执行解析消息头和消息体流程:
    • 解析消息头过程:消息头包含了发送节点的信息,如果发送节点是新节点且消息是meet类型,则加入到本地节点列表;如果是已知节点,则尝试更新发送节点的状态,如槽映射关系、主从角色等状态。
    • 解析消息体过程:如果消息体的clusterMsgDataGossip数组包含的节点是新节点,则尝试发起与新节点的meet握手流程;如果是已知节点,则根据cluster MsgDataGossip中的flags字段判断该节点是否下线,用于故障转移。
  • 消息处理完后回复pong消息,内容同样包含消息头和消息体,发送节点接收到回复的pong消息后,采用类似的流程解析处理消息并更新与接收节点最后通信时间,完成一次消息通信。

3.3、节点选择

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

  • 根据通信节点选择的流程可以看出消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量

1、选择发送消息的节点数量

  • 集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。
  • 根据以上规则得出每个节点每秒需要发送ping消息的数量=1+10*num(node.pong_received>cluster_node_timeout/2),因此cluster_node_timeout参数对消息发送的节点数量影响非常大。当我们的带宽资源紧张时,可以适当调大这个参数,如从默认15秒改为30秒来降低带宽占用率。过度调大cluster_node_timeout会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。

2、消息数据量

  • 每个ping消息的数据量体现在消息头和消息体中,其中消息头主要占用空间的字段是myslots[CLUSTER_SLOTS/8],占用2KB,这块空间占用相对固定。消息体会携带一定数量的其他节点信息用于信息交换。具体数量见以下伪代码:
def get_wanted():
    int total_size = size(cluster.nodes)
    # 默认包含节点总量的1/10
    int wanted = floor(total_size/10);
    if wanted < 3:
        # 至少携带3个其他节点信息
        wanted = 3;
    if wanted > total_size -2 :
        # 最多包含total_size - 2个
        wanted = total_size - 2;
return wanted;
  • 根据伪代码可以看出消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于Redis集群来说并不是大而全的集群更好。

4、集群伸缩

  • Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容,如图10-16所示。

  • 从图10-16看出,Redis集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动
  • 集群的三个主节点分别维护自己负责的槽和对应的数据,如果希望加入1个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点,如图10-18所示。

  • 每个节点把一部分槽和数据迁移到新的节点,每个节点负责的槽和数据相比之前变少了从而达到了集群扩容的目的。
  • 这里我们故意忽略了槽和数据在节点之间迁移的细节,目的是想让读者重点关注在槽和节点分配上来,理解集群的水平伸缩的上层原理:集群伸缩=槽和数据在节点之间的移动

4.1、扩容集群

  • 扩容是分布式存储最常见的需求,Redis集群扩容操作可分为如下步骤:
    • (1)准备新节点。
    • (2)加入集群。
    • (3)迁移槽和数据。

4.1.1、准备新节点

  • 需要提前准备好新节点并运行在集群模式下,新节点建议跟集群内的节点配置保持一致,便于管理统一。
  • 准备好配置后启动两个节点命令如下:
]# /apps/redis/bin/redis-server /apps/redis/redis_6385.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6386.conf
  • 启动后的新节点作为孤儿节点运行,并没有其他节点与之通信,集群结构如图10-19所示。

4.1.2、加入集群

  • 新节点依然采用cluster meet命令加入到现有集群中(注意新的节点不能在其他集群中)。在集群内任意节点上执行cluster meet命令让6385和6386节点加入集群,命令如下:
127.0.0.1:6379> cluster meet 127.0.0.1 6385
127.0.0.1:6379> cluster meet 127.0.0.1 6386
  • 新节点加入后集群结构如图10-20所示。

  • 集群内新旧节点经过一段时间的ping/pong消息通信之后,所有节点会发现新节点并将它们的状态保存到本地。
127.0.0.1:6379> cluster nodes
c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 slave ef8838d1768ae25c8d9f626cb14ff933f486b873 0 1661493876000 0 connected
d436b1d332a2715b03e06b4b73fc684a7142cfbf 127.0.0.1:6386@16386 master - 0 1661493876000 7 connected
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661493877625 1 connected 5462-10922
da90b04875c30e9bf57823edd03513f8e916748d 127.0.0.1:6385@16385 master - 0 1661493877000 6 connected
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 1661493875000 2 connected 0-5461
da43e740a1a560a06fcebd87a8ea70ff2d82f670 127.0.0.1:6383@16383 slave 577ae0053d0ec1e74a67ad46670e9ca445083b30 0 1661493875578 1 connected
633fc67cbc9f4697bb676fb5c939fbabdfc030e9 127.0.0.1:6382@16382 slave d4ef31621267b6d30b29337df0b435a62ca1eea6 0 1661493876000 2 connected
ef8838d1768ae25c8d9f626cb14ff933f486b873 127.0.0.1:6381@16381 master - 0 1661493878640 0 connected 10923-16383
  • 新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。对于新节点的后续操作我们一般有两种选择:
    • 为它迁移槽和数据实现扩容。
    • 作为其他主节点的从节点负责故障转移。
  • 如果一个节点已经加入到了其他集群,再手动执行cluster meet命令将其加入新的集群,会造成被加入节点的集群合并到现有集群的情况,从而造成数据丢失和错乱,后果非常严重,线上谨慎操作。

4.1.3、迁移槽和数据

  • 加入集群后需要为新节点迁移槽和相关数据,槽在迁移过程中集群可以正常提供读写服务。

1、槽的迁移计划

  • 槽是Redis集群管理数据的基本单位。
    • 首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。
    • 迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。例如,在集群中加入6385节点,如图10-21所示。加入6385节点后,原有节点负责的槽数量从6380变为4096个。
  • 槽迁移计划确定后开始逐个把槽内数据从源节点迁移到目标节点。

2、迁移数据

  • 数据迁移过程是逐个槽进行的,每个槽数据迁移的流程如图10-23所示。

  • 流程说明:
    • 1)对目标节点发送cluster setslot <slot> importing <source_node-id>命令,让目标节点准备导入槽的数据。
    • 2)对源节点发送cluster setslot <slot> migrating <target_node-id>命令,让源节点准备迁出槽的数据。
    • 3)源节点循环执行cluster getkeysinslot <slot> <count>命令,获取count个属于槽{slot}的键。
    • 4)在源节点上执行migrate <targetIp> <targetPort> "" 0 <timeout> keys1 ... keysN...命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点.
      • 批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
    • 5)重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点。
    • 6)向集群内所有主节点发送cluster setslot <slot> node <target_node-id>命令,通知槽分配给目标节点。
      • 为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。
  • 使用伪代码模拟迁移过程如下:
def move_slot(source,target,slot):
    # 目标节点准备导入槽
    target.cluster("setslot",slot,"importing",source.nodeId);
    # 目标节点准备全出槽
    source.cluster("setslot",slot,"migrating",target.nodeId);
    while true :
        # 批量从源节点获取键
        keys = source.cluster("getkeysinslot",slot,pipeline_size);
        if keys.length == 0:
            # 键列表为空时,退出循环
            break;
        # 批量迁移键到目标节点
        source.call("migrate",target.host,target.port,"",0,timeout,"keys",keys);
    # 向集群所有主节点通知槽被分配给目标节点
    for node in nodes:
        if node.flag == "slave":
            continue;
        node.cluster("setslot",slot,"node",target.nodeId);
  • 根据以上流程,我们手动使用命令把源节点6379负责的槽4096迁移到目标节点6385中,流程如下:
    • 1)目标节点准备导入槽4096数据
//在目标节点上执行,使用源节点ID(<source_node-id>)
127.0.0.1:6385> cluster setslot 4096 importing d4ef31621267b6d30b29337df0b435a62ca1eea6

//确认槽4096导入状态开启
127.0.0.1:6385> cluster nodes
da90b04875c30e9bf57823edd03513f8e916748d 127.0.0.1:6385@16385 myself,master - 0 1661497274000 6 connected [4096-<-d4ef31621267b6d30b29337df0b435a62ca1eea6]
...
    • 2)源节点准备导出槽4096数据
//在源节点上执行,使用目标节点ID(<target_node-id>)
127.0.0.1:6379> cluster setslot 4096 migrating da90b04875c30e9bf57823edd03513f8e916748d

//确认槽4096导出状态开启
127.0.0.1:6379> cluster nodes
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 myself,master - 0 1661497683000 2 connected 0-5461 [4096->-da90b04875c30e9bf57823edd03513f8e916748d]
...
    • 3)批量获取槽4096对应的键
//在源节点上执行
127.0.0.1:6379> cluster getkeysinslot 4096 100
1) "key:test:5028"
2) "key:test:68253"
3) "key:test:79212"

//确认这三个键是否存在于源节点
127.0.0.1:6379> mget key:test:5028 key:test:68253 key:test:79212
1) "value:5028"
2) "value:68253"
3) "value:79212"
    • 4)批量迁移键,migrate命令保证了每个键迁移过程的原子性
//在源节点上执行
127.0.0.1:6379> migrate 127.0.0.1 6385 "" 0 5000 keys key:test:5028 key:test:68253 key:test:79212
    • 5)重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点
    • 6)通知所有主节点槽4096指派给了目标节点6385
//使用目标节点ID(<target_node-id>)
127.0.0.1:6379> cluster setslot 4096 node da90b04875c30e9bf57823edd03513f8e916748d
127.0.0.1:6380> cluster setslot 4096 node da90b04875c30e9bf57823edd03513f8e916748d
127.0.0.1:6381> cluster setslot 4096 node da90b04875c30e9bf57823edd03513f8e916748d
127.0.0.1:6385> cluster setslot 4096 node da90b04875c30e9bf57823edd03513f8e916748d

//确认源节点6379不再负责槽4096,改为目标节点6385负责
127.0.0.1:6385> cluster nodes
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 master - 0 1661498811786 2 connected 0-4095 4097-5461
da90b04875c30e9bf57823edd03513f8e916748d 127.0.0.1:6385@16385 myself,master - 0 1661498812000 8 connected 4096
...
  • 手动执行命令演示槽迁移过程,是为了让读者更好地理解迁移流程,实际操作时肯定涉及大量槽并且每个槽对应非常多的键。

3、添加从节点

  • 使用cluster replicate <masterNodeId>命令为主节点添加对应从节点,注意在集群模式下slaveof添加从节点操作不再支持。
  • 扩容之初我们把6385、6386节点加入到集群,节点6385迁移了部分槽和数据作为主节点,但相比其他主节点目前还没有从节点,因此该节点不具备故障转移的能力。
  • 这时需要把节点6386作为6385的从节点,从而保证整个集群的高可用。
//将6386添加为6385的从节点
127.0.0.1:6386> cluster replicate da90b04875c30e9bf57823edd03513f8e916748d

//查看节点6386状态确认已经变成6385节点的从节点
127.0.0.1:6386> cluster nodes
d4ef31621267b6d30b29337df0b435a62ca1eea6 127.0.0.1:6379@16379 master - 0 1661499252717 2 connected 0-4095 4097-5461
633fc67cbc9f4697bb676fb5c939fbabdfc030e9 127.0.0.1:6382@16382 slave d4ef31621267b6d30b29337df0b435a62ca1eea6 0 1661499250000 2 connected
c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 slave ef8838d1768ae25c8d9f626cb14ff933f486b873 0 1661499252000 0 connected
d436b1d332a2715b03e06b4b73fc684a7142cfbf 127.0.0.1:6386@16386 myself,slave da90b04875c30e9bf57823edd03513f8e916748d 0 1661499248000 8 connected
ef8838d1768ae25c8d9f626cb14ff933f486b873 127.0.0.1:6381@16381 master - 0 1661499251702 0 connected 10923-16383
577ae0053d0ec1e74a67ad46670e9ca445083b30 127.0.0.1:6380@16380 master - 0 1661499253732 1 connected 5462-10922
da90b04875c30e9bf57823edd03513f8e916748d 127.0.0.1:6385@16385 master - 0 1661499253000 8 connected 4096
da43e740a1a560a06fcebd87a8ea70ff2d82f670 127.0.0.1:6383@16383 slave 577ae0053d0ec1e74a67ad46670e9ca445083b30 0 1661499254747 1 connected
  • 到此整个集群扩容完成,集群关系结构如图10-24所示。

4.3、收缩集群

  • 收缩集群意味着缩减规模,需要从现有集群中安全下线部分节点。安全下线节点流程如图10-25所示。

  • 流程说明:
    • (1)首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。
    • (2)当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭。

1、下线迁移槽

  • 下线节点需要把自己负责的槽迁移到其他节点,与之前节点扩容的迁移槽过程一致。

2、忘记节点

  • 由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。也就是说让其他节点不再与要下线节点进行Gossip消息交换。Redis提供了cluster forget <down_node-id>命令实现该功能,如图10-27所示。

  • 当节点接收到cluster forget <down_node-id>命令后,会把<down_node-id>指定的节点加入到禁用列表中,在禁用列表内的节点不再发送Gossip消息。
    • 禁用列表有效期是60秒,超过60秒节点会再次参与消息交换。也就是说当第一次forget命令发出后,我们有60秒的时间让集群内的所有节点忘记下线节点。
  • 线上操作不建议直接使用cluster forget命令下线节点,需要跟大量节点命令交互,实际操作起来过于繁琐并且容易遗漏forget节点。

5、请求路由

  • Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。因此对于希望从单机切换到集群环境的应用需要修改客户端代码

5.1、请求重定向

  • 在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向,如图10-29所示。

  • 第一个命令执行成功,是因为键key:test:1对应槽5191正好位于6379节点负责的槽范围内。第二个命令执行失败,是因为键key:test:2对应槽9252不在6379节点负责的槽范围内,返回重定向信息MOVED {slot} {ip:port}。
    • 重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。
127.0.0.1:6379> set key:test:1 value-1
OK
127.0.0.1:6379> set key:test:2 value-2
(error) MOVED 9252 127.0.0.1:6380
  • 可以使用cluster keyslot {key}命令获取key所对应的槽
127.0.0.1:6379> cluster keyslot key:test:1
(integer) 5191
127.0.0.1:6379> cluster keyslot key:test:2
(integer) 9252
  • 使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作,如下所示:
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 -c
127.0.0.1:6379> set key:test:2 value-2
-> Redirected to slot [9252] located at 127.0.0.1:6380
OK
  • redis-cli自动帮我们连接到正确的节点执行命令,这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发,如图10-30所示。

  • 节点对于不属于它的键命令只回复重定向响应,并不负责转发。正因为集群模式下把解析发起重定向的过程放到客户端完成,所以集群客户端协议相对于单机有了很大的变化。
  • 键命令执行步骤主要分两步:
    • (1)计算槽
    • (2)查找槽所对应的节点。

1、计算槽

  • Redis首先需要计算键所对应的槽。根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数,使每个键都可以映射到0~16383槽范围内。伪代码如下:
def key_hash_slot(key):
    int keylen = key.length();
    for (s = 0; s < keylen; s++):
        if (key[s] == '{'):
            break;
    if (s == keylen) return crc16(key,keylen) & 16383;
    for (e = s+1; e < keylen; e++):
        if (key[e] == '}') break;
        if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
    /* 使用{和}之间的有效部分计算槽 */
    return crc16(key+s+1,e-s-1) & 16383;
  • 根据伪代码,如果键内容包含{和}大括号字符,则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽。
    • cluster keyslot命令就是采用key_hash_slot函数实现的。
    • 其中键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化。
127.0.0.1:6380> cluster keyslot key:test:111
(integer) 10050
127.0.0.1:6380> cluster keyslot key:{hash_tag}:111
(integer) 2515
127.0.0.1:6380> cluster keyslot key:{hash_tag}:222
(integer) 2515
  • 例如在集群模式下使用mget等命令优化批量调用时,键列表必须具有相同的slot,否则会报错。这时可以利用hash_tag让不同的键具有相同的slot达到优化的目的。命令如下:
127.0.0.1:6380> mget user:10086:frends user:10086:videos
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:6380> mget user:{10086}:friends user:{10086}:videos
1) "friends"
2) "videos"
  • 开发提示
    • Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛。

2、槽节点查找

  • Redis计算得到键对应的槽后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息,内部保存在clusterState结构中,结构所示:
typedef struct clusterState {
    clusterNode *myself;                  /* 自身节点,clusterNode代表节点结构体 */
    clusterNode *slots[CLUSTER_SLOTS];    /* 16384个槽和节点映射数组,数组下标代表对应的槽 */
    ...
} clusterState;
  • slots数组表示槽和节点对应关系,实现请求重定向伪代码如下:
def execute_or_redirect(key):
    int slot = key_hash_slot(key);
    ClusterNode node = slots[slot];
    if(node == clusterState.myself):
        return executeCommand(key);
    else:
        return '(error) MOVED {slot} {node.ip}:{node.port}';
  • 根据伪代码看出节点对于判定键命令是执行还是MOVED重定向,都是借助slots[CLUSTER_SLOTS]数组实现。根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端

5.2、Smart客户端

  • 大多数开发语言的Redis客户端都采用Smart客户端支持集群协议。Smart客户端通过在内部维护slot --> node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot --> node映射。
  • 客户端如何选择见:http://redis.io/clients,从中找出符合自己要求的客户端类库。

5.3、ASK重定向

1、客户端ASK重定向流程

  • Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点,如图10-32所示。

  • 当出现上述情况时,客户端键命令执行流程将发生变化,如下所示:
    • (1)客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
    • (2)如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error)ASK{slot}{targetIP}:{targetPort}。
    • (3)客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
  • ASK重定向整体流程如图10-33所示。

  • ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别。
    • ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。
    • MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。

2、节点内部处理

  • 为了支持ASK重定向,源节点和目标节点在内部的clusterState结构中维护当前正在迁移的槽信息,用于识别槽迁移情况,结构如下:
typedef struct clusterState {
    clusterNode *myself;                                 /* 自身节点 /
    clusterNode *slots[CLUSTER_SLOTS];                   /* 槽和节点映射数组 */
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];      /* 正在迁出的槽节点数组 */
    clusterNode *importing_slots_from[CLUSTER_SLOTS];    /* 正在迁入的槽节点数组*/
    ...
} clusterState;
  • 节点每次接收到键命令时,都会根据clusterState内的迁移属性进行命令处理,如下所示:
    • 如果键所在的槽由当前节点负责,但键不存在则查找migrating_slots_to数组查看槽是否正在迁出,如果是返回ASK重定向。
    • 如果客户端发送asking命令打开了CLIENT_ASKING标识,则该客户端下次发送键命令时查找importing_slots_from数组获取clusterNode,如果指向自身则执行命令。
    • 需要注意的是,asking命令是一次性命令,每次执行完后客户端标识都会修改回原状态,因此每次客户端接收到ASK重定向后都需要发送asking命令。
    • 批量操作。ASK重定向对单键命令支持得很完善,但是,在开发中我们经常使用批量操作,如mget或pipeline。当槽处于迁移状态时,批量操作会受到影响。

6、故障转移

  • Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。

6.1、故障发现

  • 当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。
    • 主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
    • 客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。

6.1.1、主观下线

  • 集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。流程如图10-34所示。

  • 流程说明:
    • (1)节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节点a更新最近一次与节点b的通信时间。
    • (2)如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。
    • (3)节点a内的定时任务检测到与节点b最后通信时间超过cluster-node-timeout时,更新本地对节点b的状态为主观下线(pfail)。
  • 主观下线简单来讲就是,当cluster-note-timeout时间内某节点无法与另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态。每个节点内的cluster State结构都需要保存其他节点信息,用于从自身视角判断其他节点的状态。结构关键属性如下:
typedef struct clusterState {
    clusterNode *myself;        /* 自身节点 /
    dict *nodes;                /* 当前集群内所有节点的字典集合,key为节点ID,value为对应节点ClusterNode结构 */
    ...
} clusterState;

//字典nodes属性中的clusterNode结构保存了节点的状态,关键属性如下:
typedef struct clusterNode {
    int flags;                  /* 当前节点状态,如:主从角色,是否下线等 */
    mstime_t ping_sent;         /* 最后一次与该节点发送ping消息的时间 */
    mstime_t pong_received;     /* 最后一次接收到该节点pong消息的时间 */
    ...
} clusterNode;
  • 其中最重要的属性是flags,用于标示该节点对应状态,取值范围如下:
CLUSTER_NODE_MASTER 1           /* 当前为主节点 */
CLUSTER_NODE_SLAVE 2            /* 当前为从节点 */
CLUSTER_NODE_PFAIL 4            /* 主观下线状态 */
CLUSTER_NODE_FAIL 8             /* 客观下线状态 */
CLUSTER_NODE_MYSELF 16          /* 表示自身节点 */
CLUSTER_NODE_HANDSHAKE 32       /* 握手状态,未与其他节点进行消息通信 */
CLUSTER_NODE_NOADDR 64          /* 无地址节点,用于第一次meet通信未完成或者通信失败 */
CLUSTER_NODE_MEET 128           /* 需要接受meet消息的节点状态 */
CLUSTER_NODE_MIGRATE_TO 256     /* 该节点被选中为新的主节点状态 */
  • 使用以上结构,主观下线判断伪代码如下:
// 定时任务,默认每秒执行10次
def clusterCron():
    // ... 忽略其他代码
    for(node in server.cluster.nodes):
        // 忽略自身节点比较
        if(node.flags == CLUSTER_NODE_MYSELF):
            continue;
        // 系统当前时间
        long now = mstime();
        // 自身节点最后一次与该节点PING通信的时间差
        long delay = now - node.ping_sent;
        // 如果通信时间差超过cluster_node_timeout,将该节点标记为PFAIL(主观下线)
        if (delay > server.cluster_node_timeout) :
            node.flags = CLUSTER_NODE_PFAIL;
  • Redis集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障。例如图10-35的场景。

  • 节点6379与6385通信中断,导致6379判断6385为主观下线状态,但是6380与6385节点之间通信正常,这种情况不能判定节点6385发生故障。因此对于一个健壮的故障发现机制,需要集群内大多数节点都判断6385故障时,才能认为6385确实发生故障,然后为6385节点进行故障转移。而这种多个节点协作完成故障发现的过程叫做客观下线。

6.1.2、客观下线

  • 当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受消息的节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。结构如下:
struct clusterNode {       /* 认为是主观下线的clusterNode结构 */
    list *fail_reports;    /* 记录了所有其他节点对该节点的下线报告 */
    ...
};
  • 通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时,触发客观下线流程。这里有两个问题:
    • (1)为什么必须是负责槽的主节点参与故障发现决策?因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制。
    • (2)为什么半数以上处理槽的主节点?必须半数以上是为了应对网络分区等原因造成的集群分割情况,被分割的小集群因为无法完成从主观下线到客观下线这一关键过程,从而防止小集群完成故障转移之后继续对外提供服务。
  • 假设节点a标记节点b为主观下线,一段时间后节点a通过消息把节点b的状态发送到其他节点,当节点c接受到消息并解析出消息体含有节点b的pfail状态时,会触发客观下线流程,如图10-36所示。

  • 流程说明:
    • (1)当消息体内含有其他节点的pfail状态会判断发送节点的状态,如果发送节点是主节点则对报告的pfail状态处理,从节点则忽略。
    • (2)找到pfail对应的节点结构,更新clusterNode内部下线报告链表。
    • (3)根据更新后的下线报告链表告尝试进行客观下线。
  • 这里针对维护下线报告和尝试客观下线逻辑进行详细说明。

1、维护下线报告链表

  • 每个节点ClusterNode结构中都会存在一个下线链表结构,保存了其他主节点针对当前节点的下线报告,结构如下:
typedef struct clusterNodeFailReport {
    struct clusterNode *node;    /* 报告该节点为主观下线的节点 */
    mstime_t time;               /* 最近收到下线报告的时间 */
} clusterNodeFailReport;
  • 下线报告中保存了报告故障的节点结构和最近收到下线报告的时间,当接收到fail状态时,会维护对应节点的下线上报链表,伪代码如下:
def clusterNodeAddFailureReport(clusterNode failNode, clusterNode senderNode) :
    // 获取故障节点的下线报告链表
    list report_list = failNode.fail_reports;
    // 查找发送节点的下线报告是否存在
    for(clusterNodeFailReport report : report_list):
        // 存在发送节点的下线报告上报
        if(senderNode == report.node):
            // 更新下线报告时间
            report.time = now();
            return 0;
        // 如果下线报告不存在,插入新的下线报告
        report_list.add(new clusterNodeFailReport(senderNode,now()));
    return 1;
  • 每个下线报告都存在有效期,每次在尝试触发客观下线时,都会检测下线报告是否过期,对于过期的下线报告将被删除。如果在cluster-node-time*2的时间内该下线报告没有得到更新则过期并删除,伪代码如下:
def clusterNodeCleanupFailureReports(clusterNode node) :
    list report_list = node.fail_reports;
    long maxtime = server.cluster_node_timeout * 2;
    long now = now();
    for(clusterNodeFailReport report : report_list):
        // 如果最后上报过期时间大于cluster_node_timeout * 2则删除
        if(now - report.time > maxtime):
            report_list.del(report);
  • 线报告的有效期限是server.cluster_node_timeout*2,主要是针对故障误报的情况。例如节点A在上一小时报告节点B主观下线,但是之后又恢复正常。现在又有其他节点上报节点B主观下线,根据实际情况之前的属于误报不能被使用。
  • 运维提示
    • 如果在cluster-node-time*2时间内无法收集到一半以上槽节点的下线报告,那么之前的下线报告将会过期,也就是说主观下线上报的速度追赶不上下线报告过期的速度,那么故障节点将永远无法被标记为客观下线从而导致故障转移失败。因此不建议将cluster-node-time设置得过小。

2、尝试客观下线

  • 集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线,流程如图10-37所示。

  • 流程说明:
    • (1)首先统计有效的下线报告数量,如果小于集群内持有槽的主节点总数的一半则退出。
    • (2)当下线报告大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
    • (3)向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。
  • 使用伪代码分析客观下线的流程,如下所示:
def markNodeAsFailingIfNeeded(clusterNode failNode) {
    // 获取集群持有槽的节点数量
    int slotNodeSize = getSlotNodeSize();
    // 主观下线节点数必须超过槽节点数量的一半
    int needed_quorum = (slotNodeSize / 2) + 1;
    // 统计failNode节点有效的下线报告数量(不包括当前节点)
    int failures = clusterNodeFailureReportsCount(failNode);
    // 如果当前节点是主节点,将当前节点计累加到failures
    if (nodeIsMaster(myself)):
        failures++;
    // 下线报告数量不足槽节点的一半退出
    if (failures < needed_quorum):
        return;
    // 将改节点标记为客观下线状态(fail)
    failNode.flags = REDIS_NODE_FAIL;
    // 更新客观下线的时间
    failNode.fail_time = mstime();
    // 如果当前节点为主节点,向集群广播对应节点的fail消息
    if (nodeIsMaster(myself))
        clusterSendFail(failNode);
  • 广播fail消息是客观下线的最后一步,它承担着非常重要的职责:
    • 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。
    • 通知故障节点的从节点触发故障转移流程。
  • 需要理解的是,尽管存在广播fail消息机制,但是集群所有节点知道故障节点进入客观下线状态是不确定的。比如当出现网络分区时有可能集群被分割为一大一小两个独立集群中。大的集群持有半数槽节点可以完成客观下线并广播fail消息,但是小集群无法接收到fail消息,如图10-38所示。

  • 但是当网络恢复后,只要故障节点变为客观下线,最终总会通过Gossip消息传播至集群的所有节点。
  • 运维提示
    • 网络分区会导致分割后的小集群无法收到大集群的fail消息,因此如果故障节点所有的从节点都在小集群内将导致无法完成后续故障转移,因此部署主从结构时需要根据自身机房/机架拓扑结构,降低主从被分区的可能性。

6.2、故障恢复

  • 故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程,如图10-39所示。

1、资格检查

  • 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time*cluster-slave-validity-factor,则当前从节点不具备故障转移资格。参数cluster-slavevalidity-factor用于从节点的有效因子,默认为10。

2、准备选举时间

  • 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。故障选举时间相关字段如下:
struct clusterState {
    ...
    mstime_t failover_auth_time;    /* 记录之前或者下次将要执行故障选举时间 */
    int failover_auth_rank;         /* 记录当前从节点排名 */
}
  • 这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。优先级计算伪代码如下:
def clusterGetSlaveRank():
    int rank = 0;
    // 获取从节点的主节点
    ClusteRNode master = myself.slaveof;
    // 获取当前从节点复制偏移量
    long myoffset = replicationGetSlaveOffset();
    // 跟其他从节点复制偏移量对比
    for (int j = 0; j < master.slaves.length; j++):
        // rank表示当前从节点在所有从节点的复制偏移量排名,为0表示偏移量最大.
        if (master.slaves[j] != myself && master.slaves[j].repl_offset > myoffset):
           rank++;
    return rank;
  • 使用之上的优先级排名,更新选举触发时间,伪代码如下:
def updateFailoverTime():
    // 默认触发选举时间:发现客观下线后一秒内执行。
    server.cluster.failover_auth_time = now() + 500 + random() % 500;
    // 获取当前从节点排名
    int rank = clusterGetSlaveRank();
    long added_delay = rank * 1000;
    // 使用added_delay时间累加到failover_auth_time中
    server.cluster.failover_auth_time += added_delay;
    // 更新当前从节点排名
    server.cluster.failover_auth_rank = rank;
  • 所有的从节点中复制偏移量最大的将提前触发故障选举流程,如图10-40所示。

  • 主节点b进入客观下线后,它的三个从节点根据自身复制偏移量设置延迟选举时间,如复制偏移量最大的节点slave b-1延迟1秒执行,保证复制延迟低的从节点优先发起选举。

3、发起选举

  • 当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:

(1)更新配置纪元

  • 配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元(clusterNode.configEpoch)标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元整个集群又维护一个全局的配置纪元(clusterState.current Epoch),用于记录集群内所有主节点配置纪元的最大版本。执行cluster info命令可以查看配置纪元信息:
127.0.0.1:6379> cluster info
cluster_current_epoch:15    // 整个集群最大配置纪元
cluster_my_epoch:13         // 当前主节点配置纪元
...
  • 配置纪元会跟随ping/pong消息在集群内传播,当发送方与接收方都是主节点且配置纪元相等时代表出现了冲突,nodeId更大的一方会递增全局配置纪元并赋值给当前节点来区分冲突,伪代码如下:
def clusterHandleConfigEpochCollision(clusterNode sender) :
    if (sender.configEpoch != myself.configEpoch || !nodeIsMaster(sender) || !nodeIsMaster(myself)) :
        return;
    // 发送节点的nodeId小于自身节点nodeId时忽略
    if (sender.nodeId <= myself.nodeId):
        return
    // 更新全局和自身配置纪元
    server.cluster.currentEpoch++;
    myself.configEpoch = server.cluster.currentEpoch;
  • 配置纪元的主要作用:
    • 标示集群内每个主节点的不同版本和当前集群最大的版本。
    • 每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。
    • 主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。
  • 配置纪元的应用场景有:
    • 新节点加入。
    • 槽节点映射冲突检测。
    • 从节点投票选举冲突检测。
  • 开发提示
    • 之前在通过cluster setslot命令修改槽节点映射时,需要确保执行请求的主节点本地配置纪元(configEpoch)是最大值,否则修改后的槽信息在消息传播中不会被拥有更高的配置纪元的节点采纳。由于Gossip通信机制无法准确知道当前最大的配置纪元在哪个节点,因此在槽迁移任务最后的cluster setslot{slot}node{nodeId}命令需要在全部主节点中执行一遍。
  • 从节点每次发起投票时都会自增集群的全局配置纪元,并单独保存在clusterState.failover_auth_epoch变量中用于标识本次从节点发起选举的版本。

(2)广播选举消息

  • 在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。消息内容如同ping消息只是将type类型变为FAILOVER_AUTH_REQUEST。

4、选举投票

  • 只有持有槽的主节点才会处理故障选举消息(FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。
  • 投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。
  • Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。
  • 当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主节点操作,例如集群内有5个持有槽的主节点,主节点b故障后还有4个,当其中一个从节点收集到3张投票时代表获得了足够的选票可以进行替换主节点操作,如图10-41所示。

  • 运维提示
    • 故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到3/2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。
  • 投票作废:每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout*2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。

5、替换主节点

  • 当从节点收集到足够的选票之后,触发替换主节点操作:
    • (1)当前从节点取消复制变为主节点。
    • (2)执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
    • (3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

6.3、故障转移时间

  • 在介绍完故障发现和恢复的流程后,这时我们可以估算出故障转移时间:
    • (1)主观下线(pfail)识别时间=cluster-node-timeout。
    • (2)主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
    • (3)从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。
  • 根据以上分析可以预估出故障转移时间,如下:
failover-time(毫秒) ≤ cluster-node-timeout + cluster-node-timeout/2 + 1000
  • 因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。配置时可以根据业务容忍度做出适当调整,但不是越小越好,下一节的带宽消耗部分会进一步说明。

7、集群运维

7.1、集群完整性

  • 为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。
    • 执行任何键命令返回(error)CLUSTERDOWN Hash slot not served错误。这是对集群完整性的一种保护措施,保证所有的槽都指派给在线的节点。
  • 但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况,因此建议将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行,不会影响其他主节点的可用性。

7.2、带宽消耗

  • 集群内Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000以内,也是出于对消息通信成本的考虑,因此单集群不适合部署超大规模的节点。在之前节点通信小节介绍到,集群内所有节点通过ping/pong消息彼此交换信息,节点间消息通信对带宽的消耗体现在以下几个方面:
    • 消息发送频率:跟cluster-node-timeout密切相关,当节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息。
    • 消息数据量:每个消息主要的数据占用包含:slots槽数组(2KB空间)和整个集群1/10的状态数据(10个节点状态数据约1KB)。
    • 节点部署的机器规模:机器带宽的上线是固定的,因此相同规模的集群分布的机器越多每台机器划分的节点越均匀,则集群内整体的可用带宽越高。
  • 例如,一个总节点数为200的Redis集群,部署在20台物理机上每台划分10个节点,cluster-node-timeout采用默认15秒,这时ping/pong消息占用带宽达到25Mb。如果把cluster-node-timeout设为20,对带宽的消耗降低到15Mb以下。
  • 集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗。因此搭建Redis集群时需要根据业务数据规模和消息通信成本做出合理规划:
    • (1)在满足业务需要的情况下尽量避免大集群。同一个系统可以针对不同业务场景拆分使用多套集群。这样每个集群既满足伸缩性和故障转移要求,还可以规避大规模集群的弊端。如笔者维护的一个推荐系统,根据数据特征使用了5个Redis集群,每个集群节点规模控制在100以内。
    • (2)适度提高cluster-node-timeout降低消息发送频率,同时cluster-nodetimeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平衡。
    • (3)如果条件允许集群尽量均匀部署在更多机器上。避免集中部署,如集群有60个节点,集中部署在3台机器上每台部署20个节点,这时机器带宽消耗将非常严重。

7.3、Pub/Sub广播问题

  • Redis在2.0版本提供了Pub/Sub(发布/订阅)功能,用于针对频道实现消息的发布和订阅。但是在集群模式下内部实现对所有的publish命令都会向所有的节点进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重带宽负担,如图10-44所示:

  • 针对集群模式下publish广播问题,需要引起开发人员注意,当频繁应用Pub/Sub功能时应该避免在大量节点的集群内使用,否则会严重消耗集群内网络带宽。针对这种情况建议使用sentinel结构专门用于Pub/Sub功能,从而规避这一问题。

7.4、集群倾斜

  • 集群倾斜指不同节点之间数据量和请求量出现明显差异,这种情况将加大负载均衡和开发运维的难度。因此需要理解哪些原因会造成集群倾斜,从而避免这一问题。

1、数据倾斜

  • 数据倾斜主要分为以下几种:
    • 节点和槽分配严重不均。
    • 不同槽对应键数量差异过大。
      • 不同槽对应键数量差异过大。键通过CRC16哈希函数映射到槽上,正常情况下槽内键数量会相对均匀。但当大量使用hash_tag时,会产生不同的键映射到同一个槽的情况。特别是选择作为hash_tag的数据离散度较差时,将加速槽内键数量倾斜情况。通过命令:cluster countkeysinslot{slot}可以获取槽对应的键数量,识别出哪些槽映射了过多的键。再通过命令cluster getkeysinslot {slot} {count}循环迭代出槽下所有的键。从而发现过度使用hash_tag的键。
    • 集合对象包含大量元素。
      • 对于大集合对象的识别可以使用redis-cli --bigkeys命令识别,具体使用见12.5节。找出大集合之后可以根据业务场景进行拆分。同时集群槽数据迁移是对键执行migrate操作完成,过大的键集合如几百兆,容易造成migrate命令超时导致数据迁移失败。
    • 内存相关配置不一致。
      • 内存相关配置指hash-max-ziplist-value、setmax-intset-entries等压缩数据结构配置。当集群大量使用hash、set等数据结构时,如果内存压缩数据结构配置不一致,极端情况下会相差数倍的内存,从而造成节点内存量倾斜。

2、请求倾斜

  • 集群内特定节点请求量/流量过大将导致节点之间负载不均,影响集群均衡和运维成本。常出现在热点键场景,当键命令消耗较低时如小对象的get、set、incr等,即使请求量差异较大一般也不会产生负载严重不均。但是当热点键对应高算法复杂度的命令或者是大对象操作如hgetall、smembers等,会导致对应节点负载过高的情况。避免方式如下:
    • (1)合理设计键,热点大集合对象做拆分或使用hmget替代hgetall避免整体读取。
    • (2)不要使用热键作为hash_tag,避免映射到同一槽。
    • (3)对于一致性要求不高的场景,客户端可使用本地缓存减少热键调用。

7.5、集群读写分离

1、只读连接

  • 集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到负责槽的主节点上(其中包括它的主节点)。当需要使用从节点分担主节点读压力时,可以使用readonly命令打开客户端连接只读状态。之前的复制配置slave-read-only在集群模式下无效。
  • 当开启只读状态时,从节点接收读命令处理流程变为:如果对应的槽属于自己正在复制的主节点则直接执行读命令,否则返回重定向信息。
//默认连接状态为普通客户端:flags=N
127.0.0.1:6382> client list
id=8 addr=10.1.1.12:47938 fd=24 name= age=2 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default

// 打开当前连接只读状态
127.0.0.1:6382> readonly
OK
// 客户端状态变为只读:flags=r
127.0.0.1:6382> client list
id=8 addr=10.1.1.12:47938 fd=24 name= age=49 idle=0 flags=r db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default
  • readonly命令是连接级别生效,因此每次新建连接时都需要执行readonly开启只读状态。执行readwrite命令可以关闭连接只读状态

2、读写分离

  • 集群模式下的读写分离,同样会遇到:复制延迟,读取过期数据,从节点故障等问题。针对从节点故障问题,客户端需要维护可用节点列表,集群提供了cluster slaves {nodeId}命令,返回nodeId对应主节点下所有从节点信息,数据格式同cluster nodes,命令如下:
127.0.0.1:6382> cluster slaves ef8838d1768ae25c8d9f626cb14ff933f486b873
1) "c0b18c48d787ad1a63f1f4e89b31ede560493d68 127.0.0.1:6384@16384 slave ef8838d1768ae25c8d9f626cb14ff933f486b873 0 1661764098000 0 connected"
  • 解析以上从节点列表信息,排除fail状态节点,这样客户端对从节点的故障判定可以委托给集群处理,简化维护可用从节点列表难度。
  • 开发提示
    • 集群模式下读写分离涉及对客户端修改如下:
      • (1)维护每个主节点可用从节点列表。
      • (2)针对读命令维护请求节点路由。
      • (3)从节点新建连接开启readonly状态。
  • 集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离
  • 集群读写分离有时用于特殊业务场景如:
    • (1)利用复制的最终一致性使用多个从节点做跨机房部署降低读命令网络延迟。
    • (2)主节点故障转移时间过长,业务端把读请求路由给从节点保证读操作可用。
  • 以上场景也可以在不同机房独立部署Redis集群解决,通过客户端多写来维护,读命令直接请求到最近机房的Redis集群,或者当一个集群节点故障时客户端转向另一个集群。

7.6、手动故障转移

  • Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为它的从节点,如图10-45所示。

  • 在从节点上执行cluster failover命令发起转移流程,默认情况下转移期间客户端请求会有短暂的阻塞,但不会丢失数据,流程如下:
    • (1)从节点通知主节点停止处理所有客户端请求。
    • (2)主节点发送对应从节点延迟复制的数据。
    • (3)从节点接收处理复制延迟的数据,直到主从复制偏移量一致为止,保证复制数据不丢失。
    • (4)从节点立刻发起投票选举(这里不需要延迟触发选举)。选举成功后断开复制变为新的主节点,之后向集群广播主节点pong消息,(故障转移的故障恢复)。
    • (5)旧主节点接受到消息后更新自身配置变为从节点,解除所有客户端请求阻塞,这些请求会被重定向到新主节点上执行。
    • (6)旧主节点变为从节点后,向新的主节点发起全量复制流程。
  • 运维提示
    • 主从节点转移后,新的从节点由于之前没有缓存主节点信息无法使用部分复制功能,所以会发起全量复制,当节点包含大量数据时会严重消耗CPU和网络资源,线上不要频繁操作。Redis4.0的Psync2将有效改善这一问题。
  • 手动故障转移的应用场景主要如下:
    • (1)主节点迁移:运维Redis集群过程中经常遇到调整节点部署的问题,如节点所在的老机器替换到新机器等。由于从节点默认不响应请求可以安全下线关闭,但直接下线主节点会导致故障自动转移期间主节点无法对外提供服务,影响线上业务的稳定性。这时可以使用手动故障转移,把要下线的主节点安全的替换为从节点后,再做下线操作操作,如图10-46所示。
    • (2)强制故障转移。当自动故障转移失败时,只要故障的主节点有存活的从节点就可以通过手动转移故障强制让从节点替换故障的主节点,保证集群的可用性。自动故障转移失败的场景有:
      • 主节点和它的所有从节点同时故障。这个问题需要通过调整节点机器部署拓扑做规避,保证主从节点不在同一机器/机架上。除非机房内大面积故障,否则两台机器/机架同时故障概率很低。
      • 所有从节点与主节点复制断线时间超过cluster-slave-validityfactor*cluster-node-tineout+repl-ping-slave-period,导致从节点被判定为没有故障转移资格,手动故障转移从节点不做中断超时检查。
      • 由于网络不稳定等问题,故障发现或故障选举时间无法在cluster-nodetimeout*2内完成,流程会不断重试,最终从节点复制中断时间超时,失去故障转移资格无法完成转移。
      • 集群内超过一半以上的主节点同时故障。

  • 根据以上情况,cluster failover命令也可以使用提供了参数force或takeover:
    • cluster failover force:用于当主节点宕机且无法自动完成故障转移情况。从节点接到cluster failover force请求时,从节点直接发起选举,不再跟主节点确认复制偏移量(从节点复制延迟的数据会丢失),当从节点选举成功后替换为新的主节点并广播集群配置。
    • cluster failover takeover:用于集群内超过一半以上主节点故障的场景,因为从节点无法收到半数以上主节点投票,所以无法完成选举过程。可以执行cluster failover takeover强制转移,接到命令的从节点不再进行选举流程而是直接更新本地配置纪元并替换主节点。takeover故障转移由于没有通过领导者选举发起故障转移,会导致配置纪元存在冲突的可能。当冲突发生时,集群会以nodeId字典序更大的一方配置为准。因此要小心集群分区后,手动执行takeover导致的集群冲突问题。如图10-47所示。

  • 图中Redis集群分别部署在2个同城机房,机房A部署节点:master-1、master-2、master-3、slave-4。机房B部署节点:slave-1、slave-2、slave-3、master-4。
    • 当机房之间出现网络中断时,机房A内的节点持有半数以上主节点可以完成故障转移,会将slave-4转换为master-4。
    • 如果客户端应用都部署在机房B,运维人员为了快速恢复对机房B的Redis访问,对slave-1,slave-2,slave-3分别执行cluster failover takeover强制故障转移,让机房B的节点可以快速恢复服务。
    • 当机房专线恢复后,Redis集群会拥有两套持有相同槽信息的主节点。这时集群会使用配置纪元更大的主节点槽信息,配置纪元相等时使用nodeId更大的一方,因此最终会以哪个主节点为准是不确定的。如果集群以机房A的主节点槽信息为准,则这段时间内对机房B的写入数据将会丢失。
  • 综上所述,在集群可以自动完成故障转移的情况下,不要使用clusterfailover takeover强制干扰集群选举机制,该操作主要用于半数以上主节点故障时采取的强制措施,请慎用。
  • 运维提示
    • 手动故障转移时,在满足当前需求的情况下建议优先级:cluster failver>cluster failover force>cluster failover takeover。

7.7、数据迁移

  • 社区开源了很多迁移工具,这里推荐一款唯品会开发的redis-migrate-tool,该工具可满足大多数Redis迁移需求,特点如下:
    • 支持单机、Twemproxy、Redis Cluster、RDB/AOF等多种类型的数据迁移。
    • 工具模拟成从节点基于复制流迁移数据,从而支持在线迁移数据,业务方不需要停写。
    • 采用多线程加速数据迁移过程且提供数据校验和查看迁移状态等功能。

8、集群与redis-cli --cluster

8.1、用redis-cli --cluster搭建集群

8.1.1、准备节点

1、配置文件

  • 配置文件,集群相关的配置。(Redis 6.0.16)
]# vim /apps/redis/redis_6379.conf
port 6379
bind 127.0.0.1
requirepass hengha123
daemonize yes
logfile "/apps/redis/logs/6379.log"
dir /apps/redis/data_6379/
tcp-keepalive 0
appendonly yes

cluster-enabled yes                    #开启集群模式
#cluster-node-timeout 15000            #节点超时时间,单位毫秒
cluster-config-file nodes-6379.conf    #集群内部配置文件
masterauth hengha123

2、启动所有节点

]# /apps/redis/bin/redis-server /apps/redis/redis_6379.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6380.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6381.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6382.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6383.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6384.conf

8.1.2、创建集群

  • Redis Cluster 在5.0之后取消了ruby脚本redis-trib.rb的支持(手动命令行添加集群的方式不变),集合到redis-cli里,避免了再安装ruby的相关环境。直接使用redis-cli的参数--cluster来取代。
  • 使用下面命令创建集群
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 --cluster create --cluster-replicas 1 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:6383 to 127.0.0.1:6379
Adding replica 127.0.0.1:6384 to 127.0.0.1:6380
Adding replica 127.0.0.1:6382 to 127.0.0.1:6381
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
M: 424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
M: 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
S: 116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382
   replicates 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4
S: c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383
   replicates 5112861e5368e546c39112497caa2098a3a39bd2
S: 91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384
   replicates 424ed63977d5686775a3e3ac9924fbf2c30ae866
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: 5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382
   slots: (0 slots) slave
   replicates 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4
S: 91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384
   slots: (0 slots) slave
   replicates 424ed63977d5686775a3e3ac9924fbf2c30ae866
S: c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383
   slots: (0 slots) slave
   replicates 5112861e5368e546c39112497caa2098a3a39bd2
M: 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
M: 424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
  • 查看集群信息
//获取集群节点状态
127.0.0.1:6379> cluster nodes
116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382@16382 slave 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 0 1661766111814 3 connected
91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384@16384 slave 424ed63977d5686775a3e3ac9924fbf2c30ae866 0 1661766109000 2 connected
c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383@16383 slave 5112861e5368e546c39112497caa2098a3a39bd2 0 1661766110805 1 connected
8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381@16381 master - 0 1661766109796 3 connected 10923-16383
424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380@16380 master - 0 1661766110000 2 connected 5461-10922
5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379@16379 myself,master - 0 1661766111000 1 connected 0-5460

//通过cluster info命令可以获取集群当前状态
127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
...

8.2、扩容节点

1、准备节点

]# /apps/redis/bin/redis-server /apps/redis/redis_6385.conf
]# /apps/redis/bin/redis-server /apps/redis/redis_6386.conf

2、加入到集群

  • 将6385节点作为主节点加入到6379节点所在的集群。
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 --cluster add-node 127.0.0.1:6385 127.0.0.1:6379
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Adding node 127.0.0.1:6385 to cluster 127.0.0.1:6379
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: 5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382
   slots: (0 slots) slave
   replicates 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4
S: 91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384
   slots: (0 slots) slave
   replicates 424ed63977d5686775a3e3ac9924fbf2c30ae866
S: c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383
   slots: (0 slots) slave
   replicates 5112861e5368e546c39112497caa2098a3a39bd2
M: 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
M: 424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6385 to make it join the cluster.
[OK] New node added correctly.
  • 将6386节点作为6385的从节点加入到6379节点所在的集群。
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 --cluster add-node 127.0.0.1:6386 127.0.0.1:6379 --cluster-slave --cluster-master-id 47e4d119078b846441946e329adbd8ef24e105f1
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Adding node 127.0.0.1:6386 to cluster 127.0.0.1:6379
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: 5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379
   slots:[3-5460] (5458 slots) master
   1 additional replica(s)
S: 116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382
   slots: (0 slots) slave
   replicates 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4
S: 91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384
   slots: (0 slots) slave
   replicates 424ed63977d5686775a3e3ac9924fbf2c30ae866
S: c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383
   slots: (0 slots) slave
   replicates 5112861e5368e546c39112497caa2098a3a39bd2
M: 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381
   slots:[10926-16383] (5458 slots) master
   1 additional replica(s)
M: 47e4d119078b846441946e329adbd8ef24e105f1 127.0.0.1:6385
   slots:[0-2],[5461-5765],[10923-10925] (311 slots) master
M: 424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380
   slots:[5766-10922] (5157 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6386 to make it join the cluster.
Waiting for the cluster to join

>>> Configure node as replica of 127.0.0.1:6385.
[OK] New node added correctly.
  • 查看添加节点的结果
127.0.0.1:6379> cluster nodes
116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382@16382 slave 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 0 1661768258806 3 connected
7d322e9fefc0dd9c9a09397f13db7795b8a8a785 127.0.0.1:6386@16386 slave 47e4d119078b846441946e329adbd8ef24e105f1 0 1661770625058 7 connected
91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384@16384 slave 424ed63977d5686775a3e3ac9924fbf2c30ae866 0 1661768259814 2 connected
c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383@16383 slave 5112861e5368e546c39112497caa2098a3a39bd2 0 1661768260822 1 connected
8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381@16381 master - 0 1661768257793 3 connected 10923-16383
47e4d119078b846441946e329adbd8ef24e105f1 127.0.0.1:6385@16385 master - 0 1661768258000 7 connected
424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380@16380 master - 0 1661768261830 2 connected 5461-10922
5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379@16379 myself,master - 0 1661768259000 1 connected 0-5460

3、迁移槽

  • 迁移槽的时候,也会将数据一起迁移到新的节点上
  • 将127.0.0.1:6380上的一个槽迁移到127.0.0.1:6385上
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 --cluster reshard 127.0.0.1:6379  --cluster-from 424ed63977d5686775a3e3ac9924fbf2c30ae866 --cluster-slots 1 --cluster-to 47e4d119078b846441946e329adbd8ef24e105f1
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Performing Cluster Check (using node 127.0.0.1:6379)
M: 5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: 116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382
   slots: (0 slots) slave
   replicates 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4
M: 7d322e9fefc0dd9c9a09397f13db7795b8a8a785 127.0.0.1:6386
   slots: (0 slots) master
S: 91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384
   slots: (0 slots) slave
   replicates 424ed63977d5686775a3e3ac9924fbf2c30ae866
S: c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383
   slots: (0 slots) slave
   replicates 5112861e5368e546c39112497caa2098a3a39bd2
M: 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
M: 47e4d119078b846441946e329adbd8ef24e105f1 127.0.0.1:6385
   slots: (0 slots) master
M: 424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

Ready to move 1 slots.
  Source nodes:
    M: 424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380
       slots:[5461-10922] (5462 slots) master
       1 additional replica(s)
  Destination node:
    M: 47e4d119078b846441946e329adbd8ef24e105f1 127.0.0.1:6385
       slots: (0 slots) master
  Resharding plan:
    Moving slot 5461 from 424ed63977d5686775a3e3ac9924fbf2c30ae866
Do you want to proceed with the proposed reshard plan (yes/no)? yes
Moving slot 5461 from 127.0.0.1:6380 to 127.0.0.1:6385:
  • 查看迁移结果(6385有一个槽5461)
127.0.0.1:6379> cluster nodes
116c9d0bff8732997ab085678cc1860de3cef6e7 127.0.0.1:6382@16382 slave 8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 0 1661768322392 3 connected
7d322e9fefc0dd9c9a09397f13db7795b8a8a785 127.0.0.1:6386@16386 slave 47e4d119078b846441946e329adbd8ef24e105f1 0 1661770625058 7 connected
91abd1252e802b8dc0dba56620002d2f8c0b1233 127.0.0.1:6384@16384 slave 424ed63977d5686775a3e3ac9924fbf2c30ae866 0 1661768320000 2 connected
c766296ad08f6c8c3ef95ef21b8612292dcacf6e 127.0.0.1:6383@16383 slave 5112861e5368e546c39112497caa2098a3a39bd2 0 1661768321000 1 connected
8a97be6ee9643522d9ee9b71d6bf1bbaaea20cb4 127.0.0.1:6381@16381 master - 0 1661768323401 3 connected 10923-16383
47e4d119078b846441946e329adbd8ef24e105f1 127.0.0.1:6385@16385 master - 0 1661768324410 7 connected 5461
424ed63977d5686775a3e3ac9924fbf2c30ae866 127.0.0.1:6380@16380 master - 0 1661768320000 2 connected 5462-10922
5112861e5368e546c39112497caa2098a3a39bd2 127.0.0.1:6379@16379 myself,master - 0 1661768322000 1 connected 0-5460

8.3、缩容节点

  • 将6386节点从集群中移除,如果是主节点请先迁移槽。
]# /apps/redis/bin/redis-cli -h 127.0.0.1 -p 6379 -a hengha123 --cluster del-node 127.0.0.1:6379 7d322e9fefc0dd9c9a09397f13db7795b8a8a785
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
>>> Removing node 7d322e9fefc0dd9c9a09397f13db7795b8a8a785 from cluster 127.0.0.1:6379
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

9、本章重点回顾

  • 1)Redis集群数据分区规则采用虚拟槽方式,所有的键映射到16384个槽中,每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。
  • 2)搭建集群划分三个步骤:准备节点,节点握手,分配槽。可以使用redis-trib.rb create命令快速搭建集群。
  • 3)集群内部节点通信采用Gossip协议彼此发送消息,消息类型分为:ping消息、pong消息、meet消息、fail消息等。节点定期不断发送和接受ping/pong消息来维护更新集群的状态。消息内容包括节点自身数据和部分其他节点的状态数据。
  • 4)集群伸缩通过在节点之间移动槽和相关数据实现。扩容时根据槽迁移计划把槽从源节点迁移到目标节点,源节点负责的槽相比之前变少从而达到集群扩容的目的,收缩时如果下线的节点有负责的槽需要迁移到其他节点,再通过cluster forget命令让集群内其他节点忘记被下线节点。
  • 5)使用Smart客户端操作集群达到通信效率最大化,客户端内部负责计算维护键→槽→节点的映射,用于快速定位键命令到目标节点。集群协议通过Smart客户端全面高效的支持需要一个过程,用户在选择Smart客户端时建议review下集群交互代码如:异常判定和重试逻辑,更新槽的并发控制等。节点接收到键命令时会判断相关的槽是否由自身节点负责,如果不是则返回重定向信息。重定向分为MOVED和ASK,ASK说明集群正在进行槽数据迁移,客户端只在本次请求中做临时重定向,不会更新本地槽缓存。MOVED重定向说明槽已经明确分派到另一个节点,客户端需要更新槽节点缓存。
  • 6)集群自动故障转移过程分为故障发现和故障恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。
  • 7)开发和运维集群过程中常见问题包括:超大规模集群带宽消耗,pub/sub广播问题,集群节点倾斜问题,手动故障转移,在线迁移数据等。
#                                                                                                                           #
posted @ 2022-08-23 17:59  麦恒  阅读(202)  评论(0编辑  收藏  举报