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

栏目: 数据库 · 发布时间: 6年前

内容简介:【51CTO.com原创稿件】在前面的文章中已经介绍了 Redis 的几种高可用技术:持久化、主从复制和哨兵,但这些方案仍有不足,其中最主要的问题是存储能力受单机限制,以及无法实现写操作的负载均衡。本文将详细介绍集群,主要内容包括:

【51CTO.com原创稿件】在前面的文章中已经介绍了 Redis 的几种高可用技术:持久化、主从复制和哨兵,但这些方案仍有不足,其中最主要的问题是存储能力受单机限制,以及无法实现写操作的负载均衡。

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

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

  • 集群的作用
  • 集群的搭建方法及设计方案
  • 集群的基本原理
  • 客户端访问集群的方法
  • 实践须知(集群伸缩、故障转移、参数优化等)

集群的作用

集群,即 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,如下图所示:

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

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

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

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

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

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

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

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

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

redis-server redis-7000.conf 

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

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

其中返回值第一项表示节点 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 查看:

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

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

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

同理,在 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 节点已经感知到了所有其他节点:

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

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

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

分配槽

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

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

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

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

分配槽使用 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} 

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

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

指定主从关系

集群中指定主从关系不再使用 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 查看各个节点的状态,可以看到主从关系已经建立:

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

至此,集群搭建完毕。

使用 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 搭建集群时,要求节点不能包含任何槽和数据。

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

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

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

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

至此,集群搭建完毕。

集群方案设计

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

  • 高可用要求:根据故障转移的原理,至少需要 3 个主节点才能完成故障转移,且 3 个主节点不应在同一台物理机上。

每个主节点至少需要 1 个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含 6 个节点。

  • 数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过 benchmark 得到较准确估计),计算需要的主节点数量。
  • 节点数量限制:Redis 官方给出的节点数量限制为 1000,主要是考虑节点间通信带来的消耗。

在实际应用中应尽量避免大集群,如果节点数量不足以满足应用对 Redis 数据量和访问量的要求,可以考虑:①业务分割,大集群分为多个小集群;②减少不必要的数据;③调整数据过期策略等。

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

集群的基本原理

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

  • 集群最核心的功能是数据分区,因此:
  • 首先介绍数据的分区规则。
  • 然后介绍集群实现的细节:通信机制和数据结构。

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

数据分区方案

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

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

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

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

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

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

哈希取余分区

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

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

一致性哈希分区

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

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

对于每个数据,根据 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。

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

上面这张图很好的总结了 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 命令也是使用上述公式实现,如:

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

②判断 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 发起请求。

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

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

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

Smart 客户端

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

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

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

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

②此外,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 {节点8000的id} 
redis-trib.rb del-node 192.168.72.128:7001 {节点7000的id} 

ASK 错误

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

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

客户端收到 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,可以将所有产品信息放到同一个槽中,便于操作。

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

参数优化

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 工具则尽量使用,不但方便快捷,还可以大大降低出错概率。

参考文献:

  • 《Redis开发与运维》
  • 《Redis设计与实现》
  • https://redis.io/topics/cluster-tutorial
  • https://redis.io/topics/cluster-spec
  • https://mp.weixin.qq.com/s/d6hzmk31o7VBsMYaLdQ5mw
  • https://www.cnblogs.com/lpfuture/p/5796398.html
  • http://www.zsythink.net/archives/1182/
  • https://www.cnblogs.com/xxdfly/p/5641719.html

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Fluent Python

Fluent Python

Luciano Ramalho / O'Reilly Media / 2015-8-20 / USD 39.99

Learn how to write idiomatic, effective Python code by leveraging its best features. Python's simplicity quickly lets you become productive with it, but this often means you aren’t using everything th......一起来看看 《Fluent Python》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具