前面已经知道了集群是怎么进行故障转移操作和如何进行选举从节点的,而且这两个过程还有前面的一些过程都离不开一个操作,那就是发送消息。
消息
集群中的各个节点(包括从节点)是通过发送消息和接受消息来进行通信的,称发送消息的节点为发送者,接收消息的节点为接收者。
发送的消息类型主要有以下5种,这5种在前面的文章都提到过
- MEET消息:当发送者接收到客户端传来的CLUSTER MEET命令后,会向指定的REDIS服务器发送MEET消息,请求接收者加入到发送者当前所处的集群里面
- PING消息:集群中的每个节点会默认每隔一秒钟,从自己的clusterState.nodes属性中选择5个已知节点,然后在这5个节点又选出最长时间自己没有发送过PING消息的节点出来,进行发送PING消息给它,以此来检测节点是否在线,同时也会更新对接收节点的状态认知,除此之外,这5个节点中如果存在返回PONG消息的时间距离当前时间的时间差的节点,已经超过了cluster-node-timeout选项设置时长的一半,那么也会给这个节点发送PING消息,来防止对该节点的信息更新滞后
- PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,接收者为了表示已经收到这条消息,就会返回一条PONG消息给发送者,除此之外,节点也可以通过集群广播自己的PONG消息来让集群中的其他节点刷新对自己的认识,比如故障转移操作之后,这个节点成为了新的主节点,那么这个新主节点就会广播自己的PONG消息
- FAIL消息:当一个主节点判断另一个主节点进入了FAIL状态时(下线),该主节点会向集群中广播一条关于下线主节点的FAIL消息,所有收到这条消息的节点(包括从节点)会将该下线主节点标记为下线状态(flag属性)
- PUBLISH消息:当节点接收到PUBLISH命令时,那么节点会执行这个命令,并且会向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令
一条消息由**消息头(header)和消息正文(data)**组成。
消息头
节点发送的所有消息都由一个消息头包裹,消息头包含了消息正文,消息的一些状态,还记录了一些发送者自身的一些信息,因为这些信息会被接收者用到,所以严格来讲,消息头可以算是消息的一部分,每个消息头都是一个clusterMsg结构
typedef clusterMsg(
//消息的长度,包括消息头和消息正文的长度
uint32_t totlen;
//消息的类型(5种类型种的哪一种)
uint16_t type;
//消息正文包含的节点信息数量
//只在发送MEET、PING、PONG这三种Gossip协议消息时使用
uint16_t count;
//发送者所处的配置纪元
uint64_t currentEpoch;
//这个数据也是记录配置纪元,只不过有些不同
//如果这个是从节点,记录的是正在复制主节点的配置纪元
//如果这个是主节点,记录的就是自己的配置纪元
uint64_t configEpoch;
//发送者的名字(ID)
char sender[REDIS_CLUSTER_NAMELEN];
//发送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
//记录的是主节点名字
//如果这个是从节点,那么这里记录的是正在复制的主节点的名字
//如果这个是主节点,那么这里记录的是REDIS_NODE_NULL_NAME
//REDIS_NODE_NULL_NAME是一个40字节长,但值全为0的字节数组
char slaveof[REDIS_CLUSTER_NAMELEN];
//发送者的端口号
uint16_t port;
//发送者的标识值(主还是从,还是正在处于MEET等)
unsigned flags;
//发送者所处集群的状态(上线还是下线)
unsigned char state;
//消息的正文
union clusterMsgData data;
)clusterMsg;
可以看到使用的是data属性保存消息的正文,指向的是一个clusterMsgData结构(这个结构保存着所有5种消息,而每种消息都由对应的结构去储存)
typedef clusterMsgData(
//MEET、PING、PONG消息的正文
struct(
//每条MEET、PING、PONG消息都包含两个
//clusterMsgDataGossip结构
clusterMsgDataGossip gossip[1];
)
//FAIL消息的正文
struct(
clusterMsgDataFail about;
)fail
//PUBLISH消息的正文
struct(
clusterMsgDataPublish msg;
)
//...其他消息的正文
)
可以看到,在消息头里面,有着一系列发送者的信息,那么接收者就可以根据这些信息去对应找到clusterState.nodes里面的clusterNode结构,并且对应去更新里面的信息,比如修改里面的槽指派信息,还有flag标识。
MEET、PING、PONG消息的实现
Redis集群中的各个节点通过Gossip协议来交换各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种信息来实现,这三种消息的正文都由clusterMsgDataGossip结构组成的
union clusterMsgData(
//...
//MEET、PING、PONG消息的正文
struct(
//每条MEET、PING、PONG消息都包含两个clusterMsgDataGossip结构
clusterMsgDataGossip gossip[1];
)ping;
//其他消息的正文
)
这里可能就在想,那么如何区分MEET、PING、PONG消息呢?
这个是通过消息头里面的type属性来判断一条消息是MEET消息、PING消息还是PONG消息
每次发送MEET、PONG、PING消息时,发送者都从自己的已知节点列表中随机选出两个节点(可以是主节点也可以是从节点),并将这两个被选中节点的信息分别保存到clusterMsgDataGossip结构里面,所以每条MEET、PONG、PING消息都包含两个clusterMsgDataGossip结构
clusterMsgDataGossip结构记录了被选中节点的名字,发送者与被选中节点最后一次发送和接收PING消息和PONG消息的时间戳,被选中节点的IP地址和端口号,以及被选中节点的标识值
typedef struct clusterMsgDataGossip(
//被选中节点的名字
char nodename[REDIS_CLUSTER_NAMELEN];
//发送者最后一次向该节点发送PING消息的时间戳
uint32_t ping_sent;
//发送者最后一次从该节点接收PONG消息的时间戳
uint32_t pong_received;
//节点的IP地址
char ip[16];
//节点的端口号
uint16_t port;
//节点的标识值
uint16_t flags;
)clusterMsgDataGossip;
当接收者收到MEET、PING、PONG消息时,接收者会访问消息正文里面的两个clusterMsgDataGossip结构,并根据,然后对应去搜索自己的clusterState.node属性,看自己是否认识clusterMsgDataGossip里面的被选中的节点,然后对应进行下列的操作
- 如果被选中的节点不存在于接受者的已知节点链表里面,那么就代表接收者是第一次接触到被选中的节点,那么就要进行握手操作,接收者要根据结构中记录的IP地址和端口号等信息,与被选中的节点进行握手
- 如果被选中的节点存在于接收者的已知节点链表里面,那么说明接收者已经接触过被选中的节点,那么就要进行更新操作,接收者要根据结构中记录的信息,对被选中的节点进行一个状态更新,及更新对应的clusterNode结构
- 但对于MEET消息,是不是也应该要跟发送方进行握手呢?
FAIL消息的实现
当集群里的主节点A将主节点B标记为下线时,会通过集群广播一条关于主节点B的FAIL消息,所有接收到这条消息的节点(包括主从节点)都会将主节点B标记为下线
FAIL消息不使用Gossip协议去实现,因为Gossip协议在节点数量比较多的集群里面会有延迟(因为每次只告诉一个节点,而且只告诉发送者认知的两个节点的信息),但节点已下线消息需要实时性比较强,所以使用FAIL消息进行发送
FAIL消息是使用clusterMsgDataFail结构表示,而这个结构只包含一个nodename属性,用来记录已下线节点的名字
typedef struct clusterMsgDataFail(
char nodename[REDIS_CLUSTER_NAMELEN];
)clusterMsgDataFail
因为集群里的所有节点都有一个独一无二的名字(回看前面的文章,从节点的名字是ip地址加端口号),所以FAIL消息里面只需要保存下线节点的名字即可,接收到FAIL消息的节点就可以根据这个名字去判断哪个节点下线了,然后进行对应的下线操作(将节点标记为已下线)。
PUBLISH消息的实现
当客户端向集群中的某个主节点发送命令:
PUBLISH <channel> <msg>
那么接收到PUBLISH命令的主节点不仅会执行这条命令(即发送消息到对应的频道),还会通过集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行命令(即发送消息到对应的频道)
也就是说,上面的那条命令操控的不是一个主节点,而是整个主节点所在的集群,集群中的所有节点都会向指定的频道发送消息。
PUBLISH消息的正文由clusterMsgDataPublish正文表示
typedef struct(
//channel参数的长度(字节,通常一个英文或符号代表1字节)
uint32_t channel_len;
//msg参数的长度(字节,通常一个因为或符号代表1字节)
uint32_t message_len;
//保存的命令
//这里并不一定是8字符长度,按给的命令来计算的
//bulk:主体
unsigned char bulk_data[8];
)clusterMsgDataPublish;
前面两个属性都比较简单,下面就重点去认识以下bluk_data数组
- bluk_data数组的0字节到channel_len -1字节保存的是channel参数
- bluk_data数组的channel_len字节到message_len字节保存的是message参数
举个栗子
比如节点收到客户端的命令
publish "ember" "hello"
那么就会生成下面的PUBLISH消息
可以看出,其实channel_len与message_len充当一个记录bulk_data数组索引的作用
为什么不直接广播发送PUBLISH命令
最简单的方法的确是直接广播发送PUBLISH命令,但这种做法不符合集群中的**“各个节点通过发送和接收消息进行通信”**这一规则,所以就使用了PUBLISH消息来进行包装