错过血亏!深入学习Redis集群搭建方案及实现原理

  1. 云栖社区>
  2. 数据和云>
  3. 博客>
  4. 正文

错过血亏!深入学习Redis集群搭建方案及实现原理

技术小能手 2018-11-07 15:04:07 浏览1363
展开阅读全文

本文将详细介绍集群,主要内容包括:

 ●  集群的作用
 ●  集群的搭建方法及设计方案
 ●  集群的基本原理
 ●  客户端访问集群的方法

 ●  实践须知(集群伸缩、故障转移、参数优化等)

集群的作用

集群,即 Redis Cluster,是 Redis 3.0 开始引入的分布式存储方案。集群由多个节点(Node)组成,Redis 的数据分布在这些节点中。

集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。

集群的作用,可以归纳为两点:

数据分区

数据分区(或称数据分片)是集群最核心的功能。集群将数据分散到多个节点:

 ●  一方面突破了 Redis 单机内存大小的限制,存储容量大大增加。
 ●  另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。

Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及。

例如,如果单机内存太大,bgsave 和 bgrewriteaof 的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出。

高可用

集群支持主从复制和主节点的自动故障转移(与哨兵类似),当任一节点发生故障时,集群仍然可以对外提供服务。本文内容基于 Redis 3.0.6。

集群的搭建

我们将搭建一个简单的集群:共 6 个节点,3 主 3 从。方便起见,所有节点在同一台服务器上,以端口号进行区分,配置从简。

3个主节点端口号:7000/7001/7002;对应的从节点端口号:8000/8001/8002。

集群的搭建有两种方式:

 ●  手动执行 Redis 命令,一步步完成搭建
 ●  使用 Ruby 脚本搭建

两者搭建的原理是一样的,只是 Ruby 脚本将 Redis 命令进行了打包封装;在实际应用中推荐使用脚本方式,简单快捷不容易出错。下面分别介绍这两种方式。

执行 Redis 命令搭建集群

集群的搭建可以分为四步:

 ●  启动节点:将节点以集群模式启动,此时节点是独立的,并没有建立联系。
 ●  节点握手:让独立的节点连成一个网络。
 ●  分配槽:将 16384 个槽分配给主节点。
 ●  指定主从关系:为从节点指定主节点。

实际上,前三步完成后集群便可以对外提供服务;但指定从节点后,集群才能够提供真正高可用的服务。

启动节点

集群节点的启动仍然是使用 redis-server 命令,但需要使用集群模式启动。

下面是 7000 节点的配置文件(只列出了节点正常工作关键配置,其他配置,如开启 AOF,可以参照单机节点进行):


#redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file "node-7000.conf"
logfile "log-7000.log"
dbfilename "dump-7000.rdb"
daemonize yes

其中的 cluster-enabled 和 cluster-config-file 是与集群相关的配置。

cluster-enabledyes:Redis 实例可以分为单机模式(standalone)和集群模式(cluster);cluster-enabledyes 可以启动集群模式。

在单机模式下启动的 Redis 实例,如果执行 info server 命令,可以发现 redis_mode 一项为 standalone,如下图所示:

4562aac62a4b8453bdd0ebe1978d195c1112f0e8

集群模式下的节点,其 redis_mode 为 cluster,如下图所示:

b26a2143caf584327ac521459431d21ab34b6fcc

cluster-config-file:该参数指定了集群配置文件的位置。每个节点在运行过程中,会维护一份集群配置文件。

每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件。

当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的重新加入到集群中。

也就是说,当 Redis 节点以集群模式启动时,会首先寻找是否有集群配置文件。

如果有则使用文件中的配置启动;如果没有,则初始化配置并将配置保存到文件中。集群配置文件由 Redis 节点维护,不需要人工修改。

编辑好配置文件后,使用 redis-server 命令启动该节点:

redis-server redis-7000.conf

节点启动以后,通过 cluster nodes 命令可以查看节点的情况,如下图所示:

ed902ed09cc8c26975860e2129a244150c4ac57c

其中返回值第一项表示节点 id,由 40 个 16 进制字符串组成,节点 id 与主从复制一文中提到的 runId 不同。

Redis 每次启动 runId 都会重新创建,但是节点 id 只在集群初始化时创建一次,然后保存到集群配置文件中,以后节点重新启动时会直接在集群配置文件中读取。

其他节点使用相同办法启动,不再赘述。需要特别注意,在启动节点阶段,节点是没有主从关系的,因此从节点不需要加 slaveof 配置。

节点握手

节点启动以后是相互独立的,并不知道其他节点存在;需要进行节点握手,将独立的节点组成一个网络。

节点握手使用 cluster meet {ip} {port} 命令实现,例如在 7000 节点中执行 clustermeet 192.168.72.128 7001,可以完成 7000 节点和 7001 节点的握手。

注意:ip 使用的是局域网 ip,而不是 localhost 或 127.0.0.1,是为了其他机器上的节点或客户端也可以访问。

此时再使用 cluster nodes 查看:

611dee36b57c128d7aaf0ee8830fe6f8d4ba290d

在 7001 节点下也可以类似查看:

08ab319bf28df0752d9f8b20e4e0ba8bccb96882

同理,在 7000 节点中使用 cluster meet 命令,可以将所有节点加入到集群,完成节点握手:


cluster meet 192.168.72.128 7002
cluster meet 192.168.72.128 8000
cluster meet 192.168.72.128 8001
cluster meet 192.168.72.128 8002

执行完上述命令后,可以看到 7000 节点已经感知到了所有其他节点:

d978bc575fa525f035371cc58b6eff798104ca75

通过节点之间的通信,每个节点都可以感知到所有其他节点,以 8000 节点为例:

e3bd669bb9d533727512cc1251f504852961a6b9

分配槽

在 Redis 集群中,借助槽实现数据分区,具体原理后文会介绍。集群有 16384 个槽,槽是数据管理和迁移的基本单位。

当数据库中的 16384 个槽都分配了节点时,集群处于上线状态(ok);如果有任意一个槽没有分配节点,则集群处于下线状态(fail)。

cluster info 命令可以查看集群状态,分配槽之前状态为 fail:

671d48017d749be3ad86ab2bd5e22c4b8a2edc06

分配槽使用 cluster addslots 命令,执行下面的命令将槽(编号 0-16383)全部分配完毕:


redis-cli -p 7000 cluster addslots {0..5461}
redis-cli -p 7001 cluster addslots {5462..10922}
redis-cli -p 7002 cluster addslots {10923..16383}

此时查看集群状态,显示所有槽分配完毕,集群进入上线状态:

40d79c2396863fa60e939456a01020696cff7770

指定主从关系

集群中指定主从关系不再使用 slaveof 命令,而是使用 cluster replicate 命令;参数使用节点 id。

通过 cluster nodes 获得几个主节点的节点 id 后,执行下面的命令为每个从节点指定主节点:


redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

此时执行 cluster nodes 查看各个节点的状态,可以看到主从关系已经建立:

e2b8dbf0089be05da64df3d90e8a59badc793053

至此,集群搭建完毕。

使用 Ruby 脚本搭建集群

在 {REDIS_HOME}/src 目录下可以看到 redis-trib.rb 文件,这是一个 Ruby 脚本,可以实现自动化的集群搭建。

①安装 Ruby 环境

以 Ubuntu 为例,如下操作即可安装 Ruby 环境:

 ●  apt-get install ruby # 安装 Ruby 环境。
 ●  gem install redis #gem 是 Ruby 的包管理工具,该命令可以安装 ruby-redis 依赖。

②启动节点

与第一种方法中的“启动节点”完全相同。

③搭建集群

redis-trib.rb 脚本提供了众多命令,其中 create 用于搭建集群,使用方法如下:

./redis-trib.rb create --replicas 1 192.168.72.128:7000192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001192.168.72.128:8002

其中:--replicas=1 表示每个主节点有 1 个从节点;后面的多个 {ip:port} 表示节点地址,前面的做主节点,后面的做从节点。使用 redis-trib.rb 搭建集群时,要求节点不能包含任何槽和数据。

执行创建命令后,脚本会给出创建集群的计划,如下图所示;计划包括哪些是主节点,哪些是从节点,以及如何分配槽。

65134c00b9bfa768b4deb8b74a14d7f72c2eeb4f

输入 yes 确认执行计划,脚本便开始按照计划执行,如下图所示:

8aeb70d620646fd3a5addf51cd0080075e0d0471

至此,集群搭建完毕。

集群方案设计

设计集群方案时,至少要考虑以下因素:

 ●  高可用要求:根据故障转移的原理,至少需要 3 个主节点才能完成故障转移,且 3 个主节点不应在同一台物理机上。
每个主节点至少需要 1 个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含 6 个节点。
 ●  数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过 benchmark 得到较准确估计),计算需要的主节点数量。
 ●  节点数量限制:Redis 官方给出的节点数量限制为 1000,主要是考虑节点间通信带来的消耗。
在实际应用中应尽量避免大集群,如果节点数量不足以满足应用对 Redis 数据量和访问量的要求,可以考虑:①业务分割,大集群分为多个小集群;②减少不必要的数据;③调整数据过期策略等。

 ●  适度冗余:Redis 可以在不影响集群服务的情况下增加节点,因此节点数量适当冗余即可,不用太大。

集群的基本原理

上面介绍了集群的搭建方法和设计方案,下面将进一步深入,介绍集群的原理。

集群最核心的功能是数据分区,因此:

 ●  首先介绍数据的分区规则。
 ●  然后介绍集群实现的细节:通信机制和数据结构。

 ●  最后以 cluster meet(节点握手)、cluster addslots(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。

数据分区方案

数据分区有顺序分区、哈希分区等,其中哈希分区由于其天然的随机性,使用广泛;集群的分区方案便是哈希分区的一种。

哈希分区的基本思路是:对数据的特征值(如 key)进行哈希,然后根据哈希值决定数据落在哪个节点。

常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。

衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是:

 ●  数据分布是否均匀。
 ●  增加或删减节点对数据分布的影响。

由于哈希的随机性,哈希分区基本可以保证数据分布均匀;因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。

哈希取余分区

哈希取余分区思路非常简单:计算 key 的 hash 值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。

该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。

一致性哈希分区

一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为 0-2^32-1。

1a3c6475f910665b319eac1df4f8f6da500a1f69

对于每个数据,根据 key 计算 hash 值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。

与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。

以上图为例,如果在 node1 和 node2 之间增加 node5,则只有 node2 中的一部分数据会迁移到 node5;如果去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。

一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。

还是以上图为例,如果去掉 node2,node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其他节点相比负载过高。

带虚拟节点的一致性哈希分区

该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis 集群使用的便是该方案,其中的虚拟节点称为槽(slot)。

槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。

引入槽以后,数据的映射关系由数据 hash->实际节点,变成了数据 hash->槽->实际节点。

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。

仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);槽 0-3 位于 node1,4-7 位于 node2,以此类推。

如果此时删除 node2,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;可以看出删除 node2 后,数据在其他节点的分布仍然较为均衡。

槽的数量一般远小于 2^32,远大于实际节点的数量;在 Redis 集群中,槽的数量为 16384。

883c0c3eea4cea40b468268ba84eac38a21f09ee

上面这张图很好的总结了 Redis 集群将数据映射到实际节点的过程:

 ●  Redis 对数据的特征值(一般是key)计算哈希值,使用的算法是 CRC16。
 ●  根据哈希值,计算数据属于哪个槽。

 ●  根据槽与节点的映射关系,计算数据属于哪个节点。

节点通信机制

集群要作为一个整体工作,离不开节点之间的通信。

两个端口

在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。

在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。

为此,集群中的每个节点,都提供了两个 TCP 端口:

 ●  普通端口:即我们在前面指定的端口(7000 等)。普通端口主要用于为客户端提供服务(与单机节点类似);但在节点间数据迁移时也会使用。

 ●  集群端口:端口号是普通端口+10000(10000 是固定值,无法改变),如 7000 节点的集群端口为 17000。

集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。

Gossip 协议

节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。

广播是指向集群内所有节点发送消息;优点是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点是每条消息都要发送给所有节点,CPU、带宽等消耗较大。

Gossip 协议的特点是:在节点数量有限的网络中,每个节点都“随机”的与部分节点通信(并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。

Gossip 协议的优点有负载(比广播)低、去中心化、容错性高(因为通信有冗余)等;缺点主要是集群的收敛速度慢。

消息类型

集群中的节点采用固定频率(每秒 10 次)的定时任务进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。

如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。

节点间发送的消息主要分为 5 种:

 ●  MEET 消息
 ●  PING 消息
 ●  PONG 消息
 ●  FAIL 消息
 ●  PUBLISH 消息

不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的:

 ●  MEET 消息:在节点握手阶段,当节点收到客户端的 cluster meet 命令时,会向新加入的节点发送 MEET 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 PONG 消息。
 ●  PING 消息:集群里每个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。PING 消息的内容是自身节点和部分其他节点的状态信息;作用是彼此交换信息,以及检测节点是否在线。
PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:①随机找 5 个节点,在其中选择最久没有通信的 1 个节点。②扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout/2 的所有节点,防止这些节点长时间未更新。
 ●  PONG 消息:PONG 消息封装了自身状态数据。可以分为两种:第一种是在接到 MEET/PING 消息后回复的 PONG 消息;第二种是指节点向集群广播 PONG 消息。
这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
 ●  FAIL 消息:当一个主节点判断另一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。

 ●  PUBLISH 消息:节点收到 PUBLISH 命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。

数据结构

节点需要专门的数据结构来存储集群的状态。所谓集群的状态,是一个比较大的概念,包括:集群是否处于上线状态、集群中有哪些节点、节点是否可达、节点的主从状态、槽的分布……

节点为了存储集群状态而提供的数据结构中,最关键的是 clusterNode 和 clusterState 结构:前者记录了一个节点的状态,后者记录了集群作为一个整体的状态。

clusterNode

clusterNode 结构保存了一个节点的当前状态,包括创建时间、节点 id、ip 和端口号等。

每个节点都会用一个 clusterNode 结构记录自己的状态,并为集群内所有其他节点都创建一个 clusterNode 结构来记录节点状态。

下面列举了 clusterNode 的部分字段,并说明了字段的含义和作用:


typedef struct clusterNode {

//节点创建时间
mstime_t ctime;

//节点id
char name[REDIS_CLUSTER_NAMELEN];

//节点的ip和端口号
char ip[REDIS_IP_STR_LEN];
int port;

//节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等
int flags;

//配置纪元:故障转移时起作用,类似于哨兵的配置纪元
uint64_t configEpoch;

//槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
unsigned char slots[16384/8];

//节点中槽的数量
int numslots;

…………
} clusterNode;

除此之外,clusterState 还包括故障转移、槽迁移等需要的信息。

集群命令的实现

这一部分将以 cluster meet(节点握手)、cluster addslots(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。

cluster meet

假设要向 A 节点发送 cluster meet 命令,将 B 节点加入到 A 所在的集群,则 A 节点收到命令后,执行的操作如下:

 ●  A 为 B 创建一个 clusterNode 结构,并将其添加到 clusterState 的 nodes 字典中。
 ●  A 向 B 发送 MEET 消息。
 ●  B 收到 MEET 消息后,会为 A 创建一个 clusterNode 结构,并将其添加到 clusterState 的 nodes 字典中。
 ●  B 回复 A 一个 PONG 消息。
 ●  A 收到 B 的 PONG 消息后,便知道 B 已经成功接收自己的 MEET 消息。
 ●  然后,A 向 B 返回一个 PING 消息。
 ●  B 收到 A 的 PING 消息后,便知道 A 已经成功接收自己的 PONG 消息,握手完成。
 ●  之后,A 通过 Gossip 协议将 B 的信息广播给集群内其他节点,其他节点也会与 B 握手;一段时间后,集群收敛,B 成为集群内的一个普通节点。

通过上述过程可以发现,集群中两个节点的握手过程与 TCP 类似,都是三次握手:A 向 B 发送 MEET;B 向 A 发送 PONG;A 向 B 发送 PING。

cluster addslots

集群中槽的分配信息,存储在 clusterNode 的 slots 数组和 clusterState 的 slots 数组中,两个数组的结构前面已做介绍。

二者的区别在于:前者存储的是该节点中分配了哪些槽,后者存储的是集群中所有槽分别分布在哪个节点。

cluster addslots 命令接收一个槽或多个槽作为参数,例如在 A 节点上执行 cluster addslots {0..10} 命令,是将编号为 0-10 的槽分配给 A 节点。

具体执行过程如下:

 ●  遍历输入槽,检查它们是否都没有分配,如果有一个槽已分配,命令执行失败;方法是检查输入槽在 clusterState.slots[] 中对应的值是否为 NULL。
 ●  遍历输入槽,将其分配给节点 A;方法是修改 clusterNode.slots[] 中对应的比特为 1,以及 clusterState.slots[] 中对应的指针指向 A 节点。

 ●  A 节点执行完成后,通过节点通信机制通知其他节点,所有节点都会知道 0-10 的槽分配给了 A 节点。

客户端访问集群

在集群中,数据分布在不同的节点中,客户端通过某节点访问数据时,数据可能不在该节点中;下面介绍集群是如何处理这个问题的。

redis-cli

当节点收到 redis-cli 发来的命令(如 set/get)时,过程如下:

①计算 key 属于哪个槽:CRC16(key) &16383。

集群提供的 cluster keyslot 命令也是使用上述公式实现,如:

e8a0c80e8126f22baeae5764b2147fb3895e77ea

②判断 key 所在的槽是否在当前节点:假设 key 位于第 i 个槽,clusterState.slots[i] 则指向了槽所在的节点。

如果 clusterState.slots[i]==clusterState.myself,说明槽在当前节点,可以直接在当前节点执行命令。

否则,说明槽不在当前节点,则查询槽所在节点的地址(clusterState.slots[i].ip/port),并将其包装到 MOVED 错误中返回给 redis-cli。

③redis-cli 收到 MOVED 错误后,根据返回的 ip 和 port 重新发送请求。

下面的例子展示了 redis-cli 和集群的互动过程:在 7000 节点中操作 key1,但 key1 所在的槽 9189 在节点 7001 中。

因此节点返回 MOVED 错误(包含 7001 节点的 ip 和 port)给 redis-cli,redis-cli 重新向 7001 发起请求。

c7b35be05f8edc3ff4a0c687bb4dad534c7a878f

上例中,redis-cli 通过 -c 指定了集群模式,如果没有指定,redis-cli 无法处理 MOVED 错误:

8e7d81e9600848a08c49a24c21a8d6bcf5aebc8d

Smart 客户端

redis-cli 这一类客户端称为 Dummy 客户端,因为它们在执行命令前不知道数据在哪个节点,需要借助 MOVED 错误重新定向。与 Dummy 客户端相对应的是 Smart 客户端。

Smart 客户端(以 Java 的 JedisCluster 为例)的基本原理如下:

①JedisCluster 初始化时,在内部维护 slot->node 的缓存,方法是连接任一节点,执行 cluster slots 命令,该命令返回如下所示:

4269f429132946c414b9061d1a4b678a4f35a28d

②此外,JedisCluster 为每个节点创建连接池(即 JedisPool)。

③当执行命令时,JedisCluster 根据 key->slot->node 选择需要连接的节点,发送命令。

如果成功,则命令执行完毕;如果执行失败,则会随机选择其他节点进行重试,并在出现 MOVED 错误时,使用 cluster slots 重新同步 slot->node 的映射关系。

下面代码演示了如何使用 JedisCluster 访问集群(未考虑资源释放、异常处理等):


publicstatic void test() {
Set<HostAndPort>nodes = new HashSet<>();
nodes.add(newHostAndPort("192.168.72.128", 7000));
nodes.add(newHostAndPort("192.168.72.128", 7001));
nodes.add(newHostAndPort("192.168.72.128", 7002));
nodes.add(newHostAndPort("192.168.72.128", 8000));
nodes.add(newHostAndPort("192.168.72.128", 8001));
nodes.add(newHostAndPort("192.168.72.128", 8002));
JedisClustercluster = new JedisCluster(nodes);
System.out.println(cluster.get("key1"));
cluster.close();
}

注意事项如下:

 ●  JedisCluster 中已经包含所有节点的连接池,因此 JedisCluster 要使用单例。
 ●  客户端维护了 slot->node 映射关系以及为每个节点创建了连接池,当节点数量较多时,应注意客户端内存资源和连接资源的消耗。

 ●  Jedis 较新版本针对 JedisCluster 做了一些性能方面的优化,如 cluster slots 缓存更新和锁阻塞等方面的优化,应尽量使用 2.8.2 及以上版本的 Jedis。

实践须知

前面介绍了集群正常运行和访问的方法和原理,下面是一些重要的补充内容。

集群伸缩

实践中常常需要对集群进行伸缩,如访问量增大时的扩容操作。Redis 集群可以在不影响对外服务的情况下实现伸缩;伸缩的核心是槽迁移:修改槽与节点的对应关系,实现槽(即数据)在节点之间的移动。

例如,如果槽均匀分布在集群的 3 个节点中,此时增加一个节点,则需要从 3 个节点中分别拿出一部分槽给新节点,从而实现槽在 4 个节点中的均匀分布。

增加节点

假设要增加 7003 和 8003 节点,其中 8003 是 7003 的从节点,步骤如下:

①启动节点:方法参见集群搭建。

②节点握手:可以使用 cluster meet 命令,但在生产环境中建议使用 redis-trib.rb 的 add-node 工具,其原理也是 cluster meet,但它会先检查新节点是否已加入其他集群或者存在数据,避免加入到集群后带来混乱。


redis-trib.rb add-node 192.168.72.128:7003 192.168.72.1287000
redis-trib.rb add-node 192.168.72.128:8003 192.168.72.1287000

③迁移槽:推荐使用 redis-trib.rb 的 reshard 工具实现。reshard 自动化程度很高,只需要输入 redis-trib.rb reshard ip:port (ip 和 port 可以是集群中的任一节点)。

然后按照提示输入以下信息,槽迁移会自动完成:

 ●  待迁移的槽数量:16384 个槽均分给 4 个节点,每个节点 4096 个槽,因此待迁移槽数量为 4096。
 ●  目标节点 id:7003 节点的 id。
 ●  源节点的 id:7000/7001/7002 节点的 id。

④指定主从关系:方法参见集群搭建。

减少节点

假设要下线 7000/8000 节点,可以分为两步:

 ●  迁移槽:使用 reshard 将 7000 节点中的槽均匀迁移到 7001/7002/7003 节点。
 ●  下线节点:使用 redis-trib.rb del-node 工具;应先下线从节点再下线主节点,因为若主节点先下线,从节点会被指向其他主节点,造成不必要的全量复制。

redis-trib.rb del-node 192.168.72.128:7001 {节点8000id}
redis-trib.rb del-node 192.168.72.128:7001 {节点7000id}

ASK 错误

集群伸缩的核心是槽迁移。在槽迁移过程中,如果客户端向源节点发送命令,源节点执行流程如下:

c086fad293252b9b799bcbb766c872cdede241a1

客户端收到 ASK 错误后,从中读取目标节点的地址信息,并向目标节点重新发送请求,就像收到 MOVED 错误时一样。

但是二者有很大区别:ASK 错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART 客户端不会刷新 slots 缓存;MOVED 错误重定向则是(相对)永久的,SMART 客户端会刷新 slots 缓存。

故障转移

在哨兵一文中,介绍了哨兵实现故障发现和故障转移的原理。

虽然细节上有很大不同,但集群的实现与哨兵思路类似:通过定时任务发送 PING 消息检测其他节点状态;节点下线分为主观下线和客观下线;客观下线后选取从节点进行故障转移。

与哨兵一样,集群只实现了主节点的故障转移;从节点故障时只会被下线,不会进行故障转移。

因此,使用集群时,应谨慎使用读写分离技术,因为从节点故障会导致读服务不可用,可用性变差。

这里不再详细介绍故障转移的细节,只对重要事项进行说明:

节点数量:在故障转移阶段,需要由主节点投票选出哪个从节点成为新的主节点;从节点选举胜出需要的票数为 N/2+1;其中 N 为主节点数量(包括故障主节点),但故障主节点实际上不能投票。

因此为了能够在故障发生时顺利选出从节点,集群中至少需要 3 个主节点(且部署在不同的物理机上)。

故障转移时间:从主节点故障发生到完成转移,所需要的时间主要消耗在主观下线识别、主观下线传播、选举延迟等几个环节。

具体时间与参数 cluster-node-timeout 有关,一般来说:

 ●  故障转移时间(毫秒) ≤1.5 * cluster-node-timeout + 1000。

 ●  cluster-node-timeout 的默认值为 15000ms(15 s),因此故障转移时间会在 20s 量级。

集群的限制及应对方法

由于集群中的数据分布在不同节点中,导致一些功能受限,包括:

 ●  key 批量操作受限:例如 mget、mset 操作,只有当操作的 key 都位于一个槽时,才能进行。
针对该问题,一种思路是在客户端记录槽与 key 的信息,每次针对特定槽执行 mget/mset;另外一种思路是使用 Hash Tag。
 ●  keys/flushall 等操作:keys/flushall 等操作可以在任一节点执行,但是结果只针对当前节点,例如 keys 操作只返回当前节点的所有键。
针对该问题,可以在客户端使用 cluster nodes 获取所有节点信息,并对其中的所有主节点执行 keys/flushall 等操作。
 ●  事务/Lua 脚本:集群支持事务及 Lua 脚本,但前提条件是所涉及的 key 必须在同一个节点。Hash Tag 可以解决该问题。
 ●  数据库:单机 Redis 节点可以支持 16 个数据库,集群模式下只支持一个,即 db0。

 ●  复制结构:只支持一层复制结构,不支持嵌套。

Hash Tag

Hash Tag 原理是:当一个 key 包含 {} 的时候,不对整个 key 做 hash,而仅对 {} 包括的字符串做 hash。

Hash Tag 可以让不同的 key 拥有相同的 hash 值,从而分配在同一个槽里;这样针对不同 key 的批量操作(mget/mset 等),以及事务、Lua 脚本等都可以支持。

不过 Hash Tag 可能会带来数据分配不均的问题,这时需要:

 ●  调整不同节点中槽的数量,使数据分布尽量均匀。
 ●  避免对热点数据使用 Hash Tag,导致请求分布不均。

下面是使用 Hash Tag 的一个例子:通过对 product 加 Hash Tag,可以将所有产品信息放到同一个槽中,便于操作。

5ab48cd8deb1905f15ce1d9702b28c171a281ff4

参数优化

cluster_node_timeout

cluster_node_timeout 参数在前面已经初步介绍;它的默认值是 15s,影响包括:

 ●  影响 PING 消息接收节点的选择:值越大对延迟容忍度越高,选择的接收节点越少,可以降低带宽,但会降低收敛速度;应根据带宽情况和应用要求进行调整。
 ●  影响故障转移的判定和时间:值越大,越不容易误判,但完成转移消耗时间越长;应根据网络状况和应用要求进行调整。

cluster-require-full-coverage

前面提到,只有当 16384 个槽全部分配完毕时,集群才能上线。这样做是为了保证集群的完整性。

但同时也带来了新的问题:当主节点发生故障而故障转移尚未完成,原主节点中的槽不在任何节点中,此时集群会处于下线状态,无法响应客户端的请求。

cluster-require-full-coverage 参数可以改变这一设定:如果设置为 no,则当槽没有完全分配时,集群仍可以上线。

参数默认值为 yes,如果应用对可用性要求较高,可以修改为 no,但需要自己保证槽全部分配。

redis-trib.rb

redis-trib.rb 提供了众多实用工具:创建集群、增减节点、槽迁移、检查完整性、数据重新平衡等;通过 help 命令可以查看详细信息。

在实践中如果能使用 redis-trib.rb 工具则尽量使用,不但方便快捷,还可以大大降低出错概率。


原文发布时间为:2018-11-6

本文作者:编程迷思

本文来自云栖社区合作伙伴“数据和云”,了解相关信息可以关注“数据和云”。

网友评论

登录后评论
0/500
评论
技术小能手
+ 关注
所属云栖号: 数据和云