Redis专题:初识Redis Cluster的基本结构(1/3)

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

跟着本系列一路走来,我们已经知道Redis通过主从复制为手动故障转移提供了可能,通过哨兵模式实现了监控及自动故障转移。Redis高性能的表现,使得哨兵模式可以一般规模的应用中轻松应对。

随着业务系统功能、模块、规模、复杂性的增加,我们对Redis的要求越来越高,尤其是在高低峰场景的动态伸缩能力,比如:电商平台平日流量较低且平稳,双十一大促流量是平日的数倍,两种情况下对于各系统的数量要求必然不同。如果始终配备高峰时的硬件及中间件配置,必然带来大量的资源浪费。

Redis作为业界优秀的缓存产品,成为了各类系统的必备中间件。哨兵模式虽然优秀,但由于其不具备动态水平伸缩能力,无法满足日益复杂的应用场景。在官方推出集群模式之前,业界就已经推出了各种优秀实践,比如:Codis、twemproxy等。

为了弥补这一缺陷,自3.0版本起,Redis官方推出了一种新的运行模式——Redis Cluster。

Redis Cluster采用无中心结构,具备多个节点之间自动进行数据分片的能力,支持节点动态添加与移除,可以在部分节点不可用时进行自动故障转移,确保系统高可用的一种集群化运行模式。按照官方的阐述,Redis Cluster有以下设计目标:

  • 高性能可扩展,支持扩展到1000个节点。多个节点之间数据分片,采用异步复制模式完成主从同步,无代理方式完成重定向。
  • 一定程度内可接受的写入安全:系统将尽可能去保留客户端通过大多数主节点所在网络分区所有的写入操作,通常情况下存在写入命令已确认却丢失的较短时间窗口。如果客户端连接至少量节点所处的网络分区,这个时间窗口可能较大。
  • 可用性:如果大多数节点是可达的,并且不可达主节点至少存在一个可达的从节点,那么Redis Cluster可以在网络分区下工作。而且,如果某个主节点A无从节点,但是某些主节点B拥有多个(大于1)从节点,可以通过从节点迁移操作,把B的某个从节点转移至A。

简单概述。结合以上三个目标,我认为Redis Cluster最大的特点在于可扩展性,多个主节点通过分片机制存储所有数据,即每个主从复制结构单元管理部分key。因为在主从复制、哨兵模式下,同样具备其他优点。当系统容量足够大时,读请求可以通过增加从节点进行分摊压力,但是写请求只能通过主节点,这样存在以下风险点:

  • 所有写入请求集中在一个Redis实例,随着请求的增加,单个主节点可能出现写入延迟。
  • 每个节点都保存系统的全量数据,如果存储数据过多,执行rdb备份或aof重写时fork耗时增加,主从复制传输及数据恢复耗时增加,甚至失败;
  • 如果该主节点故障,在故障转移期间可能导致所有服务短时的数据丢失或不可用。

所以,动态伸缩能力是Redis Cluster最耀眼的特色。好了,开始步入正题,本文将结合实例从整体上对Redis Cluster进行介绍,在后续文章深入剖析其工作原理。

集群结构

还是延续之前的风格,通过实例的搭建与演示,给大家建立对集群结构的直观感受;然后再以源码为基础梳理其中的逻辑关系;最后详细阐述集群构建的过程,循序渐进。

动手实践

按照官方文档的说明,在Redis版本5以上,集群搭建比较简单。本文使用6个Redis实例(版本是6.2.0),三个主节点,三个从节点,每个主节点有一个副本。

  • 准备配置文件:在目录cluster-demo下创建6个文件夹,以Redis将要监听的端口号命名,依次为7000、7001、7002、7003、7004、7005。在每个目录放置一份Redis Cluster所需的最小化配置文件,命名为:cluster.conf,内容如下所示(注意修改端口):
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
  • 启动Redis实例:依次切换到6个目录,执行指令redis-server cluster.conf,以Cluster模式启动Redis实例。以7000为例,如下图:
Redis专题:初识Redis Cluster的基本结构(1/3)
image.png
  • 创建集群:我使用的Redis版本为6.2.0,所以可以直接使用redis-cli。打开terminal输入--cluster create指令,使用我们刚刚开启的Redis实例创建集群,三主三从。
redis-cli --cluster create 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 
--cluster-replicas 1

通过terminal看到输入如下图所示的内容:Redis专题:初识Redis Cluster的基本结构(1/3)上图以>>>是redis-cli创建集群时进行的一些核心操作,当然也还有一些日志中没有输出的部分,最终将建立起如下图所示的集群关系。Redis专题:初识Redis Cluster的基本结构(1/3)上图从两种视角对集群节点关系进行了描述:左侧是在不考虑节点角色情况下的物理结构,节点之间双向箭头代表了集群总线;右侧考虑了节点角色及主从分组,其中体现了主从复制关系及集群总线(集群总线仅绘制了主节点之间的,大家自行脑补,全都画出显得过于凌乱)。

在redis-cli的帮助下,Redis Cluster的搭建还是比较简单的,一条命令便解决了所有问题。从上面的过程我们可以清楚的了解到,在集群创建过程中,redis-cli是一个管理者,负责检查节点状态、主从关系建立以、数据分片以及协调节点间通过握手组建集群,但是这都离不了Redis Cluster能力的支持。

为了深入理解集群建立的过程,并为接下来其他部分的理解打好基础,接下来我将介绍Redis Cluster有关一些概念或结构,然后把集群建立的过程进行详细说明。

集群数据结构

上述示例集群中,有6个Redis实例构成了三主三从的集群结构,并且明确了每组主从节点负责的哈希槽范围,那Redis Cluster是如何描述这种关系的呢?有了上图的直观感受,我们还是要回归到数据结构,看看Redis是如何描述这种关系的。按照Redis源码数据结构之间的关系,我绘制了与Redis Cluster相关的重要数据结构的组织关系,如下图所示(以节点A的视角):Redis专题:初识Redis Cluster的基本结构(1/3)

集群状态(clusterState)

我们知道,Redis Cluster是Redis的一种运行模式,一切都要归属于Redis内最核心的数据结构redisServer,以下仅摘取关于集群模式部分字段。

struct redisServer {
    /* Cluster */
    // 是否以集群模式运行
    int cluster_enabled;      /* Is cluster enabled? */
    
    // 集群节点通信超时参数
    mstime_t cluster_node_timeout; /* Cluster node timeout. */
    
    // 自动生成的配置文件(nodes.conf),用户不能修改,存储了集群状态
    char *cluster_configfile; /* Cluster auto-generated config file name. */
    
    // 集群状态,从当前redis实例视角来看当前集群的状态
    struct clusterState *cluster;  /* State of the cluster */
}

由此可知,集群模式下每个redisServer通过clusterState来描述在它看来整个集群中所有节点的信息与状态。clusterState不仅包含当前节点本身的状态(myself),而且还包含集群内其他节点的状态(nodes)。

另外,比较关键的一点是“在它看来”,因为集群是一个无中心的分布式系统,节点之间通过网络传播信息,而网络并不是百分百可靠的,可能存在分区或断连等问题,所以每个节点维护的集群状态可能不准确或者更新不及时。

以下为clusterState的完整结构,我们在这里先做简单的了解,在稍后的章节中会陆续涉及到这里的内容。

// 这个结构存储的是从当前节点视角,整个集群所处的状态
typedef struct clusterState {
    // 当前节点信息
    clusterNode *myself;  /* This node */
    // 集群的配置纪元
    uint64_t currentEpoch;
    // 集群状态
    int state;            /* CLUSTER_OK, CLUSTER_FAIL, ... */
    // 负责哈希槽主节点的数量
    int size;             /* Num of master nodes with at least one slot */
    // 节点字典:name->clusterNode
    dict *nodes;          /* Hash table of name -> clusterNode structures */
    // 黑名单
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
    // 正在执行迁出的哈希槽及目标节点
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];
    // 正在执行导入的哈希槽及源节点
    clusterNode *importing_slots_from[CLUSTER_SLOTS];
    // 哈希槽与节点的映射关系
    clusterNode *slots[CLUSTER_SLOTS];
    // 每个哈希槽中存储key的数量
    uint64_t slots_keys_count[CLUSTER_SLOTS];
    rax *slots_to_keys;
    /* The following fields are used to take the slave state on elections. */
    // 故障转移授权时间
    mstime_t failover_auth_time; /* Time of previous or next election. */
    // 故障转移获得投票数
    int failover_auth_count;    /* Number of votes received so far. */
    // 是否发起投票
    int failover_auth_sent;     /* True if we already asked for votes. */
    // 
    int failover_auth_rank;     /* This slave rank for current auth request. */
    // 当前故障转移的配置纪元
    uint64_t failover_auth_epoch; /* Epoch of the current election. */
    int cant_failover_reason;   /* Why a slave is currently not able to
                                   failover. See the CANT_FAILOVER_* macros. */

    /* Manual failover state in common. */
    mstime_t mf_end;            /* Manual failover time limit (ms unixtime).
                                   It is zero if there is no MF in progress. */

    /* Manual failover state of master. */
    clusterNode *mf_slave;      /* Slave performing the manual failover. */
    /* Manual failover state of slave. */
    long long mf_master_offset; /* Master offset the slave needs to start MF
                                   or zero if still not received. */

    int mf_can_start;           /* If non-zero signal that the manual failover
                                   can start requesting masters vote. */

    /* The following fields are used by masters to take state on elections. */
    // 最近一次投票的配置纪元
    uint64_t lastVoteEpoch;     /* Epoch of the last vote granted. */
    int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
    /* Messages received and sent by type. */
    long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
    long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
    // 达到PFAIL的节点数量
    long long stats_pfail_nodes;    /* Number of nodes in PFAIL status,
                                       excluding nodes without address. */

} clusterState;

简单说下几个字段,方便理解集群的基础字段:

  • currentEpoch:集群当前纪元,相当于是集群所处的时代,由于重新分片、故障转移等会引起当前纪元升级;
  • myself:数据类型为clusterNode,存储当前节点的状态,稍后解释;
  • nodes:字典类型,以k-v结构存储集群内所有的节点信息,k为节点名称(也叫节点ID),v的数据类型也是clusterNode
  • slots:哈希槽与节点的映射关系,clusterNode数组,以哈希槽编号为索引,指向负责节点;

后面三个字段描述了节点自身的状态,也记录了集群中的其他兄弟节点,同时保存了集群内哈希槽的分配情况。在节点初次启动时,只会存在节点自身的情况,需要等待其他节点加入或者加入已有的集群才会有兄弟节点和哈希槽分配信息。这三个字段都与clusterNode结构有关。

节点属性(clusterNode)

Redis Cluster通过数据结构clusterNode来描述一个集群节点信息与状态。从不同视角来看,它既可以来描述节点自身的状态,也可以用来描述其他节点的状态。

  • 当Redis以集群模式启动后,就会初始化一个clusterNode对象,来维护自身状态。
  • 当节点通过握手或者心跳过程发现其他节点后,也会创建一个clusterNode来记录其他节点的信息。

无论是自身还是其他节点,都会存储在由Redis核心数据结构redisServer维护的clusterState中,随着集群状态的变化而不断更新。

clusterNode维护的信息有些是比较稳定或者是静态的,比如节点ID、ip和端口;也有一些会随着集群状态发生改变,比如节点负责的哈希槽范围、节点状态等。我们以源码+注释的方式来认识一下这个数据结构:

// 这是对集群节点的描述,是集群运作的基础
typedef struct clusterNode {
    // 节点创建时间
    mstime_t ctime; /* Node object creation time. */
    // 节点名称,也叫做节点ID,启动后会存储在node.conf中,除非文件删除,否则不会改变
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    // 节点状态,以状态机驱动集群运作
    int flags;      /* CLUSTER_NODE_... */
    // 节点的配置纪元
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    // 代表节点负责的哈希槽
    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
    // 节点负责哈希槽的数量
    int numslots;   /* Number of slots handled by this node */
    // 如果当前节点是主节点,则存储从节点数量
    int numslaves;  /* Number of slave nodes, if this is a master */
    // 如果当前节点是主节点,则存储从节点列表(数组)
    struct clusterNode **slaves; /* pointers to slave nodes */
    // 如果当前节点是从节点,则存储其主从复制的主节点
    struct clusterNode *slaveof; /* pointer to the master node. Note that it
                                    may be NULL even if the node is a slave
                                    if we don't have the master node in our
                                    tables. */

    // 最近一次发送ping请求的时间
    mstime_t ping_sent;      /* Unix time we sent latest ping */
    // 最近一次接收pong回复的时间
    mstime_t pong_received;  /* Unix time we received the pong */
    // 最近一次接收到数据的时间
    mstime_t data_received;  /* Unix time we received any data */
    // 节点达到FAIL状态的时间
    mstime_t fail_time;      /* Unix time when FAIL flag was set */
    // 故障转移过程中最近一次投票的时间
    mstime_t voted_time;     /* Last time we voted for a slave of this master */
    // 复制偏移更新时间
    mstime_t repl_offset_time;  /* Unix time we received offset for this node */
    mstime_t orphaned_time;     /* Starting time of orphaned master condition */
    // 节点的复制偏移量
    long long repl_offset;      /* Last known repl offset for this node. */
    // 节点的ip地址
    char ip[NET_IP_STR_LEN];  /* Latest known IP address of this node */
    // 节点端口号
    int port;                   /* Latest known clients port of this node */
    // 集群总线端口号
    int cport;                  /* Latest known cluster port of this node. */
    // 与节点的网络链接
    clusterLink *link;          /* TCP/IP link with this node */
    // 报告此节点宕机的节点列表
    list *fail_reports;         /* List of nodes signaling this as failing */
} clusterNode;

再来重点了解几个关键的字段。

  • 节点名称/ID:name,每个节点都有一个唯一的ID,是识别节点的唯一依据。
  • 节点状态:flags。如果你也学习了Redis源码,你会发现很多过程都是通过状态机来驱动的,Redis Cluster中的每个节点为了描述自身或其他节点的状态,以节点状态驱动系统流程,它虽然为int类型,但是其实只使用了低10位,每一位对应一个状态,先来了解一下每一位的作用,在随后的内容会涉及到:
    • CLUSTER_NODE_NULL_NAME,0,对应一个全0的二进制序列,一个新的节点通过握手加入集群时,默认是没有名称的,依次来标识该节点还没有唯一的ID。
    • CLUSTER_NODE_MASTER,1,二进制表示左起第一位是1,表明该节点是主节点;
    • CLUSTER_NODE_SLAVE,2,二进制表示左起第二位是1,表明该节点是从节点;
    • CLUSTER_NODE_PFAIL,4,二进制表示左起第3位是1,表明该节点可能发生宕机,需要其他节点确认;
    • CLUSTER_NODE_FAIL,8,二进制表示左起第4位是1,表明该节点发生宕机;
    • CLUSTER_NODE_MYSELF,16,二进制表示左起第5位是1,表明该节点是存储该对象本身;
    • CLUSTER_NODE_HANDSHAKE,32,二进制表示左起第6位是1,表明该节点处于握手过程中的第一个ping交互;
    • CLUSTER_NODE_NOADDR,64,二进制表示左起第7位是1,表明不知道该节点的网络地址;
    • CLUSTER_NODE_MEET,128,二进制表示左起第8位是1,表明向该节点发送了MEET命令;
    • CLUSTER_NODE_MIGRATE_TO,256,二进制表示左起第9位是1,表明该节点适合做复制迁移;
    • CLUSTER_NODE_NOFAILOVER,512,二进制表示左起第10位是1,表明该节点不会执行故障转移;
  • 配置纪元:configEpoch。与哨兵模式中的“epoch”类似的概念,这里configEpoch描述的是节点的纪元,它与集群的当前纪元currentEpoch可能不同。
  • 节点负责的哈希槽:slots。以bitmap方式存储了节点或者其主节点负责的哈希槽。
  • 主从关系:如果是主节点,slaves存储其副本数量;如果是从节点,slaveof存储其主节点。
  • 集群链接:link,用于维护当前节点与其他节点之间的网络链接。它把clusterState中一个个孤立的节点链接起来,形成网络,是集群总线的基础。

通过对clusterStateclusterNode两个数据结构的了解,我们基本可以从代码层面建立其集群节点中,这种“你中有我,我中有你”、主从复制的逻辑关系,相信你对本节开头结构图中的关系认识更加深刻。

集群总线(Cluster Bus)

集群总线是Redis Cluster内部用于集群治理的专用链路,它由节点与节点之间一条条TCP链接构成。集群内的每个节点都会主动与其他所有节点建立链接,所以每个节点也会被其他所有节点连接。

假如集群有N个节点,那么将会存在N*(N-1)个网络连接,从图论来讲,这就构成了具有N个顶点的有向完全图。用三个节点举例,节点与集群总线的关系如下图所示:Redis专题:初识Redis Cluster的基本结构(1/3)通过前面对集群搭建、节点属性与集群状态的了解,我们可以知道Redis Cluster是一个无中心分布式系统,所以它需要节点之间不断通过信息交换来实现状态一致,而集群总线便是节点之间信息交换的通道。这里的“通道”就是clusterNode中的link,其数据结构为clusterLink

/* clusterLink encapsulates everything needed to talk with a remote node. */
typedef struct clusterLink {
    mstime_t ctime;             /* Link creation time */
    // 与远程节点的网络链接
    connection *conn;           /* Connection to remote node */
    sds sndbuf;                 /* Packet send buffer */
    char *rcvbuf;               /* Packet reception buffer */
    size_t rcvbuf_len;          /* Used size of rcvbuf */
    size_t rcvbuf_alloc;        /* Used size of rcvbuf */
    struct clusterNode *node;   /* Node related to this link if any, or NULL */
} clusterLink;

clusterLink封装了远程节点实例,以及与其的网络链接、接收和发送数据包的信息,基于它两个节点之间就可以保持实时通信了。

需要注意的是:集群总线中所使用的端口并不是我们之前熟悉的6379这样服务于客户端的端口,而是专用的;它不是我们手动设置的,而是由服务于客户端的端口通过偏移计算(+10000)而来。比如,服务于客户端的端口为6379,那么集群总线监听的端口就为16379。

所以,若需要以集群模式部署Redis实例,我们必须保证主机上两个端口都是非占用状态,否则实例会启动失败。

通信协议

到目前为止,我们已经了解了集群节点、集群状态及集群总线。它们为集群的运行提供了基础,接下来就是让节点与节点“动起来”,让他们认识对方、介绍各自的朋友……一切都需要沟通,集群总线已经提供了沟通的通道,我们再来认识一下它们的“语言”。

消息结构

集群消息结构包含消息头和消息体两部分,所有类型消息采用通用的消息头,消息头中包含消息类型字段,根据消息类型追加不同的消息体对象。结合源码和注释了解集群消息结构:

typedef struct {
    // 固定消息头,魔数“RCmb”
    char sig[4];
    // 消息总长度:头+消息体
    uint32_t totlen;
    // 消息版本,目前是1
    uint16_t ver;
    // 对外部客户端提供服务的端口,如6379
    uint16_t port;
    // 消息类型,比如PING、PONG、MEET等,节点需要根据该值追加或解析消息体
    uint16_t type;
    // 
    uint16_t count;
    // 从发送消息的节点来看,当前集群纪元
    uint64_t currentEpoch;  
    // 发送消息节点的配置纪元或其主节点的配置纪元 
    uint64_t configEpoch;   
    // 复制偏移量:对于主节点,是命令传播的复制偏移量;对于从节点,是已处理的来自其主节点的复制偏移
    uint64_t offset;    
    // 发送消息节点的名称/ID
    char sender[CLUSTER_NAMELEN];
    // 发送消息节点负责的哈希槽
    unsigned char myslots[CLUSTER_SLOTS/8];
    // 如果是从节点,该字段放置其主节点的节点名称/ID
    char slaveof[CLUSTER_NAMELEN];
    // 发送消息节点的IP
    char myip[NET_IP_STR_LEN];
    char notused1[34]; 
    // 集群总线监听端口
    uint16_t cport;
    // 发送消息节点的状态
    uint16_t flags;
    // 从发送消息节点的视角来看,当前集群的状态,OK or FAIL
    unsigned char state;
    unsigned char mflags[3];
    // 消息体,根据上面消息类型type,决定该字段存放什么内容
    union clusterMsgData data;
} clusterMsg;

消息头主要包含了消息发送方节点的状态,这样消息接收方可以解析消息并更新本地集群状态中的节点信息。消息体由消息头中的type字段决定,在消息结构中消息体使用了联合体类型clusterMsgData

消息体结构clusterMsgData是联合体,根据type为对应的字段赋值或解析。联合体是C语言的一种数据结构,大家可以把这个字段看作是Java的泛型,由运行时动态指定。

union clusterMsgData {
    /*用于 PING, MEET and PONG三种类型消息 */
    struct {
        /* Array of N clusterMsgDataGossip structures */
        clusterMsgDataGossip gossip[1];
    } ping;

    /* 用于广播节点故障 FAIL */
    struct {
        clusterMsgDataFail about;
    } fail;

    /* PUBLISH */
    struct {
        clusterMsgDataPublish msg;
    } publish;

    /* 用于广播节点哈希槽最新状态UPDATE */
    struct {
        clusterMsgDataUpdate nodecfg;
    } update;

    /* MODULE */
    struct {
        clusterMsgModule msg;
    } module;
};

消息类型

Redis Cluster提供了几种不同的消息类型,然后把几种不同的类型组合使用完成某项功能,比如心跳、握手、配置更新等。我们首先了解几个重要的消息类型及其对应的数据结构,注意:这里说的数据结构仅指整个消息结构中的data部分。

  • PING:用于节点间的心跳请求;
  • PONG:对节点间心跳请求PING的回复;
  • MEET:节点握手请求,是一个特殊的PING类型;

以上三种消息类型共用一种消息结构clusterMsgDataGossip,通过消息头中的type字段来确定是何种类型,该结构描述了一个节点的基础信息及状态,数据结构及说明如下:

typedef struct {
    // 节点ID
    char nodename[CLUSTER_NAMELEN];
    // 消息发送节点对该节点最近一次发送ping请求的时间
    uint32_t ping_sent;
    // 消息发送节点从该节点最近一次接收pong回复的时间
    uint32_t pong_received;
    // 节点IP
    char ip[NET_IP_STR_LEN];  /* IP address last time it was seen */
    // 对外服务端口
    uint16_t port;              /* base port last time it was seen */
    // 集群总线端口
    uint16_t cport;             /* cluster port last time it was seen */
    // 在消息发送节点视角该节点的状态
    uint16_t flags;             /* node->flags copy */
    // 预留字段
    uint32_t notused1;
} clusterMsgDataGossip;

MEET消息仅在节点握手加入集群时用到(后面通过集群建立过程详细说明),PING-PONG组合用于节点间心跳交互(集群容错部分详细说明)。

  • FAIL:用于告知其他节点我(消息发送方)发现nodename节点发生了故障。如果发现某个节点发生故障,源节点将会通过此类型命令向集群内其他所有可达节点发送广播消息。
typedef struct {
    // 发生故障节点的名称
    char nodename[CLUSTER_NAMELEN];
} clusterMsgDataFail;
  • UPDATE:用于告知其他集群节点哈希槽的分配发生变化,节点接收到后需要更新本地集群状态中的哈希槽与节点的映射关系。
typedef struct {
    uint64_t configEpoch; /* Config epoch of the specified instance. */
    char nodename[CLUSTER_NAMELEN]; /* Name of the slots owner. */
    unsigned char slots[CLUSTER_SLOTS/8]; /* Slots bitmap. */
} clusterMsgDataUpdate;
  • FAILOVER_AUTH_REQUEST:从节点发起故障转移投票。
  • FAILOVER_AUTH_ACK:主节点对从节点发起的投票请求确认。

以上两者配合使用,用于从节点选举交互流程,是集群模式故障转移的基础。

集群建立过程

简单的集群示例系统搭建完毕,通过控制台输出我们大体可以了解集群创建的过程;前面也对集群有关的基本概念从理论到代码结构进行了说明。接下来总结一下整个过程,并对节点握手过程详细说明。

总体过程

结合源码流程(函数clusterManagerCommandCreate)及控制台输出,把集群创建的主要过程总结如下:

  • 根据输入参数,redis-ci依次创建集群管理节点,并与每个节点建立网络链接,获取节点及已有集群信息;
  • 依次检查输入的节点,如是否为集群已有节点、节点是否为空;
  • 主从节点分配、哈希槽分配,判断节点是否满足集群创建的条件:至少三个主节点;
  • 输出哈希槽分片及主从节点分配,并在得到用户许可后执行节点配置:
    • 针对主节点:通过CLUSTER ADDSLOTS命令,添加主节点负责的哈希槽范围;
    • 针对从节点:通过CLUSTER REPLICATE命令,创建主从复制关系;
    • 针对所有节点:通过cluster set-config-epoch命令,使其配置纪元(config epoch)加1;
  • redis-cli通过cluster meet命令触发节点握手过程,节点之间通过集群总线(Cluster Bus)传递MEETPINGPONG等信息,逐步建立起集群关系;
  • 通过7000端口的节点检查集群节点及哈希槽分配情况。
  • 还有非常重要的一点上图没有体现到:当集群建立以后,节点之间就会通过集群总线,使用二进制协议Gossip不断进行“闲聊”,以此完成节点发现、健康检查、故障检测、故障转移、配置更新、从节点迁移等工作。

接下来,我们对数据分片、主从分配、配置纪元、节点握手几个核心步骤做进一步分析。

主从分配及数据分片

  • 计算主节点数量。

根据输入的节点信息及从节点数量要求,redis-cli计算主节点的数量,然后把所有节点按照一主N从进行分组。假设输入节点的数量为n,要求每个主节点的副本数为r,则理论上可以分为:其中,m为右边计算结果向下取整。按照集群模式的要求,至少需要3个主节点。如果m<3,则提示创建失败。

  • 分配主从节点。

redis-cli根据输入节点的ip分布,优先考虑把主节点分配在不同的主机上,选择m个主节点,然后按照从节点数量r为主节点分配从节点。按照指定的从节点数量r分配完成后,如果还有剩余的节点,则再次执行从节点分配。

由于新节点启动之后,默认是主节点,主从节点分配完成后,redis-cli只需要为从节点设置主节点,等待执行配置即可。

  • 数据分片

数据分片时默认会把16384个哈希槽均分给主节点,不再详细展开了。

配置下发

主从分配及数据分片完成后,redis-cli已经在本地为节点保存了主从配置及数据分片配置信息,得到管理员的许可后,会遍历节点列表把配置下发至对应的节点。

如果是主节点,则执行数据分片配置。redis-cli使用CLUSTER ADDSLOTS命令设置节点负责的哈希槽;主节点接收后,会进行如下修改操作:

  • 如果集群状态clusterState->importing_slots不为空,则设置为NULL;
  • 修改myself的slots字段,以bitmap方式设置当前节点负责的哈希槽范围;

如果是从节点,则为其设置主节点。redis-cli向从节点发送CLUSTER REPLICATE命令;从节点接收后,执行主从复制,不再赘述。

升级纪元

经过以上配置,各个节点已经不是刚刚启动时的状态,为了表明这种变化,redis-cli把各个节点的配置纪元升级,该命令为cluster set-config-epoch

节点接收命令后,将修改myself的configEpoch,并确保集群的currentEpoch不低于此值。

节点握手

到这里,每个单独的节点已经配置完成,接下来redis-cli会向节点发起握手命令,从零开始把节点逐个加入集群。为了安全考虑,目前节点间的握手仅能通过管理员发起,握手过程通过集群总线完成。

节点启动后监听集群总线端口,会接受一切外来的网络链接并接收其发送的消息,但是如果发现消息来源节点不是集群已知节点,其发送的所有消息将被丢弃。集群已有节点接受新节点加入集群只有两种方式:

  • MEET请求:新节点以MEET消息发送请求,说明是由管理员发起的扩容命令,通过握手过程加入集群。
  • 自动发现:如果一个节点被集群中某个节点认可其是集群的有效节点,然后通过节点间的心跳告知其他节点,其他节点也会认为其是集群的有效节点。比如已知集群中有A、B、C三个节点,通过MEET请求A认可了D,通过一段时间的心跳,B、C也会接受D作为集群的节点。

结合以上两种方式,我们只需要从第二个节点以后的节点依次与第一个节点握手,再通过自动发现即可实现所有节点加入集群。

好了,我们了解一下握手过程是如何实现的,为了简单,我们仅以两个节点为例描述其过程。假设节点信息如下:

  • 节点A:127.0.0.1 7000
  • 节点B:127.0.0.1 7001

通过redis-cli发起meet命令,让节点B与节点A握手,命令为cluster meet 127.0.0.1 7000

B节点接收命令后,开始与A节点的握手过程,为了方便清晰的了解握手过程中节点状态的变化,通过下图进行说明。Redis专题:初识Redis Cluster的基本结构(1/3)图示显示了握手过程中,两个节点经过“MEET-PONG-PING-PONG”两次交互完成握手,状态变化一目了然。用文字描述一下过程:

  • B节点创建握手节点A的节点信息,初始化时无名称,状态为MEETHANDSHAKE
  • B节点创建与A节点的集群总线连接,主动发起MEET请求,而后A节点回复PONG。此时:
    • 对于B,取消A的MEET状态,获取A的名称;
    • 对于A,B以HANDSHAKE、无名称的状态加入节点列表。
  • A节点创建与B节点的集群总线连接,主动发起PING请求。而后B节点回复PONG。此时:
    • 对于B,取消A的HANDSHAKE状态
    • 对于A,取消B的HANDSHAKE状态,设置名称。
  • 至此,A、B节点握手完成,之后进入正常的心跳保持过程。

集群结构总结

这部分主要是打基础,把集群的一些基本概念介绍一下,同时介绍了Redis Cluster的物理结构和逻辑结构,通过实例与集群建立过程的说明,给大家一个比较直观的理解。

更加精彩的内容在后面~

推荐阅读



原文始发于微信公众号(码路印记):Redis专题:初识Redis Cluster的基本结构(1/3)