Redis第二十二讲 Redis高可用集群节点通信机制

本文详细介绍了Redis集群中的节点通信机制,包括每个节点的两个端口——普通端口和集群端口的作用。节点间采用Gossip协议进行通信,以分散元数据更新的压力并保证容错性。主要内容包括节点的MEET、PING、PONG、FAIL和PUBLISH消息类型及其作用。此外,还探讨了数据结构`clusterNode`和`clusterState`以及`cluster meet`和`cluster addslots`命令的实现过程。

两个端口

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

  • 普通端口:即我们在前面指定的端口(7000等)。普通端口主要用于为客户端提供服务(与单机节点类似);但在节点间数据迁移时也会使用。
  • 集群端口:端口号是普通端口+10000(10000是固定值,无法改变),如7000节点的集群端口为17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。

节点通信机制

维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:集中式和gossip ;
Redis cluster节点间采取gossip协议进行通信 。

  • 集中式(广播)
    向集群内所有节点发送消息; 优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;
    不足在于所有的元数据的更新压力全部集中在一个地方,每条消息都要发送给所有节点,CPU、带宽等消耗较大;可能导致元数据的存储压力。 很多中间件都会借助zookeeper集中式存储元数据。

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

在这里插入图片描述
gossip协议的优缺点

  • gossip协议的优点在于元数据的更新比较分散(负载),不是集中在一个地方(去中心化),更新请求会陆陆续续,打到所有节点上去更新(容错性高),有一定的延时,降低了压力;
  • 缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后(收敛速度慢)。

gossip节点间发送的消息主要分为5种:meet消息、ping消息、pong消息、fail消息、publish消息。
不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的。

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

gossip通信的10000端口

  • 每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口。
  • 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他节点点接收到ping消息之后返回pong消息。

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

数据结构

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

节点为了存储集群状态而提供的数据结构中,最关键的是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还包括故障转移、槽迁移等需要的信息。

集群命令的实现

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

cluster meet

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

  1. A为B创建一个clusterNode结构,并将其添加到clusterState的nodes字典中

  2. A向B发送MEET消息

  3. B收到MEET消息后,会为A创建一个clusterNode结构,并将其添加到clusterState的nodes字典中

  4. B回复A一个PONG消息

  5. A收到B的PONG消息后,便知道B已经成功接收自己的MEET消息

  6. 然后,A向B返回一个PING消息

  7. B收到A的PING消息后,便知道A已经成功接收自己的PONG消息,握手完成

  8. 之后,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节点,具体执行过程如下:

  1. 遍历输入槽,检查它们是否都没有分配,如果有一个槽已分配,命令执行失败;方法是检查输入槽在clusterState.slots[]中对应的值是否为NULL。

  2. 遍历输入槽,将其分配给节点A;方法是修改clusterNode.slots[]中对应的比特为1,以及clusterState.slots[]中对应的指针指向A节点

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员路同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值