CRDT(衝突なしレプリケーション型データ構造)
複数拠点で同時に書き込んでも、後でマージするだけで全レプリカが自動的に同じ状態へ収束する。合意プロトコル抜きで結果整合を実現する代数的な仕組みを原理から理解できます。
- 1.CRDT は更新を可換・結合・冪等なマージで畳み込む半束として設計し、適用順や重複に関係なく全レプリカを同じ値へ収束させる。
- 2.状態ベース(CvRDT)は状態そのものを join で併合し、操作ベース(CmRDT)は可換な操作を高々一度ずつ配送して反映する。
- 3.GCounter・PNCounter・ORSet・LWWRegister など型ごとに半束を構成し、合意プロトコルなしで結果整合と無調整の高可用を両立する。
CRDT が解こうとしている問題
複数のレプリカに同時に書き込みが入る環境では、どこかで更新がぶつかります。素朴な解決は「最後に書いた人が勝つ」や、サーバー間で合意を取って順序を決める方法ですが、前者は更新を捨て、後者は合意プロトコルによる調整コストと、分断時の可用性低下を招きます。CAP 定理でいう AP(可用性優先)に倒したいとき、調整なしで競合を解消したい――この要求に数学的に答えるのが CRDT(Conflict-free Replicated Data Type) です。
CRDT の核心は単純です。マージ操作が代数的に良い性質を持つように型を設計しておけば、更新がどんな順序・重複で届いても、全レプリカは必ず同じ状態に収束する。競合検出も巻き戻しも要らず、ただマージするだけで整合します。これは結果整合性(eventual consistency)の一形態ですが、アプリ側で衝突を解く必要がない点で 強結果整合(strong eventual consistency, SEC) と呼ばれます。
なぜ収束するのか:半束という土台
CRDT が収束する理由は、状態の集合とマージ操作が 結合半束(join-semilattice) をなすからです。状態の集合を S、マージを join(記号では二項演算)とすると、次の3性質が成り立ちます。
- 可換律(commutative):
join(a, b) = join(b, a)。どちらを先にマージしても同じ。 - 結合律(associative):
join(join(a, b), c) = join(a, join(b, c))。グループ化を変えても同じ。 - 冪等律(idempotent):
join(a, a) = a。同じ状態を二度マージしても変わらない。
この3つが揃うと、状態の集合に半順序が入ります。a ≤ b を「join(a, b) = b」と定義すると、join は任意の2要素の 最小上界(least upper bound, LUB) になります。各レプリカの状態は、更新が進むほどこの順序を 単調に上って(monotonically) いきます。
分散環境では配送が乱れます。順序の乱れは可換律と結合律が吸収し、どんな順で混ぜても結果が同じになります。重複配送(同じメッセージが二度届く)は冪等律が吸収し、二度混ぜても無害です。つまり半束の3性質は、信頼できないネットワークが持ち込む「順不同・重複」をそのまま無効化するための条件になっています。だからこそ TCP のような厳密な順序保証や exactly-once 配送がなくても収束します。
形式的には、全レプリカが互いの更新を最終的に1回以上は受け取る(最終配送)と仮定すれば、各レプリカの状態はすべての更新の join、すなわち半束の同じ LUB へ収束します。順序や重複に依存しないのは、上の3性質が「混ぜる順番と回数を結果から消す」からです。
状態ベースと操作ベース
CRDT には伝播方式の異なる2系統があります。
状態ベース(CvRDT, convergent) は、各レプリカが自分の 状態全体 を相手に送り、受け手は merge = join で自分の状態と併合します。半束なので順不同・重複に強く、配送は冪等・可換。ゴシップ通信のように「ときどき状態をまるごと交換する」運用に向きます。欠点は状態が大きいと転送量がかさむことです(差分だけ送る delta-state CRDT で軽減します)。
操作ベース(CmRDT, commutative) は、状態ではなく 操作(更新) を他レプリカへ配送し、各レプリカでローカルに適用します。収束の条件は「並行する操作どうしが可換」であること。ただし操作は重複適用すると壊れるものが多いため、各操作を高々一度ずつ(重複なく)全レプリカへ届ける 配送層が必要です。順序は因果順(causal delivery)まで緩められますが、配送保証は状態ベースより重くなります。
| 観点 | 状態ベース(CvRDT) | 操作ベース(CmRDT) |
|---|---|---|
| 伝播するもの | 状態(または差分) | 操作そのもの |
| マージの性質 | join が可換・結合・冪等 | 並行操作が可換であればよい |
| 配送への要求 | 弱い(順不同・重複OK) | 強い(重複なし・因果順の配送層が必要) |
| 主なコスト | 状態の転送量が大きい | 配送層の実装が複雑 |
| 典型運用 | ゴシップで状態交換 | 操作をログ/メッセージで配送 |
両者は理論上等価で、片方で表せる型はもう片方でも表せます。実装の都合(帯域か配送保証か)で選びます。
代表的な CRDT を組み立てる
カウンタ:GCounter と PNCounter
増加だけを扱う GCounter は、レプリカごとの増加量を持つベクタです。各レプリカ i は自分のスロットだけ増やし、merge は 要素ごとの max を取ります。max は可換・結合・冪等なので半束になり、合計値は全スロットの和で読み出します。
GCounter(レプリカ A, B, C)
A: [a=3, b=0, c=0]
B: [a=0, b=2, c=0]
merge(A,B) = [max(3,0), max(0,2), max(0,0)] = [3,2,0] → 合計 5
なぜ max なのか。各スロットは「そのレプリカが出した増加の累計」で 単調増加 します。古い値と新しい値を混ぜたとき、欲しいのは新しい方(大きい方)だけ。だから要素ごと max で、遅れて届いた更新も二重に届いた更新も正しく1回ぶんに収まります。
減算もしたい場合は、増加用 P と減算用 N の2本の GCounter を持つ PNCounter にします。値は sum(P) - sum(N)。減算を単独で冪等にするのは難しいため、「増える一方のカウンタ2本の差」に分解するのが定石です。
集合:ORSet
集合は要素ごとに「最後の操作が add か remove か」を決めねばならず、素朴には競合します。ORSet(Observed-Remove Set) は、add のたびに一意なタグ(ユニークID)を付け、remove は その時点で観測したタグだけ を消すことで解決します。要素が「生きている」とは、追加タグの集合から削除タグの集合を引いた差が空でないこと。
ORSet では、ある要素への add と remove が並行に起きると、remove は「自分が見たタグ」しか消せません。並行 add が付けた新しいタグは remove の観測に入っていないので生き残り、結果として add 優先で収束します。これは直感(消したのに、別拠点で足したぶんは残る)に合致します。逆を望むなら remove 優先の変種を使います。タグを使うぶん削除済み要素のメタデータ(トゥームストーン)が溜まるのが弱点で、定期的な回収が要ります。
レジスタ:LWWRegister
単一値を上書きするだけなら LWWRegister(Last-Write-Wins) が使えます。各書き込みにタイムスタンプを付け、merge は タイムスタンプが大きい方 を採る(同着はレプリカIDで決定的に破る)。これも max ベースの半束です。実装が軽い反面、並行更新の一方を捨てる ため、データを失いたくない用途には向きません。物理時計のずれを避けるためにロジカルクロックを使うのが通例です。
できること・できないこと
CRDT は調整なしで収束する代わりに、収束する状態は「マージ規則が決める唯一の解」 に限られます。たとえば「残高は絶対にマイナスにならない」のような 大域不変条件 は、各レプリカが独立に書く以上、マージ後に破れることがあります(2拠点が同時に引き出すと合計が在庫を超える等)。こうした不変条件には、結局なんらかの調整(予約・上限の事前配分)か、強整合な経路が必要です。
CRDT が保証するのは「最終的に全レプリカが同じ状態に収束する」ことだけで、任意の時点で最新値が読めることではありません。読み取った瞬間は他拠点の更新がまだ反映されず、古く見えることがあります。線形化可能性(linearizability)や即時の一意な答えが要る処理(口座残高の確定、ユニーク制約)は、CRDT ではなく合意ベースの強整合経路で扱うべきです。用途ごとに整合モデルを使い分けるのが実務の基本です。
「CRDT が収束する代数的条件は」には 可換・結合・冪等(結合半束を構成し merge が最小上界) と答えます。「状態ベースと操作ベースの違いは」は 前者は状態を join で併合し配送要求が弱い/後者は可換操作を重複なく因果順で配送する。「GCounter の merge はなぜ要素ごと max か」は 各スロットが単調増加で、遅延・重複を1回ぶんに収めるため。「CRDT で守れない整合は」は 残高非負などの大域不変条件、と整理しておくと安全です。
まとめ
CRDT は、状態とマージ操作が 結合半束(可換・結合・冪等) をなすように型を設計することで、更新の順序・重複によらず全レプリカを 同じ最小上界へ単調収束 させるデータ構造です。状態ベースは状態を join で併合し配送要求が弱く、操作ベースは可換な操作を重複なく配送します。GCounter(要素ごと max)、PNCounter(増減を2本に分解)、ORSet(タグで add 優先)、LWWRegister(タイムスタンプ max)など、型ごとに半束を構成します。合意プロトコルなしで高可用と結果整合を両立できる一方、残高非負のような大域不変条件は保証できないため、レプリケーションとシャーディングの設計や強整合が要る処理とは役割を分けて使うのが定石です。
データベース Article
CRDT(衝突なしレプリケーション型データ構造)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
CRDT
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
状態ベース(CvRDT)は状態そのものを join で併合し、操作ベース(CmRDT)は可換な操作を高々一度ずつ配送して反映する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「CRDT / 結果整合性」に近いか確認する。
- 強みである「CRDT は更新を可換・結合・冪等なマージで畳み込む半束として設計し、適用順や重複に関係なく全レプリカを同じ値へ収束させる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。