• redis集群


    前言

      Redis集群解决了写操作无法负载均衡,以及存储能力受单机限制等问题,实现了较为完善的高可用方案。

    一、集群的作用

      Redis集群,即 Redis cluster,是Redis3.0开始引入的分布式存储方案。

      集群由多个节点(node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写数据和集群信息得到维护;从节点只负责主节点数据和状态信息的复制。

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

      1. 数据分区:数据分区(或称数据分片)是集群最核心的功能。

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

      Redis单机内存大小受限的问题,在介绍持久化和主从复制时都有提到。例如,如果单机内存太大,bgsave和bgrewriteof的fork操作可能导致主进程阻塞,从主环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出等等。

      2. 高可用:集群支持主从复制和主节点的自动故障转移(与哨兵类似);当任一节点发生故障时,集群仍然可以对外提供服务。

    二、集群的搭建

      这一部分我们将搭建一个简单的集群,共6个节点,3主3从。方便起见:所有节点分布在3台虚拟机服务器上,以端口号和IP地址区分;配置从简。

      集群的搭建有两种方式:

      1. 手动执行Redis命令,一步步完成搭建

      2. 使用Ruby脚本搭建。

      二者搭建的原理是一样的,只是Ruby脚本将Redis命令进行了封装;在实际应用中推荐脚本搭建,简单快捷不容易出错。

    1. 执行Redis命令搭建

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

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

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

    启动节点

      集群节点的启动仍然使用redis-server命令,但需要使用集群模式启动。下面是其中一个节点的配置文件(只列出了节点正常工作关键配置,其他配置(如开启AOF)可以参照单机节点进行):

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

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

    • cluster-enabled:Redis实例可以分为单机模式(standalone)和集群模式(cluster);值为yes可以启动集群模式。在单机模式下启动的Redis实例,如果执行info server命令,可以发现redis_mode一项为standalone,如下图:  

      

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

      

    •  cluster-config-file:该参数指定了集群配置文件的名称。每个节点在运行中会维护一份集群配置文件,该配置文件是由节点创建的。不可手动修改;每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件中;当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的加入到集群中。也就是说,当Redis以集群模式启动时,会首先寻找是否有集群配置文件,如果有则使用文件中的配置启动,否则,初始化配置并将配置保存到文件中。

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

    redis-server redis.conf
    

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

      

       其中返回值第一项表示节点ID,由40个16进制字符串组成,节点ID与主从复制中提到的runid不同:Redis每次启动runid都会重建,但是节点ID只在集群初始化时创建一次,然后保存到配置文件中,以后节点重新启动时会直接在配置文件中读取。

      其他节点使用相同方法启动。需要特别注意,在启动节点阶段,节点是没有主从关系的,一次节点不需要加replicaof配置。

    节点握手

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

      节点握手使用cluster meet <ip> <port>命令实现;注意IP使用的是局域网IP而不是localhost或者127.0.0.1,是为了其他机器上的节点或客户端也可以访问。此时再使用cluster nodes查看:

      

       同理使用cluster meet命令将其他节点加入到集群,完成节点握手:

      

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

      

     分配槽

      在Redis集群中,借助槽实现数据分区,具体原理后文会介绍。集群有16384个槽,槽是数据管理和迁移的基本单位。当数据库中16384个槽都分配了节点时,集群处于上线状态(OK);如果任意一个槽没有分配节点,则集群处于下线状态(fail)。

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

      

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

    redis-cli -h 192.168.73.129 -p 6379 cluster addslots {0..16383}
    redis-cli -h 192.168.73.128 -p 6379 cluster addslots {0..16383}
    redis-cli -h 192.168.73.130 -p 6379 cluster addslots {0..16383}
    

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

      

     指定从主关系

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

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

    redis-cli -p 6380 cluster replicate 18fbacb57534abd5bd3f7fdd411a3e1f04782979
    

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

      

       至此,集群搭建完毕。

    2. 使用Ruby脚本搭建集群

      如果使用的Redis版本是3.x或4.x,在redis源码包里面的src目录下有一个redis-trib.rb文件,这是一个Ruby脚本,可以实现自动化的集群搭建。您需要安装redisgem才能运行redis-trib

      (1)安装Ruby环境

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

      (2)启动节点

      与第一种启动方式一样。

      (3)搭建集群

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

      

    ./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

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

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

      

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

      

       至此,集群搭建完毕。

    3. 使用redis-cli --cluster create 命令搭建集群(redis>5.x)

      如果您使用的是Redis5.x以上的版本,可以直接使用redis-cli --cluster create 命令一键搞定集群搭建:

    redis-cli --cluster create 192.168.142.128:7000 192.168.142.128:7001 192.168.142.128:7002 192.168.142.128:7003 192.168.142.128:7004 192.168.142.128:7005 --cluster-replicas 1
    

      

       

      集群搭建完成。

    4. 集群设计方案

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

      (1)高可用要求:根据故障转移原理,至少需要3个主节点才能完成故障转移,且3个主节点不应在同一台物理机上,每个主节点至少需要1个从节点,且从节点不应在同一台物理机上,因此高可用集群至少需要6个节点。

      (2)数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过benchmark得到较准确估计),计算需要的主节点数量。

      (3)节点数量限制:Redis官方给出了节点数量限制为1000,主要是考虑节点之间的通信带来的消耗,在实际应用中应尽量避免大集群,如果节点数量不足以满足应用对Redis数据量和访问的要求,可以考虑:a. 业务分割,大集群分割为多个小集群;b. 减少不必要的数据;c. 调整数据过期策略等。

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

    三、集群的基本原理

      集群最核心的功能是数据分区,因此首先介绍数据的分区规则;然后介绍集群实现的细节:通信机制和数据结构;最后以meet cluster(节点握手)。cluster addsolts(槽分配)为例,说明节点是如何利用上述数据结构和通信机制实现集群命令的。

      1. 数据分区方案

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

      哈希分区的基本思路:对数据的特征值(如key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。

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

      (1)数据分布是否均匀

      (2)增删节点对数据分区的影响。

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

      (1)哈希取余分区

      哈希取余分区思路非常简单:计算key的哈希值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生改变,系统中所有的数据都需要重新计算映射关系,引发大规模的数据迁移。

      (2)一致性哈希分区

      一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为0~2^32-1;对于每个数据,根据key计算哈希值,确定key在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其映射到的服务器。

      

       与哈希取余分区相比,一致性哈希分区将增删节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间新增节点node5,则只有node2中的一部分数据迁移到node5中;如果去掉node2节点,则原node2中的数据只会迁移到node4中,只有node4会受到影响。

      一致性哈希分区的主要问题在于,当节点数量较少时,增加或删除节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2节点,node4中的数据由总数据的1/4左右变成1/2左右,与其他节点相比负载过高。

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

      该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。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对数据的特征值(一般是key)计算哈希值,使用的算法是CRC16。

      根据哈希值,计算数据属于哪个槽。

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

      2. 节点通信机制

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

      两个端口

      在哨兵系统中,节点分为数据节点和哨兵节点,前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分,所有的节点都存储数据,也都参与集群状态维护。为此,进群中的每个节点,都提供了两个TCP端口:

    • 普通端口:即我们在前面指定的端口(6379等)。普通端口主要用于为客户端提供服务(与单机节点类似),但在节点数据迁移时也会使用。
    • 集群端口:端口号是普通端口号+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消息,接收者收到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命令。

    3. 数据结构

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

      节点为了存储集群状态而提供的数据结构中,最关键的是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;
    

      除了上述字段,clusterNode还包含节点连接、主从复制、故障发现和转移需要的信息等。

      clusterState

      clusterState结构保存了在当前节点视角下,集群所出的状态。主要字段包括:

    typedef struct clusterState {
     
        //自身节点
        clusterNode *myself;
     
        //配置纪元
        uint64_t currentEpoch;
     
        //集群状态:在线还是下线
        int state;
     
        //集群中至少包含一个槽的节点数量
        int size;
     
        //哈希表,节点名称->clusterNode节点指针
        dict *nodes;
      
        //槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL
        clusterNode *slots[16384];
     
        …………
         
    } clusterState;
    

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

    4. 集群的目录实现

      这一部分将以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命令登录客户端时需要额外添加一个参数:-c 来实现基本的集群支持:

    [root@localhost configs]# redis-cli -c -p 7000
    

      客户端登陆之后,当节点接收到发来的命令(如get/set等)时,过程如下:

      1. 计算key属于哪个槽:CRC16(key) & 16383

      集群提供的cluster keyslot命令也是根据上述公式实现:如:

      

       2. 判断key所在的槽是否在当前节点:假设key位于第i个槽,clusterState.slots[i]则指向了槽所在的节点,如果clusterState.slots[i] == clusterState.myself,说明槽在当前节点,可以在当前节点执行命令;否则,说明槽不在当前节点,则查询槽所在节点的地址(clusterState.slots[i].ip/port),并将其包装到moved错误中,返回给redis-cli。

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

      下面的例子展示了redis-cli和集群的互动过程:在7000节点上操作k1,但k1所在的槽12706在7002节点上,因此节点返回moved错误(包含7002节点的ip和port)给redis-cli,redis-cli重新向7002发起请求。

      

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

      

     五、实践须知

      1. 集群伸缩

      实践中常常需要对集群进行伸缩,如访问量大的时候扩容操作。Redis集群可以在不影响对外服务的情况下实现伸缩,伸缩的核心是槽迁移:修改槽与节点的对应关系,实现槽(即数据)在节点之间的移动。例如,槽均匀分布在集群的3个节点中,此时增加一个节点,则需要分别从3个节点中拿出一部分槽给新的节点,从而实现槽在4个节点中的均匀分布。

      增加节点

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

      (1)启动节点

      (2)节点握手:可以使用cluster meet命令,但是在生产环境建议使用redis-cli --cluster add-node命令。

    # 向7000节点所在的集群中添加7006主节点
    redis-cli --cluster add-node 192.168.142.128:7006 192.168.142.128:7000
    # 向主节点7006添加从节点7007 redis-cli --cluster add-node 192.168.142.128:7007 192.168.142.128:7006 --cluster-slave
    # 或
    redis-cli --cluster add-node 192.168.142.128:7007 192.168.142.128:7000 --cluster-slave --cluster-master-id d4372aa9f312d7da6caafb05def49d7410f144a9

      

      

      

       此时新加入的主节点并没有被分配槽。

      (3)迁移槽:迁移槽有两种方法

      第一种是自动分配哈希槽,可以使用命令redis-cli --cluster rebalance --cluster-threshold 1 --cluster-use-empty-masters来实现

    redis-cli --cluster rebalance --cluster-threshold 1 --cluster-use-empty-masters 192.168.142.128:7000
    

      

       第二种是手动分配哈希槽,可以使用命令redis-cli --cluster reshard来实现

    redis-cli --cluster reshard 192.168.142.128:7006
    

      

       

       删除节点

      要删除一个主节点,可以分为一下两步:

      以删除7006主节点为例:

      1. 迁移槽:使用redis-cli --cluster del-node 命令将7006节点上的槽迁移到7000、7001、7002节点上:

    # 基本格式
    redis-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes
    
    #实例
    redis-cli --cluster reshard 192.168.142.128:7000 --cluster-from d4372aa9f312d7da6caafb05def49d7410f144a9 --cluster-to 6189a8a9d01b330adfaf22598663b85f789ec583 --cluster-slots 1820 --cluster-yes
    
    redis-cli --cluster reshard 192.168.142.128:7000 --cluster-from d4372aa9f312d7da6caafb05def49d7410f144a9 --cluster-to 984b43faa7ffaad3fdb97a529491e7dd740d3659 --cluster-slots 1820 --cluster-yes
    
    redis-cli --cluster reshard 192.168.142.128:7000 --cluster-from d4372aa9f312d7da6caafb05def49d7410f144a9 --cluster-to a1a58648416bc9853c9749457389361c2acb861d --cluster-slots 1820 --cluster-yes
    

      2. 删除节点:

      要删除节点只需要使用redis-cli --cluster del-node <node-id>命令:

    redis-cli --cluster del-node 192.168.142.128:7000 d4372aa9f312d7da6caafb05def49d7410f144a9
    

      

       ASK错误

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

      

       客户端收到ASK错误后,从中读取目标节点的地址信息,并向目标节点重新发送请求,就像收到moved错误时一样。但是二者有很大的区别:ASK错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART客户端不会刷新slots缓存;moved错误重定向则是(相对)永久的,SMART客户端会刷新slots缓存。

      2. 故障转移

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

      与哨兵一样,集群只实现了主节点的故障转移,从节点故障时只会下线,不会进行故障转移。因此使用集群时,应谨慎使用读写分离技术,因为从节点故障会导致读服务不可用,可用性变差。

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

      节点数量:在故障转移阶段,需要由主节点投票选出哪个从节点称为新的主节点;从节点选举胜出需要的票数为N/2+1;其中N为主节点总量(包括故障节点),但故障节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要3个主节点(且部署在不同的物理机上)。

      故障转移时间:从主节点故障发生到完成转移,所需要的时间主要消耗在主观下线识别、主观下线传播、选举延迟等几个环节;具体时间与参数cluster-node-timeout有关,一般来说:

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

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

      3. 集群的限制及应对方法

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

      (1)key批量操作受限:如mget、mset操作,只有当操作的key位于同一个槽时,才能进行。针对该问题,一种思路是在客户端记录槽与key的信息,每次针对特定槽执行mget/mset;另外一种思路是会用hash tag。

      (2)keys/flushall操作:keys/flushall等操作可以在任一节点执行,但是结果只针对当前节点,例如keys操作只返回当前节点的所有键。针对该问题,可以在客户端使用cluster nodes获取所有节点信息,并对其中的所有主节点执行keys/flushall等操作。

      (3)事务/Lua脚本:集群支持事务及Lua脚本,但前提条件是所涉及的key必须在同一节点。hash tag可以解决该问题。

      (4)数据库:单机Redis可以支持16个数据库,集群模式下只支持一个,即db0。

      (5)复制结构:只支持一层复制结构,不支持嵌套。

      4. Hash Tag

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

      Hash Tag可以让不同的key拥有相同的hash值,从而分配在同一个槽里;这样针对不同key的批量操作(mget/mset等),以及事务、Lua脚本等都可以支持。不过Hash Tag可能会带来数据分配不均的问题,这时需要:

      (1)调整不同节点中槽的数量,使数据分布尽量均匀;

      (2)避免对热点数据使用Hash Tag,导致请求分配不均。

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

      

       5. 参数优化

      cluster_node_timeout

      默认值是15s,影响包括:

      (1)影响ping消息接收节点的选择:值越大对延迟容忍度越高,选择的接收节点越少,可以降低带宽,但会降低收敛速度;应根据带宽情况和实际应用要求进行调整。

      (2)影响故障转移的判定和时间:值越大,越不容易误判,但完成转移消耗时间越长;应根据网络状况和实际应用要求调整。

      cluster-require-full-coverage

      前面提到,只有当16384个槽全部分配完毕时,集群才能上线。这样做是为了保证集群的完整性,但同时也带来了新的问题:当主节点发生了故障而故障转移尚未完成,原主节点中的槽不在任何节点中,此时集群处于下线状态,无法应答客户端请求。

      cluster-require-full-coverage参数可以改变这一设定:如果设置为no,则当槽没有完全分配时,集群仍可以上线。参数默认为yes,如果应用对可用性要求高,可以修改为no,但需要自己保证槽全部分配。

  • 相关阅读:
    Redis系列--3.redis配置
    Redis系列--2.redis安装
    Redis系列--1.redis概述
    Redis系列--4.常用命令汇总
    Redis系列之(六)——集群redis cluster
    Redis系列之(五)——哨兵
    Redis系列之(四)——深入reids优化
    Redis系列之(三)——持久化与复制
    Redis系列之(一)——开发基础
    Python执行系统命令的方法
  • 原文地址:https://www.cnblogs.com/huige185/p/14242643.html
Copyright © 2020-2023  润新知