第十章 集群

Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。之前,Redis分布式方案一般有两种:

  • 客户端分区方案,优点是分区逻辑可控,缺点是需要自己处理数据路由、高可用、故障转移等问题。
  • 代理方案,优点是简化客户端分布式逻辑和升级维护便利,缺点是加重架构部署复杂度和性能损耗。
    现在官方为我们提供了专有的集群方案:Redis Cluster,它非常优雅地解决了Redis集群方面的问题,因此理解应用好Redis Cluster将极大地解放我们使用分布式Redis的工作量,同时它也是学习分布式存储的绝佳案例。
    本章将从数据分布、搭建集群、节点通信、集群伸缩、请求路由、故障转移、集群运维几个方面介绍Redis Cluster。

10.1 数据分布

10.1.1 数据分布理论

分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
需要重点关注的是数据分区规则。常见的分区规则有哈希分区和顺序分区两种,表10-1对这两种分区规则进行了对比。

由于Redis Cluster采用哈希分区规则,这里我们重点讨论哈希分区,常见的哈希分区规则有几种,下面分别介绍。

1. 节点取余分区

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

图10-2 翻倍扩容迁移约50%数据

2. 一致性哈希分区

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

图10-3 一致性哈希数据分布

这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:
-加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。

  • 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
  • 普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
    正因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致性哈希进行改进,比如Dynamo系统。

3. 虚拟槽分区

虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽,如图10-4所示。
当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区。Redis Cluster就是采用虚拟槽分区,下面就介绍Redis数据分区方法。

10.1.2 Redis数据分区

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

图10-4 槽集合与节点关系 568
图10-5 使用CRC16(key)&16383将键映射到槽上

Redis虚拟槽分区的特点:

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
  • 支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。

数据分区是分布式存储的核心,理解和灵活运用数据分区规则对于掌握 Redis Cluster非常有帮助。

10.1.3 集群功能限制

Redis集群相对单机在功能上存在一些限制,需要开发人员提前了解,在使用时做好规避。限制如下:

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

10.2 搭建集群

介绍完Redis集群分区规则之后,下面我们开始搭建Redis集群。搭建集群工作需要以下三个步骤:

  1. 准备节点。
  2. 节点握手。
  3. 分配槽。

10.2.1 准备节点

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

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

其他配置和单机模式一致即可,配置文件命名规则redis-{port}.conf,准备好配置后启动所有节点,命令如下:

redis-server conf/redis-6379.conf
redis-server conf/redis-6380.conf
redis-server conf/redis-6381.conf
redis-server conf/redis-6382.conf
redis-server conf/redis-6383.conf
redis-server conf/redis-6384.conf

Redis Cluster 第一次启动时如果没有集群配置文件,它会自动创建一份,文件名称采用cluster-config-file参数项控制,建议采用node-{port}.conf格式定义,通过使用端口号区分不同节点,防止同一机器下多个节点彼此覆盖,造成集群信息异常。如果启动时存在集群配置文件,节点会使用配置文件内容初始化集群信息。启动过程如图10-6所示。

图10-6 Redis集群模式启动过程

集群模式的Redis除了原有的配置文件之外又加了一份集群配置文件。当集群内节点信息发生变化,如添加节点、节点下线、故障转移等。节点会自动保存集群状态到配置文件中。需要注意的是,Redis自动维护集群配置文件,不要手动修改,防止节点重启时产生集群信息错乱。

下面是6379节点的集群配置信息:

1f77d3d4c68a0e376ded8aa0de932a9342d7192f :6379@16379 myself,master - 0 0 0 connected 517 4768 8899 13026
vars currentEpoch 0 lastVoteEpoch 0

那一串长长的ID号是节点ID,用于唯一标识集群内的一个节点。不同于Redis的运行ID,节点ID只有在初始化的时候创建一次,后面会继续复用这个ID的,可用通过cluster nodes命令去查看节点的状态:

127.0.0.1:6380> CLUSTER nodes 
42f6c0516dfb6307281119cd10831dd1f8176dd4 :6380@16380 myself,master - 0 0 0 connected 517 4768 8899 13026

这时候,每个节点只知道自己的存在,还不知道其他几个节点的存在,我们需要通过握手让6个节点彼此建立连接从而组成一个集群。

10.2.2 握手

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

图10-7 节点握手

图中执行的命令是:cluster meet 127.0.0.1 6380让节点6379和6380节点进行握手通信。cluster meet命令是一个异步命令,执行之后立刻返回。内部发起与目标节点进行握手通信。

  1. 节点6379本地创建6380节点信息对象,并发送meet消息。
  2. 节点6380接受到meet消息后,保存6379节点信息并回复pong消息。
  3. 之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信。

这里的meet、ping、pong消息是Gossip协议通信的载体,之后的节点通信部分做进一步介绍,它的主要作用是节点彼此交换状态数据信息。

下面我们可以通过cluster meet命令,让这几个节点加入到集群中:

127.0.0.1:6380> CLUSTER MEET 127.0.0.1 6379
127.0.0.1:6380> CLUSTER MEET 127.0.0.1 6381
127.0.0.1:6380> CLUSTER MEET 127.0.0.1 6382
127.0.0.1:6380> CLUSTER MEET 127.0.0.1 6383
127.0.0.1:6380> CLUSTER MEET 127.0.0.1 6384

经过一小段时间后,这6个节点都能彼此感知并组成集群,可以通过cluster nodes命令确认:

虽然,这几个节点已经组成了集群,但是暂时还是不能工作的,处于下线的状态,可以通过cluster info查看redis的集群状态:

127.0.0.1:6379> CLUSTER info
cluster_state:fail
cluster_slots_assigned:4
cluster_slots_ok:4
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
...

这个时候,被分配的槽数目为4(cluster_slots_assigned),只有当全部的16364个槽全部分配给节点之后,集群才能进入在线状态。

10.2.3 分配槽

Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过cluster addslots命令为节点分配槽。这里利用bash特性批量设置槽(slots),命令如下:

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个slot平均分配给6379、6380、6381三个节点。执行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
...

当前集群状态是OK,集群进入在线状态。所有的槽都已经分配给节点,执行cluster nodes命令可以看到节点和槽的分配关系.

目前还有三个节点没有使用,作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。集群模式下,Reids节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。使用cluster replicate {nodeId}命令让一个节点成为从节点。其中命令执行必须在对应的从节点上执行,nodeId是要复制主节点的节点ID,命令如下:

127.0.0.1:6382> CLUSTER REPLICATE 1f77d3d4c68a0e376ded8aa0de932a9342d7192f
127.0.0.1:6383> CLUSTER REPLICATE 42f6c0516dfb6307281119cd10831dd1f8176dd4
127.0.0.1:6384> CLUSTER REPLICATE f6ae5363edc7522601bc3904194f4fcf7e174458

一顿操作过后,整个集群的结构如下图所示:

集群完整结构

目前为止,我们依照Redis协议手动建立一个集群。它由6个节点构成,3个主节点负责处理槽和相关数据,3个从节点负责故障转移。手动搭建集群便于理解集群建立的流程和细节,不过读者也从中发现集群搭建需要很多步骤,当集群节点众多时,必然会加大搭建集群的复杂度和运维成本。因此Redis官方提供了redis-trib.rb工具方便我们快速搭建集群。

注:Redis 5.0已经不需要使用redis-trib.rb来搭建集群了,有很简单的办法,自行百度。

10.2.4 用redis-trib.rb搭建集群

redis-trib.rb是采用Ruby实现的Redis集群管理工具。内部通过Cluster相关命令帮我们简化集群创建、检查、槽迁移和均衡等常见运维操作,使用之前需要安装Ruby依赖环境。下面介绍搭建集群的详细步骤。

1. Ruby环境准备

书本是用编译安装ruby,现在官网上最新的稳定版本是2.6.3,建议是用RVM去安装的。下面我们也使用RVM的方法去安装Ruby。

#GPG keys
gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB

#安装RVM
\curl -sSL https://get.rvm.io | bash -s stable

#按照提示信息,把用户添加到rvm组并使得rvm生效
usermod -G rvm root
exit
su - root
source /etc/profile.d/rvm.sh

#查看ruby的版本
rvm list known 

#安装2.6.3的版本
rvm install 2.6.3

到这里,Ruby的安装环境已经准备好了。之所以不用yum来安装ruby是因为yum源上的ruby版本太低了,而我们需要使用到的 gem4.1.2 (我实验用过的是redis4.1.14)要求ruby的版本是大于2.3.0的。

安装rubygem redis依赖:

wget https://rubygems.org/downloads/redis-4.1.2.gem
gem install -l redis-4.1.2.gem

安装redis-trib.rb:

cp /{redis_home}/src/redis-trib.rb /usr/local/bin

安装完Ruby环境后,执行redis-trib.rb命令确认环境是否正确,输出如下:

[root@qcloud01 software]# redis-trib.rb 
Usage: redis-trib   

  create          host1:port1 ... hostN:portN
                  --replicas 
  check           host:port
  info            host:port
  fix             host:port
                  --timeout 
  reshard         host:port
...

从redis-trib.rb的提示信息可以看出,它提供了集群创建、检查、修复、均衡等命令行工具。这里我们关注集群创建命令,使用redis-trib.rb create命令可快速搭建集群。

2. 准备节点

首先我们跟之前内容一样准备好节点配置并启动,或者用cluster-reset重置我们前面手动配置好的节点也行。

redis-cli -p 6379 cluster reset 
redis-cli -p 6380 cluster reset
...
redis-cli -p 6384 cluster reset

3. 创建集群

启动好6个节点之后,使用redis-trib.rb create命令完成节点握手和槽分配过程,命令如下:

redis-trib.rb create --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

--replicas参数指定集群中每个主节点配备几个从节点,这里设置为1。我们出于测试目的使用本地IP地址127.0.0.1,如果部署节点使用不同的IP地址,redis-trib.rb会尽可能保证主从节点不分配在同一机器下,因此会重新排序节点列表顺序。节点列表顺序用于确定主从角色,先主节点之后是从节点。创建过程中首先会给出主从节点角色分配的计划,如下所示。

当我们同意这份计划之后输入yes,redis-trib.rb开始执行节点握手和槽
分配操作,输出如下:

最后的输出报告说明:16384个槽全部被分配,集群创建成功。这里需要注意给redis-trib.rb的节点地址必须是不包含任何槽/数据的节点,否则会拒绝创建集群。

4. 集群完整性检查

集群完整性指所有的槽都分配到存活的主节点上,只要16384个槽中有一个没有分配给节点则表示集群不完整。可以使用redis-trib.rb check命令检测,check命令只需要给出集群中任意一个节点地址就可以完成整个集群的检查工作,命令如下:

redis-trib.rb check 127.0.0.1:6379
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

10.3 节点通信

10.3.1 通信流程

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

图10-12 节点彼此传播消息

通信过程说明:

  1. 集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000。
  2. 每个节点在固定周期内通过特定规则选择几个节点发送ping消息。
  3. 接收到ping消息的节点用pong消息作为响应。

集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。

10.3.2 Gossip消息

Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息,了解这些消息有助于我们理解集群如何完成信息交换。
常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等,它们的通信模式如图10-13所示。

图10-13 不同消息通信模式
  • meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据。
  • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。具体细节将在后面10.6节“故障转移”中说明。

10.3.3 节点选择

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

图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,这块空间占用相对固定。消息体会携带一定数量的其他节点信息用于信息交换。

10.4 集群伸缩

10.4.1 伸缩原理

Redis集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动。如果希望加入1个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点即可。

10.4.2 扩容集群

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

1. 准备新节点

建议新节点的配置与旧的节点一致,比如我新增一组的节点端口为 6385、6386

redis-server redis_6385.conf
redis-server redis_6386.conf

2. 加入集群

新节点依旧使用cluster meet 命令加入到现有的集群中。在集群内任意节点
执行cluster meet命令让6385节点加入进来,命令如下:

127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6385
127.0.0.1:6379> CLUSTER MEET 127.0.0.1 6386

我们可以通过cluster nodes命令去查看redis的节点状态,新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。对于新节点的后续操作我们一般有两种选择:

  • 为它迁移槽和数据实现扩容。
  • 作为其他主节点的从节点负责故障转移。

除了使用cluster meet添加节点之外,我们还可以使用 redis-trib.rb add-node 命令去添加。生产环境下,建议使用redis-trib.rb工具,该命令内部会执行新节点状态检查,如果新节点已经加入其他集群或者包含数据,则放弃集群加入操作并打印对应的信息。使用redis-trib.rb 的命令如下:

redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379
redis-trib.rb add-node 127.0.0.1:6386 127.0.0.1:6379

3. 迁移槽和数据

加入集群后需要为新节点迁移槽和相关数据,槽在迁移过程中集群可以正常提供读写服务,迁移过程是集群扩容最核心的环节,下面详细讲解。
(1)槽迁移计划
槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。比如原本我们的集群是由三组节点的,现在增加到4组,那每个节电的槽数量大概由原本的5460个变为4096个。

(2)迁移数据
数据迁移过程是逐个槽进行的,每个槽数据迁移的流程下:

  1. 对目标节点发送cluster setslot {slot} importing {sourceNodeId} 命令,让目标节点准备导入槽的数据。
  2. 对源节点发送cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备迁出槽的数据。
  3. 源节点循环执行cluster getkey sinslot {slot} {count}命令,获取count个属于槽{slot}的键。
  4. 在源节点上执行migrate {targetIp} {targetPort} ""0 {timeout} keys {keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
  5. 重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点。
  6. 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。
图10-23 槽和数据迁移流程

手工去执行迁移槽的操作步骤很多,使用 redis-trib.rb 工具可以大大简化我们的工作。下面来演示使用 redis-trib.rb 的reshard命令来进行数据迁移的工作:

redis-trib.rb reshard 127.0.0.1:6379

打印出集群每个节点信息后,reshard命令需要确认迁移的槽数量,这里
我们输入4096个:

How many slots do you want to move (from 1 to 16384)4096

输入6385的节点ID作为目标节点,目标节点只能指定一个:

What is the receiving node ID 8d82d2dc760abe2cc40f6271777d5135b7c21c1b

然后我们输入 all ,源节点为全部节点,最后需要我们再次确认计划,这时候我们再输入yes 就会执行迁移工作了。

......
Moving slot 12285 from 127.0.0.1:6381 to 127.0.0.1:6385: 
Moving slot 12286 from 127.0.0.1:6381 to 127.0.0.1:6385: 
Moving slot 12287 from 127.0.0.1:6381 to 127.0.0.1:6385: 

当所有的槽迁移完成后,reshard命令自动退出,执行cluster nodes命令检查节点和槽映射的变化,如下所示:

[pangcm@qcloud01 ~]$ redis-cli 
127.0.0.1:6379> CLUSTER NODES
....
b301fda1740bea141c71206c2fbceadd32373889 127.0.0.1:6386@16386 master - 0 1566313650498 0 connected
1f77d3d4c68a0e376ded8aa0de932a9342d7192f 127.0.0.1:6379@16379 myself,master - 0 1566313649000 1 connected 1365-5460
8d82d2dc760abe2cc40f6271777d5135b7c21c1b 127.0.0.1:6385@16385 master - 0 1566313649000 7 connected 0-1364 5461-6826 10923-12287
....

节点6385负责的槽变为:0-1364 5461-6826 10923-12287。由于槽用于hash运算本身顺序没有意义,因此无须强制要求节点负责槽的顺序性。迁移之后建议使用redis-trib.rb rebalance命令检查节点之间槽的均衡性。命令如下:

redis-trib.rb rebalance 127.0.0.1:6380
>>> Performing Cluster Check (using node 127.0.0.1:6380)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
*** No rebalancing needed! All nodes are within the 2.0% threshold.

可以看出迁移之后所有主节点负责的槽数量差异在2%以内,因此集群节点数据相对均匀,无需调整。

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

127.0.0.1:6386> CLUSTER REPLICATE 8d82d2dc760abe2cc40f6271777d5135b7c21c1b

从节点内部除了对主节点发起全量复制之外,还需要更新本地节点的集群相关状态,查看节点6386状态确认已经变成6385节点的从节点:

b301fda1740bea141c71206c2fbceadd32373889 127.0.0.1:6386@16386 myself,slave 8d82d2dc760abe2cc40f6271777d5135b7c21c1b 0 1566314314000 0 connected
...

到此整个集群扩容完成,集群关系结构如图10-24所示。

图10-24 扩容后集群结构

10.4.3 收缩集群

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

图10-25 节点安全下线流程

流程说明:

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

1. 下线迁移槽

下线节点需要把自己负责的槽迁移到其他节点,原理与之前节点扩容的迁移槽过程一致。下面演示把上面新增的节点进行收缩,也就是把6385节点上的槽均匀地转移回到原本的三个节点上。这里
直接使用redis-trib.rb reshard命令完成槽迁移。由于每次执行reshard命令只能
有一个目标节点,因此需要执行3次reshard命令,分别迁移1365、1365、1366个槽,如下所示:

redis-trib.rb reshard 127.0.0.1:6385
#redis-trib.rb reshard 127.0.0.1:6381
>>> Performing Cluster Check (using node 127.0.0.1:6385)
...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)1365
What is the receiving node ID 1f77d3d4c68a0e376ded8aa0de932a9342d7192f/*输入6379
节点id作为目标节点.*/
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1:8d82d2dc760abe2cc40f6271777d5135b7c21c1b/*源节点6385 id*/
Source node #2:done /* 输入done确认 */
...
Do you want to proceed with the proposed reshard plan (yes/no) yes

就这样继续把其他的槽都转移到另外的两个节点上,最后6385不再负责任何槽。

下线节点槽迁出完成后,剩下的步骤需要让集群忘记该节点。

2. 忘记节点

由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。也就是说让其他节点不再与要下线节点进行Gossip消息交换。Redis提供了cluster forget {downNodeId}命令实现该功能。
当节点接收到cluster forget {down NodeId}命令后,会把nodeId指定的节点加入到禁用列表中,在禁用列表内的节点不再发送Gossip消息。禁用列表有效期是60秒,超过60秒节点会再次参与消息交换。也就是说当第一次forget命令发出后,我们有60秒的时间让集群内的所有节点忘记下线节点。
线上操作不建议直接使用cluster forget命令下线节点,需要跟大量节点命令交互,实际操作起来过于繁琐并且容易遗漏forget节点。建议使用redistrib.rb del-node{ host:port} {downNodeId} 命令。操作如下:

redis-trib.rb del-node 127.0.0.1:6385 8d82d2dc760abe2cc40f6271777d5135b7c21c1b
redis-trib.rb del-node 127.0.0.1:6386 b301fda1740bea141c71206c2fbceadd32373889

节点下线后确认节点状态:

没有6385和6386节点

10.5 请求路由

目前我们已经搭建好Redis集群并且理解了通信和伸缩细节,但还没有使用客户端去操作集群。Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。因此对于希望从单机切换到集群环境的应用需要修改客户端代码。本节我们关注集群请求路由的细节,以及客户端如何高效地操作集群。

10.5.1 请求重定向

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

MOVED重定向执行流程

下面我在之前搭建的集群上执行如下命令:

127.0.0.1:6379> set key1 test1
(error) MOVED 9189 127.0.0.1:6380

这时候就出现了move重定向,负责键key1的槽是在6380节点上。我们可以通过cluster keyslot {key}命令返回key所对应的槽。

127.0.0.1:6379> CLUSTER KEYSLOT key1
(integer) 9189

这个槽是在6380节点上,我们在6380上执行失败的语句看看

127.0.0.1:6380> set key1 test1
OK

使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作,如下所示:

# redis-cli -c 
127.0.0.1:6379> set key4 test4
-> Redirected to slot [13120] located at 127.0.0.1:6381
OK

redis-cli自动帮我们连接到正确的节点执行命令,这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发,如图10-30所示.

图10-30 客户端完成请求转发

键命令执行步骤主要分两步:计算槽,查找槽所对应的节点。

根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端。

10.5.2 Smart 客户端

1. smart客户端的原理

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

10.5.3 ASK重定向

1. 客户端ASK重定向流程

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

当出现上述情况时,客户端键命令执行流程将发生变化,如下所示:

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

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

2. 节点内部处理

为了支持ASK重定向,源节点和目标节点在内部的clusterState结构中维护当前正在迁移的槽信息,用于识别槽迁移情况。

集群环境下对于使用批量操作的场景,建议优先使用Pipeline方式,在客户端实现对ASK重定向的正确处理,这样既可以受益于批量操作的IO优化,又可以兼容slot迁移场景。

10.6 故障转移

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

10.6.1 故障发现

当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节点是否发生了故障。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线(pfail)和客观下线(fail)。

  • 主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
  • 客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。

1. 主观下线

集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
Redis集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障。对于一个健壮的故障发现机制,需要集群内大多数节点都判断某节点故障时,才能认为该节点确实发生故障,然后为其进行故障转移。而这种多个节点协作完成故障发现的过程叫做客观下线。

2. 客观下线

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

假设节点a标记节点b为主观下线,一段时间后节点a通过消息把节点b的状态发送到其他节点,当节点c接受到消息并解析出消息体含有节点b的pfail状态时,会触发客观下线流程。

图10-36 客观下线逻辑流程

下线报告的有效期限是server.cluster_node_timeout*2,主要是针对故障误报的情况。例如节点A在上一小时报告节点B主观下线,但是之后又恢复正常。现在又有其他节点上报节点B主观下线,根据实际情况之前的属于误报不能被使用。

如果在cluster-node-time*2时间内无法收集到一半以上槽节点的下线报告,那么之前的下线报告将会过期,也就是说主观下线上报的速度追赶不上下线报告过期的速度,那么故障节点将永远无法被标记为客观下线从而导致故障转移失败。因此不建议将cluster-node-time设置得过小。

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

图10-37 尝试客观下线流程

这里要特别说明最后一步,也就是广播fail消息,它承担着非常重要的职责:

  • 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。
  • 通知故障节点的从节点触发故障转移流程。
    需要理解的是,尽管存在广播fail消息机制,但是集群所有节点知道故障节点进入客观下线状态是不确定的。比如当出现网络分区时有可能集群被分割为一大一小两个独立集群中。大的集群持有半数槽节点可以完成客观下线并广播fail消息,但是小集群无法接收到fail消息。
    但是当网络恢复后,只要故障节点变为客观下线,最终总会通过Gossip消息传播至集群的所有节点。

网络分区会导致分割后的小集群无法收到大集群的fail消息,因此如果故障节点所有的从节点都在小集群内将导致无法完成后续故障转移,因此部署主从结构时需要根据自身机房/机架拓扑结构,降低主从被分区的可能性。

10.6.2 故障恢复

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

故障恢复流程

1. 资格检查

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

2. 准备选举时间

当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。

3. 发起选举

当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:
更新配置纪元-->广播选举消息

4. 选举投票

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

从节点slave b-1成功获得3张选票

故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到3/2+1个主节点选票将导致故障转移失败。这个问题也适用于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。

每个配置纪元代表了一次选举周期,如果在开始投票之后的cluster-node-timeout*2时间内从节点没有获取足够数量的投票,则本次选举作废。从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。

5. 替换主节点

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

10.6.3 故障转移时间

在介绍完故障发现和恢复的流程后,这时我们可以估算出故障转移时间:

  1. 主观下线(pfail)识别时间=cluster-node-timeout。
  2. 主观下线状态消息传播时间<=cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。
  3. 从节点转移时间<=1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

因此,故障转移时间跟cluster-node-timeout参数息息相关,默认15秒。配置时可以根据业务容忍度做出适当调整,但不是越小越好,下一节的带宽消耗部分会进一步说明。

10.6.4 故障转移演练

到目前为止介绍了故障转移的主要细节,下面通过之前搭建的集群模拟主节点故障场景,对故障转移行为进行分析。使用kill-9强制关闭主节点6381进程,正常情况下它的从节点6382应该会接替它成为新的主节点。

确认节点状态

节点状态

模拟6381故障

ps aux |grep redis-server|grep 6381
kill -9 

日志分析

  • 从节点与主节点的复制中断了:
10623:S 21 Aug 14:27:17.072 # Connection with master lost.
10623:S 21 Aug 14:27:17.077 * Caching the disconnected master state.
10623:S 21 Aug 14:27:17.988 * Connecting to MASTER 127.0.0.1:6381
10623:S 21 Aug 14:27:17.988 * MASTER <-> SLAVE sync started
10623:S 21 Aug 14:27:17.988 # Error condition on socket for SYNC: Connection refused
......
  • 6379和6380两个主节点标记6381主观下线,超过半数因此标记为客观下线状态。这里还可以看到在这两个主节点还没有主观下线故障节点的时候就收到了来自丛节点选举的消息了,这里直接拒绝选举。
##6379
27690:M 21 Aug 14:27:34.751 # Failover auth denied to 8949246cdf77c3abcaeb2a259ec701e6a00e2ce9: its master is up
27690:M 21 Aug 14:27:39.616 * Marking node f6ae5363edc7522601bc3904194f4fcf7e174458 as failing (quorum reached).
##6380
27715:M 21 Aug 14:27:34.751 # Failover auth denied to 8949246cdf77c3abcaeb2a259ec701e6a00e2ce9: its master is up
27715:M 21 Aug 14:27:39.620 * Marking node f6ae5363edc7522601bc3904194f4fcf7e174458 as failing (quorum reached).
  • 从节点识别正在复制的主节点进入客观下线后准备选举时间,日志打印了选举延迟756毫秒之后执行,并打印当前从节点复制偏移量。
##6382
10623:S 21 Aug 14:27:33.937 # Start of election delayed for 756 milliseconds (rank #0, offset 132063)
  • 延迟选举时间到达后,从节点更新配置纪元并发起故障选举。
##6382
10623:S 21 Aug 14:27:34.740 # Starting a failover election for epoch 11
  • 6379和6380主节点为从节点6382投票,日志如下:
##6379
27690:M 21 Aug 14:28:35.661 # Failover auth granted to 8949246cdf77c3abcaeb2a259ec701e6a00e2ce9 for epoch 12
##6380
27715:M 21 Aug 14:28:35.662 # Failover auth granted to 8949246cdf77c3abcaeb2a259ec701e6a00e2ce9 for epoch 12
  • 从节点获取2个主节点投票之后,超过半数执行替换主节点操作,从而完成故障转移:
##6382
10623:S 21 Aug 14:28:35.682 # Failover election won: I'm the new master.
10623:S 21 Aug 14:28:35.683 # configEpoch set to 12 after successful failover

成功完成故障转移之后,我们对已经出现故障节点6381进行恢复,观察节点状态是否正确。这里就不观察日志了,无外乎就是6381变成了6382的从节点,并且其他主节点清空了对其的下线状态,最后从节点从新的主节点那里发起复制。

6381成为了6382的从节点

10.7 集群运维

Redis集群由于自身的分布式特性,相比单机场景在开发和运维方面存在一些差异。本节我们关注于常见的问题进行分析定位。

10.7.1 集群完整性

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

10.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)在满足业务需要的情况下尽量避免大集群
2)适度提高cluster-node-timeout降低消息发送频率
3)如果条件允许集群尽量均匀部署在更多机器上

10.7.3 Pub/Sub广播问题

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

图10-44 publish命令在集群内广播

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

10.7.4 集群倾斜

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

1. 数据倾斜

数据倾斜主要分为以下几种:

  • 节点和槽分配严重不均。
    可以使用redis-trib.rb info{host:ip}进行定位,使用redis-trib.rb rebalance命令进行平衡。
  • 不同槽对应键数量差异过大。
    通过命令:cluster countkeysinslot{slot}可以获取槽对应的键数量,识别出哪些槽映射了过多的键。再通过命令clustergetkeysinslot{slot}{count}循环迭代出槽下所有的键。从而发现过度使用hash_tag的键。
  • 集合对象包含大量元素。
    对于大集合对象的识别可以使用redis-cli--bigkeys命令识别,找出大集合之后可以根据业务场景进行拆分。同时集群槽数据迁移是对键执行migrate操作完成,过大的键集合如几百兆,容易造成migrate命令超时导致数据迁移失败。
  • 内存相关配置不一致。
    内存相关配置指hash-max-ziplist-value、setmax-intset-entries等压缩数据结构配置。

2. 请求倾斜

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

10.7.5 集群读写分离

1. 只读连接

集群模式下从节点不接受任何读写请求,发送过来的键命令会重定向到负责槽的主节点上(其中包括它的主节点)。当需要使用从节点分担主节点读压力时,可以使用readonly命令打开客户端连接只读状态。之前的复制配置slave-read-only在集群模式下无效。当开启只读状态时,从节点接收读命令处理流程变为:如果对应的槽属于自己正在复制的主节点则直接执行读命令,否则返回重定向信息。

readonly命令是连接级别生效,因此每次新建连接时都需要执行readonly开启只读状态。执行readwrite命令可以关闭连接只读状态。

2. 读写分离

集群模式下的读写分离,同样会遇到:复制延迟,读取过期数据,从节点故障等问题,具体细节见6.5复制运维小节。针对从节点故障问题,客户端需要维护可用节点列表,集群提供了cluster slaves{nodeId}命令,返回nodeId对应主节点下所有从节点信息,数据格式同cluster nodes。

集群模式下读写分离涉及对客户端修改如下:
1)维护每个主节点可用从节点列表。
2)针对读命令维护请求节点路由。
3)从节点新建连接开启readonly状态。
集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离。
集群读写分离有时用于特殊业务场景如:
1)利用复制的最终一致性使用多个从节点做跨机房部署降低读命令网络延迟。
2)主节点故障转移时间过长,业务端把读请求路由给从节点保证读操作可用。

以上场景也可以在不同机房独立部署Redis集群解决,通过客户端多写来维护,读命令直接请求到最近机房的Redis集群,或者当一个集群节点故障时客户端转向另一个集群。

10.7.6 手动故障转移

Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为它的从节点。
在从节点上执行cluster failover命令发起转移流程,默认情况下转移期间客户端请求会有短暂的阻塞,但不会丢失数据,流程和前面介绍的模拟故障基本一样。

手动故障转移的应用场景主要如下:
1)主节点迁移:运维Redis集群过程中经常遇到调整节点部署的问题,如节点所在的老机器替换到新机器等。由于从节点默认不响应请求可以安全下线关闭,但直接下线主节点会导致故障自动转移期间主节点无法对外提供服务,影响线上业务的稳定性。这时可以使用手动故障转移,把要下线的主节点安全的替换为从节点后,再做下线操作操作。
2)强制故障转移。当自动故障转移失败时,只要故障的主节点有存活的从节点就可以通过手动转移故障强制让从节点替换故障的主节点,保证集群的可用性。

10.7.7 数据迁移

应用Redis集群时,常需要把单机Redis数据迁移到集群环境。redistrib.rb工具提供了导入功能,用于数据从单机向集群环境迁移的场景,命令如下:

redis-trib.rb import host:port --from  --copy --replace

redis-trib.rb import命令内部采用批量scan和migrate的方式迁移数据。这种迁移方式存在以下缺点:
1)迁移只能从单机节点向集群环境导入数据。
2)不支持在线迁移数据,迁移数据时应用方必须停写,无法平滑迁移数据。
3)迁移过程中途如果出现超时等错误,不支持断点续传只能重新全量导入。
4)使用单线程进行数据迁移,大数据量迁移速度过慢。

正因为这些问题,社区开源了很多迁移工具,这里推荐一款唯品会开发的redis-migrate-tool,该工具可满足大多数Redis迁移需求,特点如下:

  • 支持单机、Twemproxy、Redis Cluster、RDB/AOF等多种类型的数据迁移。
  • 工具模拟成从节点基于复制流迁移数据,从而支持在线迁移数据,业务方不需要停写。
  • 采用多线程加速数据迁移过程且提供数据校验和查看迁移状态等功能。
    更多细节见GitHub:https://github.com/vipshop/redis-migrate-tool。

你可能感兴趣的:(第十章 集群)