151 lines
7.4 KiB
Markdown
151 lines
7.4 KiB
Markdown
|
|
---
|
|||
|
|
tags: [分布式, Gossip, 最终一致性, 去中心化, Redis Cluster]
|
|||
|
|
create time: 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 协议的核心思想可以归结为三步,每一步都体现了"少而精"的通信策略:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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,传播方式如下:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 不会因为一次超时就判定节点下线,而是采用**两阶段确认**,避免误判:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 负责什么?
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/Redis/07-集群方案]] — Redis Cluster 集群方案总览
|
|||
|
|
- [[hhs/Redis/07-集群方案/Raft共识算法]] — 对比理解:Gossip 是最终一致,Raft 是强一致;故障转移中 Slave 选举使用类 Raft 投票
|
|||
|
|
- [[hhs/Redis/06-主从复制/哨兵模式]] — 对比理解:哨兵模式也做故障检测,但由哨兵节点集中负责,而 Cluster 用 Gossip 去中心化完成
|