Skip to content

Gossip 协议:最终一致性广播

有一种疾病,最初只在一个人身上发现。如果这个人每天随机告诉两个人,这两个人再各自告诉两个新人……

要多少天,全世界的人都会知道这个消息?

答案是:大约 30 天

这不是医学结论,是数学结论。这个「疾病传播」的模型,就是 Gossip 协议的灵感来源。


Gossip 协议的生物学期起源

Gossip 的英文意思是「八卦」。在计算机领域,Gossip 协议指的是:每个节点随机与其他节点交换信息,最终整个集群达到一致状态

它的核心假设是:在一个足够大的集群里,只要每个节点每秒与少数几个节点通信,经过 O(log n) 轮之后,信息就能传遍整个集群。

这听起来不可思议,但数学上可以证明。想象一枚石子扔进湖面,涟漪以指数速度扩散——Gossip 就是数字世界的涟漪。


三种传播方式

Gossip 协议有三种基本变体:

1. Push(推)

节点 A 有新数据时,随机选择节点 B,把数据「推送」给 B。

A:「我有个新消息,要不要?」
B:「好,给我!」

2. Pull(拉)

节点 A 没有数据时,随机选择节点 B,向 B「拉取」最新数据。

A:「你有什么新消息?」
B:「我有这些,都给你!」

3. Push-Pull(推拉混合)

A 和 B 互相交换自己知道的信息,双向同步。

A:「我有 X,你有 Y 吗?」
B:「我有 Y,我还有 Z!这是我有的:Y + Z」
A:「收到!我把我有的也给你:X + ...」

Push-Pull 收敛最快,但通信开销最大。实际系统常用 Push 或 Push-Pull。


反熵与谣言传播

Gossip 有两种工作模式,需要区分清楚:

反熵(Anti-Entropy)

「熵」是混乱的程度。反熵的目标是消除混乱,让所有节点最终达成完全一致

实现方式是全量数据对比:节点 A 和节点 B 互相交换完整数据,找出差异并修复。

优点:最终一致性 100% 缺点:通信开销大,不适合频繁执行

谣言传播(Rumor Mongering)

谣言的特点是:传着传着就不传了

每个消息都有一个「存活计数」(TTL),每传播一次 TTL -1,TTL 到 0 就停止传播。

java
public class RumorMessage {
    private String data;
    private int ttl;

    public RumorMessage(String data, int initialTtl) {
        this.data = data;
        this.ttl = initialTtl;
    }

    // 每传播一次,TTL 减 1
    public boolean propagate() {
        ttl--;
        return ttl > 0; // 返回 true 表示谣言还会继续传播
    }
}

谣言传播不保证 100% 一致性,但通信开销小,适合最终一致性要求不高的场景。


收敛速度:O(log n)

Gossip 最重要的数学性质:任意节点的信息在 O(log n) 轮内传播到全网

假设集群有 100 台机器:

  • 第 1 轮:1 → 2 台
  • 第 2 轮:3 → 4 台
  • 第 3 轮:7 → 8 台
  • 第 4 轮:15 → 16 台
  • 第 5 轮:31 → 32 台
  • 第 6 轮:63 → 64 台
  • 第 7 轮:全网

7 轮搞定。这就是为什么 Gossip 能在大规模集群中工作得很好。


工程实践:Cassandra 与 Consul

Cassandra 的 Gossip

Cassandra 用 Gossip 做两件事:

  1. 节点发现:新节点加入集群时,通过 Gossip 了解集群拓扑
  2. 元数据同步:节点状态、分区位置、负载信息

每个节点维护一个「种子节点列表」(Seed Nodes),新节点通过种子节点加入 Gossip 环,逐步了解集群全貌。

java
// Cassandra Gossip 的简化模型
public class CassandraGossip {
    private Map<String, EndpointState> endpointStateMap = new ConcurrentHashMap<>();

    public void onGossip(Map<String, EndpointState> peerState) {
        for (Map.Entry<String, EndpointState> entry : peerState.entrySet()) {
            String node = entry.getKey();
            EndpointState remoteState = entry.getValue();

            EndpointState localState = endpointStateMap.get(node);
            if (localState == null || remoteState.getVersion() > localState.getVersion()) {
                // 同步更新的状态
                endpointStateMap.put(node, remoteState);
            }
        }
    }
}

Consul 的 Gossip 池

Consul 维护了两个独立的 Gossip 池:

  • LAN Gossip:同一数据中心内的节点,用于服务发现和失败检测
  • WAN Gossip:跨数据中心,用于多个数据中心之间的通信

这样设计的好处是:LAN 内延迟低,可以频繁同步;WAN 只同步必要信息,减少跨地域流量。


Gossip 的优缺点

优点

优点说明
去中心化没有单点故障,任何节点都可以随时加入离开
可扩展节点数增加时,开销增长是亚线性的
容错节点故障不影响其他节点,信息可以绕道传播
简单实现简单,不需要复杂的分布式算法

缺点

缺点说明
最终一致性,非强一致收敛时间不确定,可能出现短暂的不一致
消息冗余同一消息可能被多次传播,造成带宽浪费
收敛时间不确定O(log n) 是数学期望,实际可能更慢

面试追问方向

Gossip 能否保证强一致性?

不能。Gossip 只能保证最终一致性。

原因很简单:Gossip 是异步传播,节点 A 在传播消息时,节点 B 可能还没收到,但 B 已经对外提供服务了。这就是「短暂的不一致窗口」。

那 Gossip 适合什么场景?

  • 服务发现:节点加入/离开的信息,不需要强一致性
  • 元数据同步:集群拓扑、配置信息,允许短暂不一致
  • 失败检测:某个节点是否还活着,不需要精确到毫秒
  • 不适合:需要强一致性的场景(如分布式事务、分布式锁)

那为什么 Cassandra 用 Gossip 做一致性?

Cassandra 本身是「最终一致性」数据库,它的一致性是通过可调一致性( tunable consistency)实现的——你可以选择读 QUORUM 或读 ALL,而不是强制的。


留给你的问题

Gossip 的收敛时间是 O(log n),但这只是数学期望。

实际环境中,如果网络分区发生,Gossip 可能会在两个分区内部收敛,形成「双峰」状态——两个分区内部一致,但分区之间不一致。

你能想到什么机制来检测和解决这种「脑裂」问题吗?

Hint:这和分布式一致性协议(如 Raft)的选主机制有某种联系。

基于 VitePress 构建