Gossip は、ノードが周期的に少数のピアを選択して状態を交換し、更新が複数のローカルな相互作用を経て拡散する、対等な情報伝播手法の一種です。これにより、固定されたブロードキャストセンターへの依存が軽減されますが、ノードは同時に完全に同一の情報を参照するわけではありません。
したがって、Gossip はメンバーシップ、状態の要約、または収束可能な更新の伝播に適しています。しかし、それだけでは唯一のリーダーを承認したり、すべてのノードが同じ順序でコマンドを処理することを保証したりすることはできません。伝播とコンセンサスは異なる問題を解決します。
2つの分類を混同しない
Push、Pull、Push-Pull は情報の流れを記述するものであり、Rumor Mongering(噂流布)と Anti-Entropy(非熵化/整合性維持)は伝播と追従補完の戦略を記述するものです。これらは独立して組み合わせ可能な 2 つの次元です。
流れ 1回の相互作用で行うこと 主な特徴
Push ローカルの新しい情報をピアにプッシュする 更新が発生した際に急速に拡散できる
Pull ピアに自分が不足しているものを問い合わせる 能動的に追従するのに適しているが、ピアが必ずしも完全なデータを持っているわけではない
Push-Pull 双方が比較して差分を交換する 1回の接触で双方が利益を得られ、ペイロードが増加する可能性もある
Rumor は通常、コストを抑えるために新しい更新を有限の回数だけ伝播させた後に停止します。Anti-Entropy は定期的に状態を比較し、偶然に漏れた部分を補完します。Pull によって 1 回の通信で追いつくのは、ピアが必要なデータを持っておらず、通信が成功し、容量が十分である場合に限られます。
なぜ初期の伝播が速いのか
ランダム伝播:拡散は増加するが、毎ラウンド正確に倍増すると仮定してはならない
A
B
C
D
E
F
E への重複ヒット
ランダムな選択では既知のノードを再訪問する可能性があります。カバレッジの末尾は、トポロジ、パケット損失、帯域幅の影響を受けます。
理想的なモデルでは、更新を知っているノードが未知のノードと接触し続け、初期段階ではカバーされるノード数が急速に増加します。しかし後期になると、すでに更新を知っているノードをより多く選択するようになり、残りの少数のノードがカバーの末尾(テール)となります。
ランダムな混合が良好で、メッセージに十分な容量があるなどの仮定の下では、多くのエピデミック(伝染)プロトコルにおいて、伝播時間はノード数に対して対数的に増加します。これは確率とモデル条件に基づく結論であり、任意のネットワークが正確に log 2 N ラウンドですべてのノードに到達できるという意味ではありません。
2つのデータセンター間で接続が切断されると、ローカルな拡散がどれだけ速くても、パーティションを跨ぐことはできません。また、更新の発生速度が利用可能な伝播帯域幅を超えると、バックログ(溜まり)が増加し続ける可能性があります。Efficient Reconciliation and Flow Control では、有限な容量下でのアンチエントロピーの制約について直接的に議論しています。
4ノード、2ノードが知情:次のラウンドですべてがカバーされるとは限らない
同期ラウンドの開始時に、ノード 0 と 1 のみが更新を知っていると仮定します。各知情ノードは、他の 3 ノードから等確率で 1 つのターゲットを選択して Push し、メッセージはすべて到達するとします。新しく知情となったノードは、次のラウンドになってから転送を行います。2つの送信者が持つ 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 のみで 4 ノードすべてをカバーでき、その確率は 2/9 です。0→1,1→0 の場合は新規カバーが全く増えず、確率は 1/9 です。その他の選択では、知情ノードが 1 つだけ増加します。平均して 10/9 ノードが増加し、機械的な倍増(2つ増加)が要求されるものではありません。
N ノード、k 個の知情ノードに一般化すると、上記の独立した一様選択かつパケット損失なしの条件下では、次のラウンドで新規に知情となるノード数の期待値は (N−k)×[1−(1−1/(N−1))^k] となります。これは 1 ラウンドあたりの期待値であり、実際の各伝播軌跡ではありません。ネットワークパーティション、ローカル志向の選択、またはメッセージ容量の不足は、このモデルを変更します。
有限回の伝播では、確率的な漏れが残る可能性があります。例えば、A が更新を B に伝えた後に停止し、B が唯一の接触で A を選んだ場合、C や D はその Rumor を二度と受信できません。周期的なアンチエントロピーの意義は、後続の追従機会を提供することにあります。もし更新自体が完全に破棄されていれば、後続の接触がどれだけ頻繁でも、内容は回復できません。
2ノードが知情でも、次のラウンドで 4 になるとは限らない
期待値の式は、未知のノード 1 つごとに分解して理解できます。1 つの送信者が特定の未知ノードを選ばない確率は 1 − 1/ ( N − 1 ) です。k 個の独立した送信者がすべてそれを選ばない確率は、この数の k 乗です。補集合を取ることで、そのノードがこのラウンドで知情となる確率が得られ、これを N-k 個の未知ノードについて合計します。異なる未知ノードが知情となるかどうかは相互に独立である必要はなく、期待値の線形性(可加性)だけで十分です。
E [ Δ ] = ( N − k ) [ 1 − ( 1 − N − 1 1 ) k ]
図を準備しています 再読み込み
2ノードが知情でも、次のラウンドで 4 になるとは限らない 有限のランダムな接触では、既知のノードへの重複ヒットが発生します。4ノード・2送信者の9通りの等確率のターゲット組み合わせのうち、すべてをカバーするのは2通りだけです。
{"id":"gossip-one-round","title":"2ノードが知情でも、次のラウンドで 4 になるとは限らない","summary":"有限のランダムな接触では、既知のノードへの重複ヒットが発生します。4ノード・2送信者の9通りの等確率のターゲット組み合わせのうち、すべてをカバーするのは2通りだけです。","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":"全9組み合わせ:金色は完全カバーを示す","coverage":"このラウンド終了時の知情ノード","heading":"2ノードが知情でも、次のラウンドで 4 になるとは限らない","intro":"ラウンド開始時に A と B が更新を知っています。それぞれの Push ターゲットを選択し、このラウンドでの新規カバーと重複接触を直接確認します。","known":"知情","probability":"完全カバー確率","reset":"リセット","scope":"同期された 1 ラウンドの Push。自己送受信なし、パケット損失なし、独立した一様選択。更新を新規に受信したノードは、次のラウンドになってから転送します。画面は確率的なサンプリング結果ではなく、決定論的な選択を示しています。","scopeLabel":"モデルの範囲と規約","targetA":"A のターゲット","targetB":"B のターゲット","unknown":"未知"}}
重複排除、バージョン、マージが最終状態を決定する
同じ更新が異なる経路で複数回到達する可能性があります。受信側は、副作用の重複実行を避けるために、安定した識別子またはバージョンルールが必要です。もし完全な状態が伝播される場合は、2つの状態をどのようにマージするかを定義する必要があります。もし操作が伝播される場合は、消失、重複、順序を考慮する必要があります。
例えば、各ノードが独自の単調増加カウンタコンポーネントを維持し、別の副本の状態を受信した後に、各コンポーネントについて最大値を取り、合計することで、重複メッセージによるカウンタの重複累算を防ぐことができます。「合計を 1 増やす」という指示を直接ブロードキャストし、受信するたびに実行すると、重複に対して耐性を持ちません。
新旧の判断は、ローカルでの受信時刻だけでは行えません。遅延メッセージは新しいメッセージよりも遅く到着する可能性があります。メンバーが再起動した際にカウンタが 0 にリセットされる場合、incarnation(生成)などの世代情報を必要とします。より一般的なマージ条件については、衝突解決 を参照してください。
G-Counter:各ライターのカプセル状態を交換し、スナップショットを再度追加しない
3人のライターがそれぞれ自分のコンポーネントのみを増加させます。A は 2 回増加し、B は 3 回、C は 1 回増加しました。副本間で状態を交換する際、同じライターについては大きい方のコンポーネントを取り、合計します。重複したスナップショットは新しい操作にはなりません。
ライターごとに最大値を取る:重複した状態でもカウントは重複しない
ローカル A=2, B=0
合計 2
A=1, B=3 を受信
古い A と新しい B
マージ A=2, B=3
合計 5;重複マージでも 5 のまま
各ライターは自分のコンポーネントのみを増加させる;Gossip は伝送を提供し、join が収束セマンティクスを定義する。
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 演算は、交換法則、結合法則、冪等性を満たすため、完全な状態が重複・乱序で到達しても収束します。前提条件は、すべての増加が最終的に伝播され、各ライターが自分の単調なコンポーネントを独占していることです。2つのプロセスが同じ身份を誤用してそれぞれ 0 から 1 まで増加した場合、マージすると 1 しか得られず、2回の独立した増加を表現できません。
再起動時に永続的なコンポーネントを復元するか、新しい incarnation を使用することができますが、新しい身份を使用することは無コストな修復ではありません。古いコンポーネントは随意に削除できず、そうすると合計値が減少するため、身份の回収や圧縮プロトコルを別途設計する必要があります。ここで示す G-Counter は、増加のみをサポートし、任意の減算はサポートしません。状態型 CRDT のマージ条件および GCounter の実装例については、Akka Distributed Data を参照してください。
メンバーシップ伝播とデータアンチエントロピー
SWIM のプローブ層は存続証拠を取得し、伝播層は参加、離脱、疑念、存続の更新を拡散します。dead イベントのみを伝播として記述するのは不十分です。疑念の反論や新規メンバーの参加も、同様に拡散する必要があります。プロトコルの詳細については SWIM を参照してください。
大規模な業務データの場合、完全な状態を逐条伝播するのは高コストになり得るため、まず要約を交換し、同期が必要な範囲を特定します。Merkle tree はそのためのツールの一つであり、伝送を削減するのに役立ちますが、バージョンの衝突を自動的に解決するわけではありません。詳細は アンチエントロピーとデータ修復 を参照してください。
Fanout、周期、ペイロード予算
ノードあたり 1 秒間に 3 人のピアと連絡し、1 回の送信で約 2 KB を送信する場合、1 ノードあたりの単方向送信量は約 6 KB/s です。1000 ノードでは約 6 MB/s となり、返信やプロトコルオーバーヘッドは含まれていません。ここで示しているのは推計手法であり、任意の実装のデフォルト設定ではありません。
Fanout を高めたり周期を短くしたりすると、伝播が速くなる一方で、輻輳や処理待ちキューが発生し、実際の収束が逆に遅くなる可能性があります。単一の更新の再送信回数、メッセージサイズ、キューのバックログを制限し、アンチエントロピーのための追従機会を確保する必要があります。
障害関連の更新は、優先度の低い統計情報よりも緊急性が高いですが、優先度付けによって特定のカテゴリが永久に送信されなくなることはあってはなりません。周期的な要約とイベントの高速伝播を組み合わせることで、即時性と最終的なカバレッジの両立を図ります。
Gossip で代替できないもの
2つのノードが一時的に異なるメンバーシップリストを保持していることは、Gossip が許容する状態です。もしそれぞれがローカルのリストに基づいて排他的な書き込み権限を付与すると、同時に自分自身が唯一の所有者であるとみなす可能性があります。排他的な意思決定が必要な場合は、弱い一貫性を持つビューを、明確なセキュリティルールを持つ調整プロトコルに接続する必要があります。
同様に、Raft の AppendEntries は状態マシンレプリケーションプロトコルの一部であり、それ自体がすべての信頼できるブロードキャストの定義として使用できるわけではありません。システムを判断する際は、伝播、順序付け、永続化、配信保証をそれぞれ明確に記述し、「リーダーがいるかどうか」だけで正しさを比較しないようにしてください。
参考文献
キーワード:gossip、push、pull、push-pull、rumor mongering、anti-entropy、fanout、確率的カバレッジ、状態マージ