【Cloud Spannerの核心】Paxosリーダー選出のメカニズムと、障害に怯えない分散設計の真髄
こんにちは。テクニカルリードの私だ。
今日のコードレビュー、あるいはアーキテクチャ設計レビューで、こんな質問を受けたとしよう。
> 「Cloud Spannerはマネージドだから可用性が高いのはわかるけどさ、もし背後でリーダーノードが死んだら、トランザクションはどうなるの?数秒間止まったりしない?」
この問いに対して、公式ドキュメントの表面的な言葉を並べるだけで終わるようでは、チーフアーキテクトとしては失格だ。
我々は、Cloud Spannerが内部で動かしている分散合意アルゴリズム「Paxos」、そしてその中で繰り広げられるリーダー選出プロセスとタイムアウトの物理的現実を完全に理解し、それを見越したアプリケーション設計を行わなければならない。
今回は、Spannerのコアアーキテクチャの心臓部である「Paxosグループのリーダー選出」に直球で焦点を当て、実務で絶対に知っておくべき知見を授けよう。
—
1. Cloud Spannerの裏側:Paxosグループとリーダーの役割
まず前提を共有しよう。Cloud Spannerは、データを単一の巨大なデータベースとして扱わせるが、物理的には数千、数万の「スプリット(Split)」と呼ばれるシャードに分割され、世界中に分散配置されている。
各スプリットは、高可用性と耐障害性を担保するために、複数(通常は奇数、例えば5つや3つ)のレプリカによってPaxosグループを形成している。
ここで重要なのは、「読み書き(Read-Write)のすべてのトランザクションは、必ずそのPaxosグループの『リーダー』を経由しなければならない」という点だ。
- リーダーの役割:
- クライアントからの書き込みリクエストを受け付ける。
- タイムスタンプを割り振る(TrueTimeとの連携)。
- Paxosのプロポーザ(提案者)として、他のレプリカ(アクセプター)に合意形成のログを同期する。
つまり、リーダーがダウンするということは、そのスプリットに対する書き込みのパイプラインが一時的に途絶えることを意味する。では、その時Spannerの内部では何が起きているのか?
—
2. リーダー故障と選出プロトコルの実態
Paxosグループ内では、リーダーは「永久にリーダー」なのではない。心拍(Heartbeat)のやり取りによってその地位を維持している。
2.1 障害検知(ハートビートの途絶)
リーダーレプリカは、定期的にフォロワーレプリカに対してハートビートを送信し、「私は生きている、まだリーダーだ」と主張し続ける。
もし、ネットワークの分断やノード自体のハードウェア障害、あるいはGC(ガベージコレクション)の停止などによって、リーダーからのハートビートが一定期間(数秒未満の非常にシビアな閾値)途絶えると、フォロワーたちは次のように判断する。
> 「リーダーが死んだ。あるいは孤立した。」
2.2 新リーダーの選出(Leader Election)
リーダーの不在を検知したフォロワーたちは、Paxosのアルゴリズムに基づき、速やかに新しいリーダーを選出するためのプロセスを開始する。
1. term(任期)のインクリメント: 各レプリカは自身の持つterm番号を繰り上げ、自身を次の候補者(Candidate)とする。
2. 投票の依頼: 他のレプリカに対して「私を次のリーダーにしてくれ」と投票をリクエストする。
3. 過半数の合意(Quorum): ペンディング中のログを含め、最も進んだ(最新の)状態を持っているレプリカが、過半数(Quorum)の賛成票を獲得した時点で、新しいリーダーとして即座に戴冠する。
この一連のプロセスは、Googleの高度に最適化されたインフラストラクチャ上では、数秒(多くの場合、数千ミリ秒単位)で完了する。人間が体感する「データベースが死んだ」というレベルの停止ではなく、ミリ秒単位のマイクロ・アウトテージ(瞬断)として処理される。
—
3. 実務へのインパクト:この仕組みから何を設計に落とし込むか?
「おっ、じゃあ自動でリーダーが選出されるから、アプリ側は何もしなくていいんだね?」と思ったそこのあなた。甘い。実務の現場では、この「数秒のリーダー選出期間」が引き起こす挙動を考慮した設計が不可欠だ。
3.1 接続断とリトライの嵐(Thundering Herd Problem)
リーダーが切り替わる瞬間、旧リーダーに送られていたインフライト(処理中)のトランザクションはアボート(中断)されるか、タイムアウトを迎える。
ここでアプリケーション側が「指数バックオフ(Exponential Backoff)を伴う適切なリトライ機構」を実装していないとどうなるか?
新リーダーが選出され、さあ受け付けを再開するぞという瞬間に、数千のクライアントからのリトライリクエストが一斉に殺到し、新リーダーが再び過負荷で倒れるという最悪のバッドパターン(雪崩現象)を引き起こす。
【設計プラクティス】
Spannerの公式クライアントライブラリは、デフォルトでトランザクションのリトライを賢くハンドリングしてくれるが、独自にカスタムクエリやバッチ処理を組む場合は、必ずジッター(Jitter)を含めたリトライロジックを強制すること。
悪い例:リトライ間隔が固定、またはリトライなし
良い例:ジッター付き指数バックオフの概念実装(疑似コード)
import time
import random
def execute_with_spanner_retry(transaction_func, max_retries=5):
retries = 0
base_delay = 0.1 # 100ms
while retries < max_retries:
try:
return transaction_func()
except SpannerAbortedError as e:
retries += 1
if retries >= max_retries:
raise e
# 指数関数的に待ち時間を増やし、ランダムなジッター(揺らぎ)を加えてリトライのタイミングを分散させる
sleep_time = (base_delay (2 retries)) + (random.uniform(0, 1) 0.1)
time.sleep(sleep_time)
3.2 ホットスポット(Hotspotting)の回避
リーダー選出プロセスそのものよりも厄介なのは、「特定のリーダーに負荷が集中する構造」を作ってしまうことだ。
例えば、次のようなプライマリキー設計をしていないだろうか?
- `user_id` に連番(Auto Increment)を使っている。
- `created_at` のようなタイムスタンプをキーの先頭に置いている。
これらはすべて、ある特定の瞬間に「ひとつのスプリット、ひいてはひとりのPaxosリーダー」にすべての書き込みが集中する、いわゆる「ホットスポット」を生み出す。
リーダーがどれだけ優秀なアルゴリズムで選出されようとも、物理的なCPUやネットワークの限界を超えた負荷が1台のリーダーに集中すれば、パフォーマンスは劣化し、最悪の場合はリーダーの応答遅延から障害へと発展する。
【設計プラクティス】
キーの先頭には必ずハッシュ化された値や、適切に分散するプレフィックス(UUIDなど)を配置し、書き込み負荷が綺麗に複数のPaxosグループ(=複数のリーダー)に分散するアーキテクチャをファーストチョイスとしろ。
—
4. チーフアーキテクトからの最終提言
Cloud SpannerのPaxosリーダー選出は、Googleの分散システム研究の粋を集めた驚異的な自動化機構だ。開発者が手動でフェイルオーバーのスクリプトを書く必要など微塵もない。
しかし、「マネージドだからブラックボックスのままでいい」というのはプロのエンジニアのセリフではない。
- リーダーの喪失と再選出には、不可避のミリ秒単位のレイテンシスパイクが存在すること。
- アプリケーション側は、その一時的なアボートに対して「リトライとジッター」で優しく寄り添う必要があること。
- そもそも1つのリーダーに負荷を集中させない、美しく分散されたデータモデリングを行うこと。
これらを理解し、コードと設計に落とし込めて初めて、「Cloud Spannerを真に使いこなしている」と言える。
今日のレビューからは、これらの観点がコードに反映されているか厳しくチェックさせてもらう。健闘を祈る。
コメント