インメモリOCC:Silo とエポックベースコミット
メニーコアで集中ロックがボトルネックになる――Silo はエポック単位でコミット順を確定し、共有カウンタを排した OCC でほぼ線形にスケールします。その検証フェーズと TID 設計を原理から解説します。
- 1.Silo は各レコードに TID とロックビットを埋め込み、読み取りは TID を記録するだけのロックフリー。コミット時に write set だけをロックし、read set の TID 変化と書き込み中フラグを検証して、変わっていれば自分をアボートする。
- 2.直列順を決める集中タイムスタンプを廃し、約40msごとに進む共有エポックだけを全コアで共有する。TID 上位にエポックを埋め込み、コミット順はエポック境界でのみ確定するため、コミットパスに共有カウンタへの書き込みが現れずスケールする。
- 3.永続化(durability)もエポック単位で行い、安定エポックまでのトランザクションをまとめて WAL に流してグループコミットする。応答はそのエポックが永続化されるまで保留する。
Silo が解く問題:集中点がスケールを殺す
メニーコア(数十コア)のサーバー上でインメモリ OLTP を回すと、ボトルネックは I/O ではなくコア間で共有される一点に移ります。グローバルなロックテーブル、トランザクション ID を払い出す集中カウンタ、コミット順を決めるタイムスタンプ発行器――これらは1つの共有メモリ語への読み書きを全コアに強制し、キャッシュコヒーレンシのトラフィックがコア数に比例して膨らみます。結果として「正しさのための集中点」がスループットの上限を決めてしまいます。
Silo(Tu ら, 2013)は、楽観的並行性制御(OCC)を土台にしつつ、コミットパスから共有カウンタへの書き込みをすべて取り除くことでこの壁を破ります。鍵は、直列順を「1件ごとの集中タイムスタンプ」ではなく「**エポック(時代)**という粗い時間区切り」で決めることです。OCC そのものの読み取り・検証・書き込みの三段構成はオプティミスティック並行性制御を前提にします。本稿はそこに Silo 固有の TID 設計とエポックを足して解説します。
TID:レコードに埋め込む状態
Silo はロックテーブルを別に持たず、各レコードのヘッダに TID(Transaction ID)ワードを1つ埋め込みます。この1語が同時実行制御の状態をすべて担います。
1つの符号なし整数語を3つに区切って使います。
- エポック番号(上位ビット):このレコードを最後に書いたトランザクションが属したエポック。直列順の粗い決定に使う。
- シーケンス(中位ビット):同一エポック内での順序を表す単調増加部分。
- 状態ビット(下位):
lock(書き込み中の排他)・latest(最新版か)・absent(削除済み)などのフラグ。
読み取り側はこの TID をアトミックに1語読むだけで、レコードの「版」と「ロック中か」を同時に知れます。読み取りはロックを一切取りません。読んだレコードについて (レコードへの参照, 読んだ時点の TID) を read set に記録するだけです。書き込みは自分専用の作業領域(write set)に溜め、共有データには触れません。ここまでは普通の OCC の読み取りフェーズです。
Silo の TID で重要なのは、TID が単調増加する集中カウンタから払い出されるのではない点です。後述するように、TID はコミット時に「自分が読み書きしたレコードの TID とエポック」からローカルに計算されます。これがスケールの肝になります。
コミットプロトコル:3フェーズの検証
コミット要求が来ると、Silo は次の手順で検証します。デッドロックを避けるため、ロックは決まった順序(例えばアドレス順)で取ります。
# フェーズ1: write set のロック
write set の各レコードを、グローバルに一意な順序でソートし
TID の lock ビットを compare-and-swap で立てる(取れなければスピン)
# ここでメモリフェンスを張り、現在の共有エポックを読む -> commit-epoch とする
# フェーズ2: read set の検証
for each (レコード r, 読んだときの TID t) in read set:
cur = r の現在の TID をアトミック読み
if cur != t: # 自分が読んだ後に誰かが書き換えた
abort
if cur.lock がセット and r は自分の write set にない:
abort # 他者がいま書き込み中
# 自分がロック中の行は自分のものなので除外
# フェーズ3: TID 計算と書き戻し
新 TID = 「read/write set 内の全 TID より大きく、
かつ commit-epoch を上位に持つ最小の TID」をローカル計算
write set を実レコードへ反映し、各レコードの TID を新 TID に更新、lock を解放
古典 OCC は「自分と並行した他トランザクションの read/write set」と突き合わせて検証しますが(→ OCC とタイムスタンプ順序)、Silo は他者の集合を一切見ません。代わりに「読んだレコードの TID が、読んだ時から変わっていないか」だけを見ます。TID はレコードが書き換わるたびに必ず増えるので、TID が同じなら誰も触っていないと確定でき、これは read set 検証として十分です。集中構造を参照せずローカルなレコード読みだけで済むのが、スケールする理由です。
フェーズ1で write set を先にロックしてからエポックを確定させる順序が本質的です。先にロックを取り、メモリフェンスを挟んでから commit-epoch を読むことで、「ロック中フラグが立った状態」を並行読者から確実に見えるようにし、かつ自分のコミットがどのエポックに属するかを固定します。これにより、検証中の他トランザクションは自分のロック中レコードに対してアボートを選び、矛盾した直列化が起きません。
エポック:集中カウンタを消す仕掛け
直列順を「1コミットごとに一意な順番」で決めようとすると、必ず単調増加カウンタへの書き込みが要り、それが集中点になります。Silo はここを粗くすることで回避します。
専用スレッドが約40ミリ秒ごとにグローバルエポック番号 E を1つ進めるだけです。各ワーカーコアはコミット時にこの E を読むだけで(書き込まない)、その値を新 TID の上位に埋め込みます。同じエポック内で多数のトランザクションがコミットしても、上位のエポック番号は共通で、下位のシーケンス部分だけが各レコードごとにローカルに決まります。
Silo は同一エポック内のトランザクション間に全順序を保証しません。直列化可能性が要求するのは「ある直列順と等価な結果」であって、その直列順がコミット時刻と一致する必要はありません。エポック境界をまたぐ依存だけがエポック番号で順序づけられ、エポック内の並びは read set 検証が矛盾を排除します。「順序を決めるのはエポック境界、エポック内は検証で整合させる」という分業が、集中カウンタの除去を可能にします。
この設計の効果は、コミットパス上に書き込み先の共有語が無くなることです。各コアは自分が触ったレコードの TID(局所)とグローバルエポック(読むだけ)しか参照しないため、コア数を増やしてもキャッシュ行の奪い合いが起きず、スループットがほぼ線形に伸びます。エポックを進める1スレッドの書き込みは40msに1回なので無視できます。
永続化もエポック単位で
エポックは並行性制御だけでなく永続化(durability)の単位も兼ねます。Silo は各ワーカーのコミット結果をエポックごとにまとめ、ログスレッドが WAL に書き出します。
durability の流れ:
各ワーカー: コミットしたトランザクションのログを、その commit-epoch のバッファへ
ログスレッド: あるエポック e のログを全ワーカー分集めて fsync で永続化
「永続化が完了した最大エポック」= persistent epoch を公開
クライアント応答: そのトランザクションの commit-epoch <= persistent epoch
になるまで保留してから返す
つまり個々のトランザクションを1件ずつ fsync するのではなく、1エポック分のログをまとめて1回の fsync で落とす――これはグループコミットと fsync バッチングそのものの粗粒度版です。応答を persistent epoch まで遅らせるため、クラッシュ後に「応答したのに失われた」トランザクションは生じません。エポックが40ms粒度なので、遅延の上限もエポック周期に収まります。
リカバリ時は、各レコードの TID(上位のエポックとシーケンス)が版の新旧を表すので、ログを適用する際により大きい TID を持つ書き込みだけを採用すれば、ログの適用順に依存せず最新状態を復元できます。これはWALの REDO に TID を版スタンプとして使う形です。
2PL・古典 OCC との対比
| 観点 | 2PL(集中ロック) | 古典 OCC | Silo(インメモリ OCC) |
|---|---|---|---|
| 読み取り時のロック | 共有ロックを取る | 取らない | 取らない(TID を記録するだけ) |
| 直列順の決め方 | ロック取得順 | コミット番号(集中カウンタ) | エポック境界+レコード TID |
| コミットパスの共有書き込み | ロックテーブル更新 | 集中トランザクション番号 | なし(エポックは読むだけ) |
| 検証の対象 | —(ロックで防ぐ) | 他者の read/write set | 自分の read set の TID 変化のみ |
| デッドロック | 起きうる | 起きない | 起きない(write set をアドレス順にロック) |
| スケール上の弱点 | ロック競合・待ち | 集中カウンタが競合 | ホット行への書き込み集中時のアボート |
Silo は楽観的方式の長所(読み取りがブロックしない・デッドロックが原理的に起きない)を保ったまま、古典 OCC に残っていた集中トランザクション番号という最後のスケール阻害要因をエポックで消した、と整理できます。一方で OCC である以上、少数のホットなレコードに書き込みが集中するワークロードでは read set 検証の失敗が増え、アボート&リトライのコストが跳ね上がる弱点は残ります。これは2相ロックが「待ってでも確実に進む」のと表裏の関係です。
なお Silo は版を残さない単一版方式で、読み取りも最新版を見て検証で守ります。古い版を保持して読みをアボートさせないMVCC系(MVTO や SSI)とは設計思想が異なり、Silo はメモリ効率と検証の単純さを優先しています。
まとめ
- Silo は各レコードに TID とロックビットを埋め込み、読み取りはロックフリーで TID を read set に記録するだけ。コミット時に write set をアドレス順にロックし、read set の TID 変化と書き込み中フラグだけを見て検証する。
- 直列順を決める集中タイムスタンプを廃止し、約40msで進むグローバルエポックを読むだけにした。TID 上位にエポックを埋め込み、コミットパスから共有語への書き込みを消したことでメニーコアでほぼ線形にスケールする。
- エポックは永続化の単位でもあり、1エポック分のログをまとめて fsync するグループコミットで durability を取り、応答を persistent epoch まで保留して喪失を防ぐ。
- 楽観的方式の利点(非ブロッキング・デッドロックなし)を保ちつつ集中点を除いた設計だが、ホット行への書き込み集中にはアボート増という OCC 共通の弱点が残る。
データベース Article
インメモリOCC:Silo とエポックベースコミットを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
直列順を決める集中タイムスタンプを廃し、約40msごとに進む共有エポックだけを全コアで共有する。TID 上位にエポックを埋め込み、コミット順はエポック境界でのみ確定するため、コミットパスに共有カウンタへの書き込みが現れずスケールする。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「データベース / トランザクション」に近いか確認する。
- 強みである「Silo は各レコードに TID とロックビットを埋め込み、読み取りは TID を記録するだけのロックフリー。コミット時に write set だけをロックし、read set の TID 変化と書き込み中フラグを検証して、変わっていれば自分をアボートする。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。