Gossip 是一类对等信息传播方法:节点周期性选择少量对端交换状态,让更新经过多次局部交互扩散。它减少对一个固定广播中心的依赖,但节点不会在同一时刻看到完全相同的信息。
因此,Gossip 适合传播成员、状态摘要或可收敛更新;不能单独授权唯一 leader 或保证所有节点按同一顺序处理命令。传播与共识解决不同问题。
两组分类不要混在一起
Push、Pull、Push-Pull 描述信息流向;Rumor Mongering 与 Anti-Entropy 描述传播和补齐策略。这是两组可组合的维度。
| 流向 | 一次交互做什么 | 主要特点 |
| Push | 将本地新信息推给对端 | 更新出现时可以迅速扩散 |
| Pull | 询问对端自己缺什么 | 适合主动追赶,但对端未必更完整 |
| Push-Pull | 双方比较并交换差异 | 一次联系可让双方受益,载荷也可能增加 |
Rumor 通常把某条新更新传播有限次数后停止,以控制成本;Anti-Entropy 定期比较状态,补回偶然遗漏。Pull 一次就追上只有在对端拥有完整所需数据、传输成功且容量足够时才成立。
为什么早期传播很快
理想化模型里,已知更新的节点继续接触未知节点,传播初期覆盖数可快速增长。后期却会越来越多地选中已经知道的节点,剩余少量节点成为覆盖尾部。
在随机混合良好、消息有足够容量等假设下,许多 epidemic 协议的传播时间随节点数近似对数增长。这是带概率和模型条件的结论,不是任意网络都能在精确 log₂N 轮到达全部节点。
若两个机房之间断连,局部扩散再快也无法跨过分区。若更新产生速度超过可用传播带宽,积压也可能持续增长。Efficient Reconciliation and Flow Control 直接讨论了反熵在有限容量下的约束。
四个节点、两个知情者:下一轮也未必覆盖全部
假设同步轮次开始时只有 0、1 知道更新,每个知情节点从另外三个节点中等概率选一个目标 push,消息全部送达。新知情节点到下一轮才转发。两个发送者共有 3×3=9 种等可能选择,可以直接穷举:
from itertools import product
from fractions import Fraction
known = {0, 1}
outcomes = [known | {target0, target1}
for target0, target1 in product((1, 2, 3), (0, 2, 3))]
assert len(outcomes) == 9
assert sum(len(result) == 4 for result in outcomes) == 2
assert sum(len(result) == 2 for result in outcomes) == 1
expected_new = Fraction(sum(len(result) - 2 for result in outcomes), 9)
assert expected_new == Fraction(10, 9)
assert expected_new == 2 * (1 - Fraction(2, 3)**2)
其中只有 0→2,1→3 与 0→3,1→2 能覆盖四个节点,概率 2/9;0→1,1→0 完全没有新增覆盖,概率 1/9。其余选择都只增加一个知情节点。平均新增 10/9 个,不是机械翻倍所要求的两个。
推广到 N 个节点、k 个知情者,在上述独立均匀选择且无丢包的条件下,下一轮新增的期望是 (N−k)×[1−(1−1/(N−1))^k]。它是对一轮的期望,不是每条实际传播轨迹。网络分区、偏向局部的选点或消息容量不足,都会改变这个模型。
有限传播次数还会留下概率性遗漏。例如 A 把更新传给 B 后停止,B 唯一一次又选回 A,C、D 就再也收不到这条 rumor。周期反熵的意义是提供后续补齐机会;如果更新本身已被完全丢弃,后续接触再频繁也恢复不了内容。
两个知情节点,并不保证下一轮变成四个
期望式可以按一个未知节点拆开理解:一个发送者不选它的概率是 1−1/(N−1),k 个独立发送者都没选它的概率是这个数的 k 次方。取补集得到它本轮获知的概率,再对 N−k 个未知节点求和。不同未知节点是否获知并不需要相互独立,期望的可加性已经足够。
E[Δ]=(N−k)[1−(1−N−11)k]
正在呈现知识画面
两个知情节点,并不保证下一轮变成四个有限随机接触会重复命中已知节点。四节点两发送者的九种等概率目标组合中,只有两种覆盖全部节点。
{"id":"gossip-one-round","title":"两个知情节点,并不保证下一轮变成四个","summary":"有限随机接触会重复命中已知节点。四节点两发送者的九种等概率目标组合中,只有两种覆盖全部节点。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"two\"><label><span data-i18n=\"targetA\"></span><select id=\"targetA\"><option>B</option><option>C</option><option>D</option></select></label><label><span data-i18n=\"targetB\"></span><select id=\"targetB\"><option>A</option><option>C</option><option>D</option></select></label></div><div id=\"nodes\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"coverage\"></span><strong id=\"coverage\"></strong></div><div><span data-i18n=\"added\"></span><strong id=\"added\"></strong></div><div><span data-i18n=\"probability\"></span><strong id=\"probability\"></strong></div></div><p class=\"hint\" data-i18n=\"cases\"></p><div id=\"cases\" class=\"cases\"></div><p class=\"formula\">E[Δ] = (4−2) × [1−(1−1/3)²] = 10/9</p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.cases{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.metrics{grid-template-columns:repeat(3,minmax(0,1fr))}.cases .cell{font-size:10px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}function cover(a,b){return new Set(['A','B',a,b]);}function draw(){const a=$('#targetA').value,b=$('#targetB').value,s=cover(a,b);cells('nodes',[...'ABCD'].map(n=>({text:n+' '+t(s.has(n)?'known':'unknown'),cls:s.has(n)?'on':'empty'})));set('coverage',s.size+' / 4');set('added',s.size-2);set('probability','2 / 9');let values=[];for(const x of 'BCD')for(const y of 'ACD')values.push({text:'A→'+x+' B→'+y+' · '+cover(x,y).size+'/4',cls:cover(x,y).size===4?'gold':''});cells('cases',values);}document.querySelectorAll('select').forEach(e=>e.onchange=draw);draw();","audio":false,"strings":{"added":"新增节点","cases":"全部九种组合:金色表示完整覆盖","coverage":"本轮结束时已知节点","heading":"两个知情节点,并不保证下一轮变成四个","intro":"本轮开始时 A、B 知道更新。分别选择它们的 push 目标,直接看本轮新增覆盖与重复联系。","known":"已知","probability":"全部覆盖概率","reset":"重置","scope":"同步的一轮 push,无自发自收、无丢包、独立均匀选点。新收到更新的节点到下一轮才转发。画面是确定性选择,不是随机采样结果。","scopeLabel":"模型范围与约定","targetA":"A 的目标","targetB":"B 的目标","unknown":"未知"}}
去重、版本与合并决定最终状态
同一更新可能经不同路径多次到达。接收端需要稳定标识或版本规则,避免重复执行副作用。若传播的是完整状态,则需要定义如何合并两个状态;若传播的是操作,则要考虑丢失、重复和顺序。
例如每个节点维护自己的单调计数分量,收到另一副本的状态后对每个分量取最大值,再求和,就能避免重复消息把计数重复累加。直接广播“总数加一”并在每次收到时执行,则不能容忍重复。
新旧判断也不能只看本地接收时间。延迟消息可能比新消息更晚到;成员重启后若计数归零,需要 incarnation 等代际信息。更一般的合并条件见 冲突解决。
G-Counter:交换各写者的累计状态,不能把快照再加一次
三个写者各自只增加自己的分量。A 已产生两次增加,B 产生三次,C 产生一次。副本交换状态时,对同一写者取较大分量,再求总数。重复快照不会变成新操作。
from itertools import permutations
def join(a, b):
return {writer: max(a.get(writer, 0), b.get(writer, 0))
for writer in a.keys() | b.keys()}
updates = [{"A": 2}, {"B": 3}, {"C": 1}]
for order in permutations(updates):
state = {}
for update in (*order, order[0], order[1]): state = join(state, update)
assert state == {"A": 2, "B": 3, "C": 1}
assert sum(state.values()) == 6
assert join({"A": 2}, {"A": 1}) == {"A": 2}
assert join({"A": 2}, {"A": 1}) != {"A": 3}
assert sum(join({"A#1": 2}, {"A#2": 1}).values()) == 3
逐分量 max 满足交换律、结合律和幂等性,因此完整状态在重复、乱序到达时仍可收敛。前提是所有增加最终传播,并且每个写者独占自己的单调分量;两个进程误用同一身份各自从 0 加到 1,合并只会得到 1,无法表达两次独立增加。
重启时可恢复持久分量,也可使用新 incarnation,但新身份不是无成本的修复:旧分量不能随意删除,否则总数会下降,需要另行设计身份回收或压缩协议。这里的 G-Counter 也只支持增加,不支持任意扣减。状态型 CRDT 的合并条件及 GCounter 实例见 Akka Distributed Data。
成员传播与数据反熵
SWIM 的探测层取得存活证据,传播层扩散加入、离开、怀疑与存活更新。只写成传播 dead 事件是不完整的:反驳怀疑和新成员加入同样需要扩散。协议细节见 SWIM。
对于大规模业务数据,逐条传播完整状态可能太昂贵,可以先交换摘要,再定位需要同步的范围。Merkle tree 是其中一种工具,它帮助减少传输,不会自动决定版本冲突。详见 反熵与数据修复。
Fanout、周期与载荷预算
若每节点每秒联系 3 个对端、每次发送约 2 KB,一个节点单向发送量约 6 KB/s;一千节点约 6 MB/s,尚未包含回复和协议开销。这里展示的是估算方法,不是任何实现的默认配置。
提高 fanout 或缩短周期可能加快传播,也可能造成拥塞与处理排队,使实际收敛反而更慢。还要限制单条更新的重传次数、消息大小、队列积压,并为反熵预留补齐机会。
故障相关更新通常比低优先级统计信息更紧急,但优先级不能让某些类别永久得不到发送。混合使用周期摘要与事件快速传播,可以兼顾及时性与最终覆盖。
Gossip 不能替代什么
两个节点暂时持有不同成员列表,是 Gossip 允许的状态。它们若各自凭本地列表授予独占写权限,就可能同时认为自己是唯一所有者。需要独占决策时,应把弱一致视图接入具有明确安全规则的协调协议。
同样,Raft AppendEntries 是复制状态机协议的一部分,不能拿它当成所有可靠广播的定义。判断系统时,应分别写出传播、排序、持久化与交付保证,避免只凭“有没有 leader”比较正确性。
参考
关键词:gossip、push、pull、push-pull、rumor mongering、anti-entropy、fanout、概率覆盖、状态合并