Redis Cluster Gossip 协议深度解析
前言
Gossip 协议,这个名字听起来像办公室八卦。
但它的内核,远比“八卦”二字深刻得多。
Redis Cluster 选择了 Gossip,而不是 ZooKeeper 那样的集中式协调器。为什么?
三个核心原因:去中心化、弹性扩展、最终一致。
集中式方案有一个明显的软肋——元数据存储节点成为单点瓶颈。而 Gossip 把状态更新的压力分散到每一个节点上,集群规模可以水平扩展到数百甚至上千个节点,每个节点的通信负载几乎恒定不变。
本文将从四个维度彻底拆解 Redis 的 Gossip 实现:
-
数学根基:Gossip 为什么收敛?收敛速度有多快?
-
消息结构:
clusterMsg结构体的每个字节在做什么? -
运行时引擎:
clusterCron如何驱动整个协议运转? -
故障检测状态机:从 PFAIL 到 FAIL,到底经历了什么?
读完这篇文章,你不仅能看懂 Redis 的集群通信源码,还能理解为什么这种“八卦算法”在分布式系统中无处不在。
第一章:Gossip 的数学根基
1.1 什么是 Gossip 协议?
Gossip 协议,又称 Epidemic Protocol(流行病协议)。这个名字很形象:信息在节点间的传播方式,和病毒在人群中的传播一模一样。
核心机制异常简单:
每个节点周期性地随机选择若干个其他节点,交换彼此已知的状态信息。经过若干轮之后,所有节点的状态收敛到一致。
Gossip 协议的起源可以追溯到 Xerox PARC(施乐帕洛阿尔托研究中心)的一篇论文,最初被称为“八卦算法”或“病毒算法”。
下面这张图展示了三种 Gossip 传播模式的演进:

-
直接邮寄(Direct Mail):节点状态变化时主动推送给邻居,但消息可能丢失,不保证最终一致。
-
反熵(Anti-Entropy):节点间定期交换全部数据摘要并修复差异,能保证最终一致,但通信开销大。
-
谣言传播(Rumor Mongering):有新信息时积极传播,信息传遍全网后转为被动模式,是 Redis 实际采用的策略。
1.2 收敛速度的数学保证
Gossip 协议最迷人的特性是什么?指数级的收敛速度。
在均匀随机选点、fanout(每次传播的目标数)为常数的经典模型下,信息传播到全网所需的轮次期望为 O(log N),其中 N 是节点总数。
举个例子:假设集群有 100 个节点,每个节点每次随机选 3 个节点交换信息。那么一条新信息传遍全网的期望轮次大约是 5-7 轮。如果每秒执行一轮,那就是 5-7 秒。
中国科学院的研究者进一步证明:在满足有限时间收敛的条件下,随机 Gossip 算法能保证在有限时间收敛内一定会收敛,而且是理论上可能存在的最快算法。
这个数学保证意味着什么?
Gossip 协议天然适合大规模分布式系统。节点数从 10 增长到 10000,信息传播延迟只从 O(log 10) 增长到 O(log 10000)——也就是从约 3 轮变成约 14 轮,远远慢于线性增长。
1.3 与 SWIM 协议的关系
这里必须提到一个重要的概念:SWIM(Scalable Weakly-consistent Infection-style Process Group Membership Protocol)。
SWIM 是 Gossip 协议在成员管理领域的一个经典实现。它把成员管理问题拆成了两个独立组件:
-
故障检测组件(Failure Detector):负责检测节点是否存活。
-
传播组件(Dissemination Component):负责将成员变更信息传播到全网。
这种“检测-传播”分离的设计,后来被 Redis Cluster、Consul、Cassandra 等大量分布式系统借鉴。Redis Cluster 的 PFAIL → FAIL 两阶段故障检测,本质上就是 SWIM 思想的工程实践。
第二章:消息结构——clusterMsg
2.1 总线端口:为什么是 16379?
在深入消息结构之前,先明确一个常被忽略的细节。
Redis Cluster 中每个节点都有两个端口:
-
数据端口:默认 6379,用于处理客户端请求。
-
集群总线端口(Cluster Bus Port):数据端口 + 10000,默认 16379,用于节点间 Gossip 通信。
为什么需要独立的总线端口?
因为集群内部通信使用二进制协议,而客户端通信使用 RESP 文本协议。将它们分离到不同端口,可以避免协议混淆,也便于网络层的流量控制和监控。集群总线使用的协议就是 RCmb(Redis Cluster message bus)——我们马上会看到这个魔数。
2.2 clusterMsg 结构体全景
Gossip 协议在 Redis 中的核心数据结构是 clusterMsg,定义在 cluster.h 中。下面给出它的完整结构(基于 Redis 7.0 源码),并附上每个字段的详细注释:
typedef struct {
// ========== 消息头(Header) ==========
char sig[4]; // 魔数 "RCmb"(Redis Cluster message bus)
uint32_t totlen; // 消息总长度(字节)
uint16_t ver; // 协议版本,当前为 1
uint16_t port; // 发送者的数据端口(如 6379)
uint16_t type; // 消息类型:PING/PONG/MEET/FAIL/UPDATE...
uint16_t count; // gossip 部分包含的节点数量
// ========== 发送者基本信息 ==========
uint64_t currentEpoch; // 发送者视角下的集群统一纪元(用于选举)
uint64_t configEpoch; // 主节点唯一的配置纪元(冲突时自增)
uint64_t offset; // 主从复制偏移量
char sender[CLUSTER_NAMELEN]; // 发送者节点 ID(40 字节)
unsigned char myslots[CLUSTER_SLOTS/8]; // 位图,标识发送者负责的槽(16384 bits = 2048 bytes)
char slaveof[CLUSTER_NAMELEN]; // 如果是从节点,记录其主节点 ID
char myip[NET_IP_STR_LEN]; // 发送者 IP 地址
char notused1[34]; // 预留字段(向后兼容)
// ========== 更多元信息 ==========
uint16_t cport; // 发送者的集群总线端口(port + 10000)
uint16_t flags; // 发送者节点标志(MASTER/SLAVE/PFAIL/FAIL...)
unsigned char state; // 发送者视角下的集群状态
unsigned char mflags[3]; // 消息标志(如 CLUSTERMSG_FLAG0_PAUSED)
// ========== 消息体(Union,按类型不同而不同) ==========
union clusterMsgData data;
} clusterMsg;
这个结构体大约 2.5KB。其中最大的一块是 myslots 字段(2048 字节),它用位图的方式表示 16384 个槽的归属。每一位代表一个槽,1 表示该槽由当前节点负责。
下面这张图直观展示了 clusterMsg 的内存布局:

2.3 字段深潜:epoch、flags 和 myslots
currentEpoch vs configEpoch:这是最容易混淆的两个字段。
-
configEpoch:每个主节点独有的标识,类似于“节点版本号”。当两个主节点声称负责同一个槽时,configEpoch较大的那个胜出,另一个则被迫自增。 -
currentEpoch:集群级别的统一纪元,用于选举投票。它类似于 Raft 中的 term,确保整个集群在同一个时间线上达成共识。
flags:节点状态标志位。常见的取值包括:
| 标志 | 含义 |
|---|---|
CLUSTER_NODE_MASTER |
主节点 |
CLUSTER_NODE_SLAVE |
从节点 |
CLUSTER_NODE_PFAIL |
疑似下线(Possible Failure) |
CLUSTER_NODE_FAIL |
确认下线 |
CLUSTER_NODE_HANDSHAKE |
握手状态(尚未完成加入) |
CLUSTER_NODE_MEET |
需要发送 MEET 消息 |
CLUSTER_NODE_NOADDR |
无地址(无法连接) |
myslots:2048 字节的位图,每一位对应一个槽。Redis 将全部数据划分为 16384 个槽,为什么是这个数字?
原因有三:
-
16384 足够多,可以均匀分配给数千个节点。
-
16384 bits = 2048 bytes,这个大小刚好可以让
clusterMsg保持在 MTU 以内,避免 IP 分片。 -
CRC16 算法的输出范围是 0-16383,天然匹配。
2.4 消息类型
Redis Cluster 定义了 9 种消息类型:
#define CLUSTERMSG_TYPE_PING 0 // 心跳探测,携带 gossip 信息
#define CLUSTERMSG_TYPE_PONG 1 // PING 的回复,同样携带 gossip 信息
#define CLUSTERMSG_TYPE_MEET 2 // 请求加入集群
#define CLUSTERMSG_TYPE_FAIL 3 // 广播节点下线
#define CLUSTERMSG_TYPE_PUBLISH 4 // Pub/Sub 消息转发
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 5 // 请求故障转移投票
#define CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 6 // 投票确认
#define CLUSTERMSG_TYPE_UPDATE 7 // 更新槽位配置
#define CLUSTERMSG_TYPE_MFSTART 8 // 手动故障转移开始
PING 和 PONG 是最频繁的消息类型。它们不仅用于心跳检测,更重要的是携带 gossip 信息——即发送者已知的其他节点的状态。
2.5 Gossip Section 的精妙设计
clusterMsg 的消息体中,PING/PONG/MEET 类型携带的是 clusterMsgDataGossip 结构数组,这就是 Gossip Section:
typedef struct {
char nodename[CLUSTER_NAMELEN]; // 被描述的节点 ID
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;
这个数组的长度是可变的。在 clusterMsg 的头部,count 字段记录了 gossip section 中包含多少个这样的条目。每个条目描述一个节点。
关键设计:发送者不会一次性把自己的全部节点列表发出去,而是随机挑选一部分。为什么?
-
控制消息大小:如果集群有 1000 个节点,全部发送会超过 MTU,导致 IP 分片,性能急剧下降。
-
保证传播效率:数学证明,即使每次只发送部分节点,经过 O(log N) 轮后信息仍然能覆盖全网。
每次 PING 消息携带的 gossip 条目数有一个动态范围:
-
最少 3 个:确保即使在小集群中也有足够的冗余。
-
最多
total_nodes - 2个:也就是约等于全部已知节点。
此外,Redis 还有一个精巧的优化:pong_received 字段的复用。如果接收者发现自己记录的某节点 pong_received 比发送者记录的时间更旧,且满足一定条件,接收者会直接采纳发送者的时间戳。这样就减少了不必要的 PING 发送,节约了带宽。
第三章:运行时引擎——从 clusterCron 到消息收发
3.1 事件循环中的 Gossip 调度
Redis 是一个单线程的事件驱动系统,所有功能都挂在 aeMain 事件循环中。Gossip 协议的调度入口是 clusterCron 函数,它被 serverCron 以每秒 10 次的频率调用。
// 简化的调用链
int main() {
initServer();
aeMain(server.el); // 事件循环
}
int serverCron() {
run_with_period(100) { // 每 100 毫秒执行一次 = 每秒 10 次
if (server.cluster_enabled)
clusterCron();
}
}
为什么是每秒 10 次?这是一个精心选择的频率:
-
太高(比如每秒 100 次):网络和 CPU 开销过大。
-
太低(比如每秒 1 次):故障检测延迟太长,收敛速度慢。
-
每秒 10 次意味着每 100 毫秒一次,正好在响应性和资源消耗之间找到平衡点。
clusterCron 函数每次执行时会做以下事情:

3.2 随机选择算法:不只“随机”那么简单
Gossip 协议的核心是“随机选择通信节点”。但 Redis 的实现远不止简单的 rand()。
// clusterCron 中的节点选择逻辑(伪代码)
void clusterCron(void) {
int fresh_ping_sent = 0;
// 遍历所有已知节点
for (each node in server.cluster->nodes) {
// 条件1:如果超过一半超时时间没有收到 PONG,立即发送
if (now - node->pong_received > server.cluster_node_timeout / 2) {
clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
fresh_ping_sent++;
continue;
}
// 条件2:否则,收集到候选列表,后续选最久未通信的
// ...
}
// 从候选列表中每次选择若干最久未通信的节点(数量动态计算,最多不超过 5 个)
// ...
// 每 10 次循环,额外随机挑选 1 个节点(防止信息孤岛)
if (!(iteration % 10)) {
// 随机选一个节点发送 PING
}
}
这里有三个精妙的设计:
设计一:优先响应“快超时”的节点。 如果一个节点距离上次收到 PONG 的时间已经超过了 cluster_node_timeout / 2,那么本轮立即给它发 PING,不管它是不是“最久未通信”。这是一种“预防性”机制,避免节点被误判为下线。
设计二:每次选择若干最久未通信的节点(数量动态计算,最多不超过 5 个)。 这是经典 gossip 算法中的“反熵”策略——优先与那些信息可能最“陈旧”的节点同步。5 这个数字是经验值:在几百个节点的集群中,5 个节点足以保证 O(log N) 的收敛速度,同时控制消息量。
设计三:每 10 次循环随机选 1 个节点。 这是为了打破“信息孤岛”。如果每次都选“最久未通信”的节点,可能会出现一种情况:某些节点之间永远没有直接通信,只能通过中间节点中转。随机选择可以增加拓扑的连通性,加速信息扩散。
3.3 消息接收:clusterProcessPacket
当节点收到消息时,网络 IO 触发 clusterReadHandler,最终调用 clusterProcessPacket 进行处理:
void clusterReadHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
// 从 socket 读取数据
// ...
clusterProcessPacket(link); // 核心处理函数
}
int clusterProcessPacket(clusterLink *link) {
clusterMsg *hdr = (clusterMsg*) link->rcvbuf;
// 1. 检查魔数
if (memcmp(hdr->sig, "RCmb", 4) != 0) return 1;
// 2. 查找或创建发送者节点对象
clusterNode *sender = clusterLookupNode(hdr->sender);
if (!sender && type == CLUSTERMSG_TYPE_MEET) {
// MEET 消息:新节点加入
sender = createClusterNode(...);
clusterAddNode(sender);
}
// 3. 更新发送者状态(ping_sent / pong_received 等)
// ...
// 4. 处理 gossip section
if (sender) {
clusterProcessGossipSection(hdr, link);
}
// 5. 根据消息类型执行特定逻辑
switch (hdr->type) {
case CLUSTERMSG_TYPE_PING:
clusterSendPing(link, CLUSTERMSG_TYPE_PONG); // 回复 PONG
break;
case CLUSTERMSG_TYPE_FAIL:
markNodeAsFailingIfNeeded(...);
break;
// ...
}
}
这里有一个值得注意的安全设计:消息魔数校验。每条消息的开头 4 个字节必须是 RCmb(Redis Cluster message bus)。这可以快速过滤掉非 Gossip 协议的流量,防止协议混淆攻击。
3.4 Gossip Section 处理:clusterProcessGossipSection
这是整个 Gossip 协议最核心的函数——如何根据别人告诉你的信息,更新自己的世界观。
void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) {
uint16_t count = ntohs(hdr->count); // gossip 条目数量
clusterMsgDataGossip *g = (clusterMsgDataGossip*) hdr->data.ping.gossip;
for (int i = 0; i < count; i++) {
// 查找本地是否已有该节点
clusterNode *node = clusterLookupNode(g->nodename);
if (node) {
// 节点已存在:更新信息
// 关键1:如果收到关于自己的 gossip,忽略(防止信息污染)
if (node == myself) continue;
// 关键2:采纳更新的 configEpoch
// 关键3:采纳更新的 pong_received 时间戳(带宽优化)
// 关键4:如果发送者报告该节点为 FAIL 或 PFAIL,
// 则调用 clusterNodeAddFailureReport 增加失败报告数
if (g->flags & (CLUSTER_NODE_FAIL | CLUSTER_NODE_PFAIL)) {
if (clusterNodeAddFailureReport(node, sender)) {
// 如果失败报告数达到多数派,标记为 FAIL
markNodeAsFailingIfNeeded(node);
}
}
} else {
// 节点不存在:创建新节点对象(黑名单机制暂不处理)
// 只在收到 MEET 或特定条件下才真正加入
}
g++; // 下一个 gossip 条目
}
}
这个函数体现了 Gossip 协议的 “信任但验证” 原则:
-
信任:接收发送者提供的节点信息,更新本地状态。
-
验证:不盲目信任。对于 FAIL 状态,需要收集多个独立节点的报告(多数派),才最终确认。
这里还有一个精妙的带宽优化:关于 pong_received 时间戳的采纳逻辑。如果接收者发现发送者的 pong_received 更新(且合理),就直接采纳,避免自己再发一次 PING。这体现了“能省则省”的工程哲学。
第四章:故障检测——从 PFAIL 到 FAIL 的状态流转
4.1 两阶段故障检测:PFAIL 和 FAIL
Redis Cluster 的故障检测分为两个阶段:
-
PFAIL(Possible Failure,疑似下线):节点 A 主观认为节点 B 可能挂了。
-
FAIL(Confirmed Failure,确认下线):集群中的多数主节点都认为节点 B 挂了。
这是一个 主观 → 客观 的确认过程。

为什么需要两阶段?因为网络分区和瞬时的网络抖动很常见。如果一超时就判定为 FAIL,会导致频繁的误判和故障转移,严重影响稳定性。两阶段设计大大降低了误判率。
4.2 PFAIL 的判定条件
在 clusterCron 中,每个节点会检查与其他节点的通信状态:
// 检查是否超时
delay = now - node->ping_sent;
if (delay > server.cluster_node_timeout) {
// 超时,且不处于 PFAIL 或 FAIL,则标记为 PFAIL
if (!(node->flags & (CLUSTER_NODE_PFAIL | CLUSTER_NODE_FAIL))) {
node->flags |= CLUSTER_NODE_PFAIL;
}
}
这里的 cluster_node_timeout 是一个可配置参数(默认 15000 毫秒)。调大它可以降低误判率,但会增加故障检测延迟;调小则相反。
4.3 从 PFAIL 到 FAIL:多数派确认
当一个节点 A 将节点 B 标记为 PFAIL 后,它会通过 Gossip 协议传播这一信息。其他节点收到后,会记录“A 认为 B 是 PFAIL”。这就是 clusterNodeAddFailureReport 函数做的事情。
当某个节点收集到足够多的 PFAIL 报告后,就会将 B 标记为 FAIL:
void markNodeAsFailingIfNeeded(clusterNode *node) {
// 1. 节点必须处于 PFAIL 状态
// 2. 自己必须能够访问该节点(防止网络分区误判)
// 3. 收集到的失败报告数必须达到多数派
int needed_quorum = (server.cluster->stats_num_masters / 2) + 1;
int failures = clusterNodeFailureReportsCount(node);
if (failures < needed_quorum) return;
// 标记为 FAIL
node->flags |= CLUSTER_NODE_FAIL;
node->flags &= ~CLUSTER_NODE_PFAIL;
// 广播 FAIL 消息
clusterSendFail(node->name);
}
这里有三个关键条件:
-
节点必须处于 PFAIL 状态:这是本地主观判断的前提。
-
自己必须能访问该节点:如果自己都访问不了,说明可能是网络分区,不应该贸然判定别人 FAIL。
-
失败报告数 ≥ 多数派:这是最核心的条件,确保判定是“客观”的。
4.4 FAIL 消息的广播与处理
一旦节点被标记为 FAIL,发起节点会立即广播一条 CLUSTERMSG_TYPE_FAIL 消息。收到 FAIL 消息的节点会:
-
无条件将目标节点标记为 FAIL(不需要再次收集多数派)。
-
如果自己是从节点且主节点是目标节点,则发起故障转移。
这种“一锤定音”的设计,确保了 FAIL 状态能在 O(log N) 时间内传遍全网,触发快速的故障转移。
第五章:串联——一个完整的故事
现在我们把所有碎片拼接起来,讲一个完整的故事:一个新节点如何加入 Redis Cluster,以及一个节点故障如何被检测和转移。
5.1 节点加入流程

-
管理员执行
CLUSTER MEET <ip> <port>,新节点向种子节点发送 MEET 消息。 -
种子节点将新节点加入本地节点列表,并回复 PONG。
-
在后续的 PING/PONG 交互中,种子节点通过 gossip section 向其他节点传播新节点的信息。
-
其他节点收到 gossip 后,主动向新节点发送 MEET,建立连接。
-
新节点通过与各节点的 gossip 交互,逐步学习到整个集群的拓扑和槽位分布。
整个过程完全自动化,不需要任何中心协调器。
5.2 故障检测与转移流程

-
从节点 B 定期向主节点 A 发送 PING,检测心跳。
-
超时后,B 将 A 标记为 PFAIL,并通过 gossip 传播。
-
其他主节点(如 C 和 D)收到 gossip 后,也会检查 A 的状态。
-
当 C 收集到足够多的 PFAIL 报告(多数派)后,将 A 标记为 FAIL,并广播 FAIL 消息。
-
B 收到 FAIL 消息后,立即发起故障转移选举。
-
B 获得多数主节点的投票后,晋升为新的主节点,接管 A 负责的槽位。
整个故障检测到转移的过程,从第一个节点发现 PFAIL 到完成转移,通常在 cluster_node_timeout 的 1-2 倍时间内完成。
5.3 数据流向全景图
下面这张图展示了整个 Gossip 通信的数据流向:

从这个全景图中可以看出,Gossip 协议在 Redis 中的实现是一个双工、持续、自愈的系统:
-
双工:PING 和 PONG 互为镜像,双方都在交换信息。
-
持续:每秒 10 次的频率,保证了信息的及时传播。
-
自愈:故障检测和转移机制内建于协议中,无需人工干预。
总结:Gossip 的哲学与局限
它为什么这么优秀?
Gossip 协议的成功,源于它深刻理解了分布式系统的核心矛盾:强一致性 vs 可用性 vs 分区容错(CAP 定理)。
它主动放弃了强一致性,换来了:
-
极致的水平扩展性:节点数增加,每个节点的负载几乎恒定。
-
天然的容错性:没有单点,任何节点挂掉都不影响协议运转。
-
实现的简洁性:核心逻辑只有几百行代码,易于理解和维护。
它有什么局限?
Gossip 协议不是银弹。它有几个公认的局限性:
-
最终一致性意味着延迟:信息传播需要 O(log N) 轮,集群规模越大,收敛时间越长。
-
网络分区时的脑裂风险:如果发生对称分区(主节点数量为偶数时),两边都可能独立选出新主,需要额外的 epoch 机制来仲裁。
-
跨地域部署的挑战:广域网的高延迟会放大 Gossip 的收敛延迟,需要引入分层 Gossip(Hierarchical Gossip)等优化手段。
关于网络分区补充
理论上主节点数量设置为奇数,当发生分区,最多只有一个分区能获得多数派选出新主,当分区恢复后,通过configEpoch自动解决新旧主冲突,版本号大的胜出。
但在数据层面,如果客户端连的旧主,且没有要求 WAIT 同步复制,此时写入的数据在分区恢复后可能会丢失。
一图胜千言

最后的思考
理解 Gossip 协议,不仅仅是读懂几百行 C 代码。
它代表了一种设计哲学:在分布式系统中,有时候“足够好”比“完美”更有价值。强一致性很美,但代价是可用性和扩展性的牺牲。Gossip 用“最终一致性”换来了无与伦比的弹性和简洁性。
下次你在代码中调用 CLUSTER MEET,或者看到 Redis 日志中的 PFAIL 和 FAIL 时,希望你脑海中能浮现出这篇文章中的那些数据结构和流程图。因为它们,才是 Redis Cluster 在数千个节点中稳定运行的真正原因。
参考资料:
-
Redis 源码
cluster.h和cluster.c(版本 4.0+) -
SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol, 2002
-
Redis Cluster Specification (https://redis.io/docs/reference/cluster-spec/)
更多推荐

所有评论(0)