Files
cs-note/hhs/Redis/07-集群方案/Gossip协议.md
T
2026-05-27 11:24:47 +08:00

7.4 KiB
Raw Blame History

tags, create time
tags create time
分布式
Gossip
最终一致性
去中心化
Redis Cluster
2026-05-27 10:00

Gossip 协议

概述

Gossip 协议(也叫 Epidemic Protocol,流行病协议)是一种去中心化的信息传播协议。它的灵感来自社交网络中的"八卦传播"——一个人把消息告诉几个朋友,每个朋友再告诉他们各自的几个朋友,信息很快就会传遍整个社交圈。

Redis Cluster 使用 Gossip 协议在节点之间同步集群状态(谁是 Master、哪些 slot 归谁、节点是否健康),而不需要一个中心化的协调者。

一、为什么需要 Gossip?

分布式集群面临一个基本问题:每个节点如何知道其他节点的状态?

两种经典方案:

方案 做法 优点 缺点
中心化注册中心 引入 ZooKeeper/etcd 管理节点信息 强一致,查询快 单点故障风险,增加运维复杂度
去中心化 Gossip 节点之间互相"八卦",没有中心 无单点故障,天然高可用 最终一致性,传播有延迟

Redis Cluster 选择了后者——Redis 本身就是一个高性能的单线程数据库,引入外部依赖(ZK/etcd)会增加架构复杂度,违背了 Redis 轻量、快速的设计哲学。

二、Gossip 的传播原理

Gossip 协议的核心思想可以归结为三步,每一步都体现了"少而精"的通信策略:

flowchart LR
    A["节点 A<br/>有新消息"] -->|"1. 随机选择几个邻居"| B["节点 B, C, D"]
    B -->|"2. 收到后继续转发"| E["节点 E, F"]
    C -->|"2. 收到后继续转发"| G["节点 G, H"]
    E -->|"3. 持续传播直到收敛"| I["所有节点"]

具体到 Redis Cluster,传播方式如下:

sequenceDiagram
    participant A as Node A
    participant B as Node B
    participant C as Node C
    participant D as Node D

    Note over A: 新状态: slot 0~5460 归我管
    A->>B: PING (携带 A 的状态摘要 + 随机挑 3 个节点状态)
    B->>A: PONG (携带 B 的状态摘要)
    A->>C: PING (携带更新后的状态)
    C->>A: PONG
    Note over A,B,C,D: 几轮之后<br/>所有节点都知道 slot 0~5460 归 A 管

收敛速度

Gossip 的收敛速度与集群规模的关系:

  • 传播轮数:O(log N) 轮后信息传遍所有节点
  • 每轮通信量:每个节点选 min(3, sqrt(N)) 个目标
集群规模 每节点每轮通信目标数 大约收敛轮数 总消息量
6 节点 3 ~3 轮 ~18 条
100 节点 10 ~7 轮 ~700 条
1000 节点 32 ~10 轮 ~10000 条

[!NOTE] Redis Cluster 的 Gossip 通信频率 每秒执行 10 次(即每 100ms 一次),每次从集群中随机选取 5 个节点,从中选出最久没通信的一个节点发送 PING。这个频率在 cluster-node-timeout 的窗口内足以让状态收敛。

[!QUESTION] 思考 为什么不直接让每个节点把消息广播给所有其他节点?对比一下"全广播"和"Gossip"的带宽开销。

三、Gossip 的四种消息类型

Redis Cluster 的 Gossip 通信使用以下四种消息(实际协议中有更多,这四种是核心):

消息类型 发送条件 作用
PING 每秒定时发送 心跳探活 + 携带发送者自身状态和随机选中的节点状态
PONG 收到 PING 后回复 确认存活 + 携带接收者自身状态
MEET 新节点加入集群 强制目标节点加入集群(区别于 PING 的"可选")
FAIL 检测到某节点不可达 广播故障通知,触发 Failover

[!NOTE] PING 报文不只是心跳 PING 报文中携带的信息远不止"我还活着"。它包含:

  • 发送节点的 nodeId、IP、端口、角色(Master/Slave)
  • 集群当前的 clusterEpoch(配置版本号,用于冲突解决)
  • 发送节点负责的槽位 bitmap(一个 16384 位的位图)
  • 发送节点的 configEpoch(本节点见过的最大纪元)
  • 随机选择的其他节点的状态(间接传播,每个节点附带 2~3 字节 flags 表示 PFAIL 等状态)

这意味着,即使节点 A 从未和节点 D 直接通信,A 也能通过 B 的 PING 报文了解到 D 的状态——这就是"八卦"的力量。同时,epoch 机制保证了:当两个节点对 slot 归属有分歧时,epoch 更大的配置胜出,解决了最终一致性的冲突问题。

四、故障检测:PFAIL → FAIL 双阶段确认

Gossip 的一个关键实战应用是故障检测。Redis Cluster 不会因为一次超时就判定节点下线,而是采用两阶段确认,避免误判:

sequenceDiagram
    participant A as Node A
    participant B as Node B
    participant C as Node C
    participant D as Node D

    A->>A: 与 Node D 通信超时
    Note over A: 标记 D 为 PFAIL<br/>(可能下线, 暂不处理)
    A->>B: PING (携带: D is PFAIL)
    A->>C: PING (携带: D is PFAIL)
    B->>A: PONG (我这里 D 也超时了, PFAIL)
    C->>A: PONG (D 对我正常, 无 PFAIL)
    Note over A: 收集到集群半数以上<br/>Master 对 D 的 PFAIL 票
    Note over A: D 从 PFAIL 升级为 FAIL<br/>广播 FAIL 消息
    A->>B: FAIL(D)
    A->>C: FAIL(D)
    Note over B,C: 收到 FAIL 后<br/>触发 D 的 Slave 故障转移

[!IMPORTANT] 为什么要两阶段? PFAIL(Possibly Fail) 是本地判断:节点 A 发现 D 通信超时,可能只是 A 和 D 之间网络抖动。 FAIL 是集群共识:只有当集群中过半 Master 节点都在 cluster-node-timeout 窗口内标记 D 为 PFAIL,才会升级为 FAIL。

这个设计本质上是一个轻量级的"多数派投票"——和 Raft 中的选举逻辑异曲同工,但通过 Gossip 而非显式投票完成。

五、Gossip 的局限性

Gossip 并非完美方案,它有一个核心 trade-off:

[!IMPORTANT] 最终一致性,而非强一致性 Gossip 保证信息最终会传遍所有节点,但不保证立刻。在传播过程中,不同节点对集群状态的认知可能是不一致的。

这就是为什么 Redis Cluster 需要 epoch(集群纪元) 来解决冲突——当两个节点对 slot 归属有分歧时,epoch 更大的配置胜出。

其他局限:

局限 说明 Redis 的应对
消息冗余 同一条信息可能被多次传播 报文中携带 epoch,节点收到旧 epoch 的消息直接丢弃
不适合实时场景 收敛需要数秒 故障检测用 PFAIL/FAIL 双阶段确认,不依赖 Gossip 收敛
带宽开销随节点增长 O(N log N) 级,每节点每轮 min(3, √N) 个目标 大集群通过调低通信频率、使用 proxy 分层缓解

[!QUESTION] 对比思考 Gossip 和 Raft 都能解决分布式一致性问题,但适用场景不同。前者是"最终一致",后者是"强一致"。结合 hhs/Redis/07-集群方案/Raft共识算法,思考:Redis Cluster 为什么同时用了这两种协议——Gossip 负责什么?Raft 负责什么?

关联笔记