「Dual Writeは、うまく設計しないと“無停止移行”ではなく“静かに壊れる移行”になる」 という話です。
これ、かなり大事です。
ゼロダウンタイム移行というと、なんとなく「止めずに安全に引っ越せる魔法」っぽく聞こえますが、現実はそんなに甘くない。むしろ止まらないまま壊れるのがいちばん怖い。壊れた瞬間に気づけないからです。
Dual Write は、同じデータを古いシステムと新しいシステムの両方に書くやり方です。
たとえば、users テーブルを新しいDBへ移行したいとします。
という流れです。
一見すると「これなら止めずに移行できそう」と思いますよね。
私も最初に見ると、かなり筋が良さそうに見えると思います。問題は、2回書くこと自体が安全ではないことです。
記事の中心はここです。
ユーザーがプロフィールを更新したとします。
このとき、アプリが「旧DBが成功したからOK」と返してしまうと、新DBには古い情報が残るわけです。
これが1回だけならまだしも、実運用では何日も何週間も、こういう小さな失敗が積み重なります。
すると、最終的に新DBへ読み取りを切り替えた瞬間に、ユーザーは
みたいな地獄を見ることになります。
ここがこの話のいちばん嫌なところで、障害として見えるのは「移行が終わった後」 なんですよね。
つまり、移行中は静か。なのに、あとで爆発する。これは本当に厄介です。
記事では、ゼロダウンタイム移行でよく使われる Expand-Contract パターンも紹介しています。
これはざっくりいうと、こんな流れです。
この中で、Dual Write は Expand フェーズでよく使われます。
図としては、
という構造です。
ただし、ここで重要なのは、DBを2つにまたがって書く操作は、原則として1つの原子的な処理ではない ということです。
専門用語でいう atomic は「全部成功するか、全部失敗するか」のこと。
Dual Write は普通、その保証がありません。
だから、片方成功・片方失敗 が起こりうる。
これは理屈では当たり前ですが、実装するときにうっかり忘れがちなんですよね。かなり人間っぽい落とし穴だと思います。
記事では、Stripe のような大規模な会社がどう対処しているかにも触れています。
Stripe は非常に多くのスキーマ変更を行っているようで、その前提として「Dual Write は失敗しうるもの」と割り切っているのがポイントです。
彼らの対策として挙げられているのは主に3つです。
これは、本番と同じ書き込みを新システムにも流すけど、まだ本命扱いはしない 方式です。
新しいDBにはデータを入れてみるけれど、それを結果としては使わない。
いわば「本番リハーサル」です。
これ、かなり賢いと思います。
いきなり本番の正解にしないで、まずは負荷や整合性を観察する。これは現実的です。
idempotent(冪等) というのは、同じ処理を何回やっても結果が壊れない性質のことです。
たとえば、「同じ注文を2回送っても重複しない」ように作るイメージです。
Dual Write では片方が失敗したときに再試行が必要になります。
そのとき、冪等でないと「再試行したら二重登録された」みたいな事故が起きる。
これは本当にイヤなバグです。再試行が善意なのに、むしろ事故を増やすからです。
ここがいちばん重要だと感じました。
reconciliation は、古いDBと新しいDBを定期的・継続的に比較して、ズレを見つけて直す作業です。
たとえば、
といった差分を自動検出します。
記事では、これを「安全網」と表現しています。
まさにその通りで、Dual Write を本番でやるなら、突合作業なしはかなり無謀 だと思います。
正直なところ、Dual Write の本体よりも、この reconciliation の方が本番ではよほど重要ではないか、という気すらします。
「書くこと」より「ズレを見つけて直すこと」の方が難しいからです。
記事では、Dual Write 実装でありがちな間違いも整理されています。
db1.save() と db2.save() を並べただけでは、当然ながら1つのトランザクションにはなりません。
片方成功、片方失敗は普通に起こります。
ここを「まあ順番に呼ぶだけでしょ」と軽く見ると、あとで痛い目を見る。
分散システムの怖さって、まさにこういうところにあります。
Dual Write している最中に、どこから読むのかをはっきり決めないといけません。

記事では、次の3パターンが挙げられています。
個人的には、読み取りの正解を曖昧にしたまま移行を進めるのが一番危ない と思います。
書き込みの話ばかりに目が行きますが、実際には「どっちを真実とするか」を決める方がずっと大事です。
Dual Write と backfill ジョブを作っただけで安心してしまうのも危険です。
必要なのは、
です。

これがないと、静かにデータが壊れていく。
しかも静かなので、チーム全体が「順調です」と思い込んでしまう。ここが本当に怖いところです。
記事の後半には、システムデザイン面接で聞かれそうな質問も載っています。
たとえば、
片方のDBへの書き込みは成功したが、もう片方が失敗したらどうする?
これに対しては、単に「リトライします」だけでは弱いです。
より強い答えとしては、

という流れが挙げられています。
要するに、同期的な完璧さを目指すのではなく、非同期で壊れたものを後から必ず直す仕組みを持つ という考え方です。
この発想はかなり現実的だと思います。
もう1つの面接質問は、
shadow write と dual write をどう使い分ける?
これも良い問いです。
という違いがあります。
私はこの区別、かなり重要だと思います。
似た言葉ですが、リスクの重さが全然違う からです。

この記事を読んでいちばん強く感じたのは、
「データ移行は、移す作業ではなく、壊れないように管理し続ける作業」 だということです。
Dual Write は便利です。
でも便利なぶん、油断しやすい。
そして、油断した瞬間に「ゼロダウンタイム」が「ゼロ整合性」になる。
ここが実に皮肉で、しかも現場ではかなりありがちな失敗だと思います。