从Ping-Pong消息学习Gossip协议的实现

Gossip 协议的基本工作机制

在一个使用了 Gossip 协议的集群中,每个集群节点会维护一份集群的状态信息,包括集群中各节点的信息、运行状态,以及数据在各节点间的分布情况。

当集群节点按照 Gossip 协议工作时,每个节点会以一定的频率从集群中随机挑选一些其他节点,把自身的信息和已知的其他节点信息,用 PING 消息发送给选出的节点。而其他节点收到 PING 消息后,也会把自己的信息和已知的其他节点信息,用 PONG 消息返回给发送节点

Gossip 协议正是通过这种随机挑选通信节点的方法,让节点信息在整个集群中传播。

Redis 是如何实现 Gossip 通信的

节点通信的常见消息有哪些

arduino 复制代码
#define CLUSTERMSG_TYPE_PING 0  //Ping消息,用来向其他节点发送当前节点信息
#define CLUSTERMSG_TYPE_PONG 1  //Pong消息,对Ping消息的回复
#define CLUSTERMSG_TYPE_MEET 2  //Meet消息,表示某个节点要加入集群
#define CLUSTERMSG_TYPE_FAIL 3  //Fail消息,表示某个节点有故障

Ping 消息,这是一个节点用来向其他节点发送信息的消息类型,而 Pong 是对 Ping 消息的回复。Meet 消息是一个节点表示要加入集群的消息类型,而 Fail 消息表示某个节点有故障。

Redis 定义了一个结构体 clusterMsg,它用来表示节点间通信的一条消息。它包含的信息包括发送消息节点的名称、IP、集群通信端口和负责的 slots,以及消息类型、消息长度和具体的消息体。

arduino 复制代码
typedef struct {
   ...
   uint32_t totlen;    //消息长度
   uint16_t type;     //消息类型
   ...
   char sender[CLUSTER_NAMELEN];  //发送消息节点的名称
   unsigned char myslots[CLUSTER_SLOTS/8]; //发送消息节点负责的slots
   char myip[NET_IP_STR_LEN];  //发送消息节点的IP
   uint16_t cport;      //发送消息节点的通信端口
   ...
   union clusterMsgData data;  //消息体
} clusterMsg;

clusterMsgData,这个数据结构正是定义了节点间通信的实际消息体。它包含了多种消息类型对应的数据结构,包括 clusterMsgDataGossip、clusterMsgDataFail、clusterMsgDataPublish 和 clusterMsgDataUpdate。

arduino 复制代码
union clusterMsgData {
    //Ping、Pong和Meet消息类型对应的数据结构
    struct {
        clusterMsgDataGossip gossip[1];
    } ping;
 
    //Fail消息类型对应的数据结构
    struct {
        clusterMsgDataFail about;
    } fail;
 
    //Publish消息类型对应的数据结构
    struct {
        clusterMsgDataPublish msg;
    } publish;
 
    //Update消息类型对应的数据结构
    struct {
        clusterMsgDataUpdate nodecfg;
    } update;
 
    //Module消息类型对应的数据结构
    struct {
        clusterMsgModule msg;
    } module;
};

clusterMsgDataGossip 数据结构定义如下所示:

arduino 复制代码
typedef struct {
    char nodename[CLUSTER_NAMELEN]; //节点名称
    uint32_t ping_sent;  //节点发送Ping的时间
    uint32_t pong_received; //节点收到Pong的时间
    char ip[NET_IP_STR_LEN];  //节点IP
    uint16_t port;              //节点和客户端的通信端口
    uint16_t cport;             //节点用于集群通信的端口
    uint16_t flags;             //节点的标记
    uint32_t notused1;    //未用字段
} clusterMsgDataGossip;

Ping 消息的生成和发送

Redis 的 serverCron 函数是在周期性执行的。而它会调用 clusterCron 函数(在 cluster.c 文件中)来实现集群的周期性操作,这就包括了 Gossip 协议的通信。

scss 复制代码
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
   ...
   run_with_period(100) {
      //每100ms调用依次clusterCron函数
      if (server.cluster_enabled) clusterCron();  
   }
   ...
}

clusterCron 函数的一个主要逻辑就是每经过 10 次执行,就会随机选五个节点,然后在这五个节点中,遴选出最早向当前节点发送 Pong 消息的那个节点,并向它发送 Ping 消息。而 clusterCron 函数本身是每 1 秒执行 10 次,所以,这也相当于是集群节点每 1 秒向一个随机节点发送 Gossip 协议的 Ping 消息。

ini 复制代码
void clusterCron(void) {
   ...
   if (!(iteration % 10)) { //每执行10次clusterCron函数,执行1次该分支代码
   int j;
   for (j = 0; j < 5; j++) { //随机选5个节点
            de = dictGetRandomKey(server.cluster->nodes);
            clusterNode *this = dictGetVal(de);
 
      //不向断连的节点、当前节点和正在握手的节点发送Ping消息
      if (this->link == NULL || this->ping_sent != 0) continue;
      if (this->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_HANDSHAKE))
         continue;
      //遴选向当前节点发送Pong消息最早的节点
      if (min_pong_node == NULL || min_pong > this->pong_received) {
         min_pong_node = this;
         min_pong = this->pong_received;
      }
    }
    //如果遴选出了最早向当前节点发送Pong消息的节点,那么调用clusterSendPing函数向该节点发送Ping消息
    if (min_pong_node) {
       serverLog(LL_DEBUG,"Pinging node %.40s", min_pong_node->name);
       clusterSendPing(min_pong_node->link, CLUSTERMSG_TYPE_PING);
    }
  }
  ...
}

向其他节点发送 Ping 消息的函数是 clusterSendPing,而实际上,Ping 消息也是在这个函数中完成构建和发送的。 clusterSendPing 函数的主要逻辑可以分成三步,分别是:构建 Ping 消息头、构建 Ping 消息体和发送消息。

  • 第一步,构建 Ping 消息头

clusterSendPing 函数会调用 clusterBuildMessageHdr 函数来构建 Ping 消息头,如下所示:

rust 复制代码
if (link->node && type == CLUSTERMSG_TYPE_PING)
   link->node->ping_sent = mstime(); //如果当前是Ping消息,那么在发送目标节点的结构中记录Ping消息的发送时间
clusterBuildMessageHdr(hdr,type); //调用clusterBuildMessageHdr函数构建Ping消息头

clusterBuildMessageHdr 函数会设置 clusterMsg 结构体中的各个成员变量,比如消息类型,发送消息节点的名称、IP、slots 分布等信息。

  • 第二步,构建 Ping 消息体

clusterMsgData当它表示 Ping、Pong 消息时,其实是一个 clusterMsgDataGossip 类型的数组,这也就是说,一个 Ping 消息中会包含多个 clusterMsgDataGossip 结构体,而每个 clusterMsgDataGossip 结构体实际对应了一个节点的信息。

arduino 复制代码
union clusterMsgData {
    struct {
        //当消息是Ping或Pong时,使用clusterMsgDataGossip类型的数组
        clusterMsgDataGossip gossip[1];
  } ping;
  ...
}

当 clusterSendPing 函数构建 Ping 消息体时,它会将多个节点的信息写入 Ping 消息。那么,clusterSendPing 函数具体会写入多少个节点的信息呢?这其实是由三个变量控制的,分别是 freshnodes、wanted 和 maxiterations。

其中,freshnodes 的值等于集群节点数减 2。

而 wanted 变量的值和 freshnodes 大小也有关,wanted 的默认值是集群节点数的 1/10,但是如果这个默认值小于 3,那么 wanted 就等于 3。如果这个默认值大于 freshnodes,那么 wanted 就等于 freshnodes 的大小,这部分的计算逻辑如下所示:

ini 复制代码
wanted = floor(dictSize(server.cluster->nodes)/10);
if (wanted < 3) wanted = 3;
if (wanted > freshnodes) wanted = freshnodes;

maxiterations 的值就等于 wanted 的三倍大小。

clusterSendPing 会根据这三个值的大小,执行一个循环流程,在这个循环中,它每次从集群节点中随机选一个节点出来,并调用 clusterSetGossipEntry 函数为这个节点设置相应的 Ping 消息体,也就是 clusterMsgDataGossip 结构。

ini 复制代码
while(freshnodes > 0 && gossipcount < wanted && maxiterations--) {
   dictEntry *de = dictGetRandomKey(server.cluster->nodes);
   clusterNode *this = dictGetVal(de);
   ...
   clusterSetGossipEntry(hdr,gossipcount,this); //调用clusterSetGossipEntry设置Ping消息体
   freshnodes--;
   gossipcount++;
}

第三步,发送 Ping 消息

clusterSendPing 函数主体逻辑的最后一步就是调用 clusterSendMessage 函数,将 Ping 消息发送给随机选出的目标节点。

Ping 消息的处理和 Pong 消息的回复

节点在调用 clusterSendPing 函数向其他节点发送 Ping 消息前,会检查它和其他节点连接情况,如果连接断开了,节点会重新建立连接.

ini 复制代码
void clusterCron(void) {
...
di = dictGetSafeIterator(server.cluster->nodes);
while((de = dictNext(di)) != NULL) {
   clusterNode *node = dictGetVal(de);
   ...
   if (node->link == NULL) {
    ...
    fd = anetTcpNonBlockBindConnect(server.neterr, node->ip, 
                node->cport, NET_FIRST_BIND_ADDR);
  ...
  link = createClusterLink(node);
  link->fd = fd;
  node->link = link;
  aeCreateFileEvent(server.el,link->fd,AE_READABLE, clusterReadHandler,link);
  ...
  }
  ...
}
...
}

一个节点在和其他节点建立的连接上,设置的监听函数是 clusterReadHandler。所以,当一个节点收到 Ping 消息时,它就会在 clusterReadHandler 函数中进行处理,我们来看下这个函数。

clusterReadHandler 函数执行一个 while(1) 循环,并在这个循环中读取收到的消息,当读到一个完整的消息后,它会调用 clusterProcessPacket 函数处理这个消息,如下所示:

scss 复制代码
void clusterReadHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
...
while(1) { //持续读取收到的数据
   rcvbuflen = sdslen(link->rcvbuf);
   ...
   nread = read(fd,buf,readlen); //读取收到的数据
   ...
   //读取到一个完整的消息
   if (rcvbuflen >= 8 && rcvbuflen == ntohl(hdr->totlen)) {
   if (clusterProcessPacket(link)) { ...} //调用clusterProcessPacket函数处理消息
   ...
}
}

当收到的是 Ping 消息时,clusterProcessPacket 函数会先调用 clusterSendPing 函数,向 Ping 消息发送节点返回 Pong 消息,如下所示:

scss 复制代码
int clusterProcessPacket(clusterLink *link) {
   ...
   if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_MEET) {
      ... //处理Meet消息,将发送Meet消息的节点加入本地记录的节点列表中
      clusterSendPing(link,CLUSTERMSG_TYPE_PONG); //调用clusterSendPing函数返回Pong消息。
   }
   ...
}

Ping 和 Pong 消息使用的是同一个函数 clusterSendPing 来生成和发送的,所以它们包含的内容也是相同的。

下面的代码就展示了节点收到 Ping-Pong 消息后,对本地信息进行更新的代码分支:

scss 复制代码
int clusterProcessPacket(clusterLink *link) {
   ...
   if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG ||
        type == CLUSTERMSG_TYPE_MEET)
  {
     ...
     //当收到Pong消息时,更新本地记录的目标节点Pong消息最新返回时间
       if (link->node && type == CLUSTERMSG_TYPE_PONG) {
          link->node->pong_received = mstime();
          ...
  }
  ...//如果发送消息的节点是主节点,更新本地记录的slots分布信息
  //调用clusterProcessGossipSection函数处理Ping或Pong消息的消息体
  if (sender) clusterProcessGossipSection(hdr,link);
  }
  ...
}

此文章为10月Day26学习笔记,内容来源于极客时间《Redis 源码剖析与实战》

相关推荐
努力的小雨2 小时前
从“Agent 元年”到 AI IDE 元年——2025 我与 Vibe Coding 的那些事儿
后端·程序员
源码获取_wx:Fegn08953 小时前
基于springboot + vue小区人脸识别门禁系统
java·开发语言·vue.js·spring boot·后端·spring
wuxuanok3 小时前
Go——Swagger API文档访问500
开发语言·后端·golang
用户21411832636023 小时前
白嫖Google Antigravity!Claude Opus 4.5免费用,告别token焦虑
后端
爬山算法4 小时前
Hibernate(15)Hibernate中如何定义一个实体的主键?
java·后端·hibernate
用户26851612107565 小时前
常见的 Git 分支命名策略和实践
后端
程序员小假5 小时前
我们来说一下 MySQL 的慢查询日志
java·后端
南囝coding5 小时前
《独立开发者精选工具》第 025 期
前端·后端
To Be Clean Coder5 小时前
【Spring源码】从源码倒看Spring用法(二)
java·后端·spring