PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

非同期Dual Writeの落とし穴:ゼロダウンタイム移行が「データ破壊」になる理由

記事のキーポイント

この記事が言っていることを、ざっくり一言でいうと

​「Dual Writeは、うまく設計しないと“無停止移行”ではなく“静かに壊れる移行”になる」​ という話です。

これ、かなり大事です。
ゼロダウンタイム移行というと、なんとなく「止めずに安全に引っ越せる魔法」っぽく聞こえますが、現実はそんなに甘くない。むしろ止まらないまま壊れるのがいちばん怖い。壊れた瞬間に気づけないからです。

Dual Writeって何?

Dual Write は、​同じデータを古いシステムと新しいシステムの両方に書くやり方です。

たとえば、users テーブルを新しいDBへ移行したいとします。

image_0003.svg

という流れです。

一見すると「これなら止めずに移行できそう」と思いますよね。
私も最初に見ると、かなり筋が良さそうに見えると思います。問題は、​2回書くこと自体が安全ではないことです。

何がそんなに危ないのか

記事の中心はここです。

例:片方だけ成功する

ユーザーがプロフィールを更新したとします。

このとき、アプリが「旧DBが成功したからOK」と返してしまうと、​新DBには古い情報が残るわけです。

image_0004.svg

これが1回だけならまだしも、実運用では何日も何週間も、こういう小さな失敗が積み重なります。
すると、最終的に新DBへ読み取りを切り替えた瞬間に、ユーザーは

みたいな地獄を見ることになります。

ここがこの話のいちばん嫌なところで、​障害として見えるのは「移行が終わった後」​ なんですよね。
つまり、移行中は静か。なのに、あとで爆発する。これは本当に厄介です。

Expand-Contractパターンの中で起こること

記事では、ゼロダウンタイム移行でよく使われる Expand-Contract パターンも紹介しています。

これはざっくりいうと、こんな流れです。

image_0005.svg

  1. Expand
    新しいスキーマやDBを追加し、古いシステムも残したまま動かす
  2. Migrate Data
    既存データを新側へ移す
  3. Validate
    古いDBと新しいDBを比べて、ズレがないか確認する
  4. Contract
    読み取りを新側へ切り替え、古い側を片付ける

この中で、Dual Write は Expand フェーズでよく使われます。

図としては、

という構造です。

ただし、ここで重要なのは、​DBを2つにまたがって書く操作は、原則として1つの原子的な処理ではない ということです。
専門用語でいう atomic は「全部成功するか、全部失敗するか」のこと。
Dual Write は普通、その保証がありません。

だから、​片方成功・片方失敗 が起こりうる。
これは理屈では当たり前ですが、実装するときにうっかり忘れがちなんですよね。かなり人間っぽい落とし穴だと思います。

Stripeのやり方が示唆的で面白い

image_0006.svg

記事では、Stripe のような大規模な会社がどう対処しているかにも触れています。
Stripe は非常に多くのスキーマ変更を行っているようで、その前提として「Dual Write は失敗しうるもの」と割り切っているのがポイントです。

彼らの対策として挙げられているのは主に3つです。

1. Shadow Write

これは、​本番と同じ書き込みを新システムにも流すけど、まだ本命扱いはしない 方式です。

新しいDBにはデータを入れてみるけれど、それを結果としては使わない。
いわば「本番リハーサル」です。

これ、かなり賢いと思います。
いきなり本番の正解にしないで、まずは負荷や整合性を観察する。これは現実的です。

2. Idempotency と Retries

idempotent(冪等)​ というのは、同じ処理を何回やっても結果が壊れない性質のことです。
たとえば、「同じ注文を2回送っても重複しない」ように作るイメージです。

image_0007.svg

Dual Write では片方が失敗したときに再試行が必要になります。
そのとき、冪等でないと「再試行したら二重登録された」みたいな事故が起きる。
これは本当にイヤなバグです。再試行が善意なのに、むしろ事故を増やすからです。

3. Continuous Reconciliation

ここがいちばん重要だと感じました。

reconciliation は、古いDBと新しいDBを定期的・継続的に比較して、ズレを見つけて直す作業です。
たとえば、

といった差分を自動検出します。

記事では、これを「安全網」と表現しています。
まさにその通りで、Dual Write を本番でやるなら、​突合作業なしはかなり無謀 だと思います。

image_0008.svg

正直なところ、Dual Write の本体よりも、この reconciliation の方が本番ではよほど重要ではないか、という気すらします。
「書くこと」より「ズレを見つけて直すこと」の方が難しいからです。

よくある失敗パターン

記事では、Dual Write 実装でありがちな間違いも整理されています。

1. DBをまたいだ原子的処理だと思い込む

db1.save()db2.save() を並べただけでは、当然ながら1つのトランザクションにはなりません。
片方成功、片方失敗は普通に起こります。

ここを「まあ順番に呼ぶだけでしょ」と軽く見ると、あとで痛い目を見る。
分散システムの怖さって、まさにこういうところにあります。

2. 移行中の読み取り戦略が曖昧

Dual Write している最中に、どこから読むのかをはっきり決めないといけません。

image_0010.png

記事では、次の3パターンが挙げられています。

個人的には、​読み取りの正解を曖昧にしたまま移行を進めるのが一番危ない と思います。
書き込みの話ばかりに目が行きますが、実際には「どっちを真実とするか」を決める方がずっと大事です。

3. 監視と検証を軽視する

Dual Write と backfill ジョブを作っただけで安心してしまうのも危険です。

必要なのは、

です。

image_0012.png

これがないと、​静かにデータが壊れていく
しかも静かなので、チーム全体が「順調です」と思い込んでしまう。ここが本当に怖いところです。

面接でも問われやすいポイント

記事の後半には、システムデザイン面接で聞かれそうな質問も載っています。

たとえば、

片方のDBへの書き込みは成功したが、もう片方が失敗したらどうする?

これに対しては、単に「リトライします」だけでは弱いです。
より強い答えとしては、

image_0013.png

という流れが挙げられています。

要するに、​同期的な完璧さを目指すのではなく、非同期で壊れたものを後から必ず直す仕組みを持つ という考え方です。
この発想はかなり現実的だと思います。

もう1つの面接質問は、

shadow write と dual write をどう使い分ける?

これも良い問いです。

という違いがあります。

私はこの区別、かなり重要だと思います。
似た言葉ですが、​リスクの重さが全然違う からです。

image_0014.png

この話の本質

この記事を読んでいちばん強く感じたのは、
​「データ移行は、移す作業ではなく、壊れないように管理し続ける作業」​ だということです。

Dual Write は便利です。
でも便利なぶん、油断しやすい。
そして、油断した瞬間に「ゼロダウンタイム」が「ゼロ整合性」になる。

ここが実に皮肉で、しかも現場ではかなりありがちな失敗だと思います。

まとめ


参考: The Production Problem with Async Dual Writes

同じ著者の記事