Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】A cache key ate 99.9% of my records and the pipeline looked green

2026年10月05日に「Dev.to」が公開したITニュース「A cache key ate 99.9% of my records and the pipeline looked green」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

データ処理パイプラインでキャッシュキー誤用によりデータが99.9%消失、システムは正常と判断した。この問題を防ぐには、各工程で入出力データ件数を必ず確認し、異常があれば停止する仕組みが不可欠だ。リランを容易にし、履歴を上書きしない設計も重要である。

ITニュース解説

ある日、データ処理パイプラインでとんでもないバグが発生した。それは、システムが正常に動作していると報告し続けながら、実際にはほとんどのデータをサイレントに消し去ってしまうというものだった。具体的には、投入されたレコードの99.9%が失われ、たった0.1%しか処理されない状況が何週間も続いたのである。

このバグの恐ろしい点は、エラーメッセージもスタックトレースもなく、ジョブも失敗せず、アラートも上がらなかったことだ。パイプラインの実行は常に「成功(グリーン)」と表示され、決められた時間内に完了し、自身が正常だと報告していた。なぜなら、ジョブの健全性を測る指標が「実行回数」であり、「処理されたレコードの行数」ではなかったからだ。この問題が発覚したのは、ビジネスユーザーが作成されたキャンペーンリストが短いことに気づき、疑問を投げかけた時だった。これは「成功しているのに何もしていない」というタイプのバグであり、システム開発において特に注意すべき点を示唆している。

このデータ消失の根本的な原因は、パイプラインの特定の処理段階にあった。この段階では、レコードがバッチで到着し、参照データによって「エンリッチ」(補足情報が付加されること)され、その後に次の処理のために一時的に保管される。問題のコードは非常に単純に見えた。

レコードからアカウントIDを取得し、それをキャッシュキーとして参照する。もしキャッシュにデータがなければ、参照サービスからフェッチしてキャッシュに保存し、その後、レコードとエンリッチされたデータを次の段階に渡す、という流れだ。

ここで見落とされていたのは、「accountId」がレコードごとにユニークではない、という事実だった。一つのアカウントが、異なる製品、異なる請求サイクル、異なる日付など、様々な理由で一つのバッチ内に複数のレコードを生成することがよくある。しかし、キャッシュキーとして「accountId」だけを使用していたため、数千もの異なるレコードが、実際にはごく少数のキャッシュエントリに集約されてしまった。そして、この同じキャッシュキーが、最終的なデータを一時保管する処理でも使われていたため、「後から書き込まれたデータが前のデータを上書きする(Last write wins)」という現象が発生し、結果として大量のデータが失われたのである。パイプラインはすべてのレコードを処理したと認識していたが、ほとんどのレコードを永続化していなかったのだ。

このバグの修正自体は、キャッシュキーに「accountId」だけでなく、「productCode」「billingCycle」「recordDate」といった情報を組み合わせることで、各レコードがユニークなキーを持つようにする、たった一行の変更で済んだ。しかし、本当に重要な教訓は、このバグがシステム内のどこでも検知できなかったこと、つまり「検知できないこと」こそが根本的な欠陥だったという点だ。

このようなサイレントなデータ消失を防ぎ、システムの信頼性を高めるためには、いくつかの習慣を導入することが有効である。

習慣1: 各処理段階の境界でレコードの行数をカウントし、比率を検証する パイプラインの各段階は、どれだけのレコードを受け取り、どれだけのレコードを出力したかを常に把握すべきである。そして、これらの数値が許容範囲外で一致しない場合、その処理を成功とみなすべきではない。

具体的には、各処理段階の実行結果として「受け取ったレコード数」「出力したレコード数」「拒否したレコード数」を記録するデータ構造を用意する。そして、受け取ったレコード数と、出力および拒否されたレコード数の合計が一致するかどうかを確認する。もし一致しなければ、何らかのレコードが消失したと判断し、エラーを発生させる。これは「保存の法則」と呼ばれる。

さらに、出力されたレコード数を受け取ったレコード数で割った比率が、事前に定めた最小比率(例えば95%)を下回る場合もエラーとする。これは、たとえすべてのレコードが何らかの形で「説明可能」であっても、突然のスループットの低下は何か異常が発生したことを示唆するからだ。意図的に特定の条件でレコードをフィルタリングする(例:テストアカウントのレコードを破棄する)場合は、「拒否されたレコード」として明示的な理由を付与することで、意図的な処理とバグによる消失を区別できる。

このような行数に基づく検証がない段階は、たとえ多くの単体テストをパスしていても「未テスト」であると考えるべきだ。実際、今回のキャッシュキーのバグも、テストデータが「アカウントごとに一つのレコード」という単純な構成だったため、単体テストでは検出できなかった。

習慣2: 再実行を「退屈」なものにする 運用可能なパイプラインとそうでないパイプラインを分ける大きな違いは、「昨日と同じバッチを今すぐ再実行したらどうなるか?」という問いに対する答えだ。もしその答えが「最初に何かを確認する必要がある」というものであれば、そのパイプラインは脆弱だと言える。なぜなら、パイプラインの再実行は、システム障害の後、上流データの破損の後、バグ修正の後など、頻繁に発生する「当たり前のこと」だからだ。そして、多くの場合、時間的プレッシャーの中で行われる。

再実行しても常に同じ結果が得られる特性を「冪等性(べきとうせい)」と呼ぶ。これは、後付けで追加するようなものではなく、設計の段階から組み込むべき概念だ。冪等性を実現するための鍵は、すべてのレコードが「決定的な自然キー」を持つこと、そして書き込みがそのキーに基づいて行われることにある。

「自然キー」とは、そのレコードが持つビジネス上の意味合いから一意に特定できるフィールドの組み合わせのことだ。例えば、アカウントID、製品コード、請求サイクル、日付など、ビジネス上のデータそのものから導き出されるキーである。シーケンス番号や読み込みタイムスタンプ、読み込み時に生成されるUUIDなどは、実行するたびに変わってしまうため、自然キーとしては適さない。

この自然キーを使ってデータベースに書き込む際には、ON CONFLICT DO UPDATEのような「UPSERT」(存在すれば更新、なければ挿入)文を利用し、データベースレベルで「record_key」に対するユニーク制約を設定することが重要だ。アプリケーションコードでのチェックでは競合状態が発生する可能性があるが、データベースのユニークインデックスは、複数の処理が同時に動作した場合でも一意性を保証してくれる。

この仕組みを導入することで、単なる安全性の向上にとどまらないメリットがある。例えば、ある一日のデータをすべて削除して再読み込みするような作業が、特別な儀式なしにいつでも実行可能になる。これは、問題が発生した際の修正速度に大きく影響する。

習慣3: 履歴を履歴として扱う(SCD Type 2) もう一つのサイレントなデータ破損は、ディメンションデータを「その場で上書き」することだ。例えば、顧客のセグメント情報が変更された際に、既存のデータベースレコードをUPDATE文で直接更新してしまうと、過去にその古いセグメントを参照して作成されたすべてのレポートが、遡って誤った内容になってしまう。すでに配布されたレポートでさえ、その内容は意味をなさなくなる。

この問題を解決するのが「SCD Type 2」(Slowly Changing Dimension Type 2、ゆっくり変化するディメンション タイプ2)という手法だ。これは、事実を上書きするのではなく、変更が発生したときに既存のレコードを「閉じて」、新しいレコードを「開く」ことで履歴を管理する。

具体的には、顧客情報が変更された場合、古い顧客レコードの有効期限(valid_to)を更新してis_currentフラグをFALSEにし、新しい情報を持つ顧客レコードを新たに挿入し、その有効開始日(valid_from)とis_currentフラグをTRUEにする。このようにすることで、過去のある時点における顧客情報を正確に参照できるようになる。

変更を検出する際には、IS DISTINCT FROMというSQL句が非常に役立つ。これはNULL値を安全に比較できるため、NULLから実際の値への変更なども正確に検出できる。また、挿入時にはWHERE NOT EXISTS句を組み合わせることで、同じ入力で何度実行しても、同じ状態が一つだけ作成されるように、これも冪等性を保つことができる。

この方法を採用すれば、特定の時点でのデータをクエリすることが非常に簡単になる。例えば、ある使用履歴データと顧客ディメンションを結合する際に、使用日の範囲が顧客ディメンションの有効期間内にあるものだけを結合することで、その時点での顧客セグメントを正確に取得できる。これにより、レポートは過去のデータに対して常に同じ答えを返し、データの整合性に関する論争を防ぎ、監査もしやすくなる。

これらの習慣は、決して高度な技術を要求するものではない。しかし、今回のキャッシュキーのバグは、まさにこれらの基本的な原則が欠けていたために発生した。特に、各処理段階での行数保存の原則を守っていれば、今回のバグは最初の実行で「出力が入力の0.1%しかない」というエラーを出し、何週間も気づかれないことはなかっただろう。そして、再実行が冪等であれば、そのようなエラーが発生しても、安心して修正して再実行することが可能になる。サイレントなデータ消失は、多くの場合、システムが本来停止すべき時に「止まってはいけない」というプレッシャーから生まれるのだ。

入力と出力の数を常に数え、消失したレコードがないことを確認する。再実行を簡単で退屈なものにし、安心して失敗できるシステムを作る。そして、データを上書きせず、履歴を大切に保管する。これらは「賢い」解決策というよりも、堅牢なシステムを構築するための基本的な「作法」であり、今回のケースのように「賢いつもり」がデータ消失を招くこともあるのだ。

関連コンテンツ

関連IT用語

関連ITニュース