変更はすでにログにある
一言でいうと
CDC(Change Data Capture)は、アプリケーションが電文を送ってくれるのを待たずに、データベースにすでに起きた変更を読み取って、ほかのシステムへ流す連携方式です。最も信頼できる方法は、DBのトランザクションログを読むことで、その代償として、ログの保存・スキーマ変更・順序のような新しい運用上の責任が生まれます。
なぜ必要なのか
これまで、このコースの連携はすべて、送る側が送ってくれる構造でした。勘定系が処理結果をハブに渡し、ハブが情報系と外部システムへ運びます。ところが現場には、このようにつなげないシステムがたくさんあります。ソースを直せないパッケージ、20年前の元帳バッチ、数十個の画面が同じテーブルを直接書き換えるレガシーがそうです。「変更が起きるたびに電文を送ってください」という要求は、そのシステムのすべての書き込み箇所を探して直せという意味で、1か所でも漏らすと、黙ってデータがずれます。
2つ目の理由は二重書き込みです。アプリケーションがDBに書いてからMQにも発行すると、その間にプロセスが死んだ瞬間に、DBにはあってMQにはない変更ができます。2つのリソースを1つのトランザクションにまとめる分散トランザクションは重く、ほとんどのブローカーがサポートしていません。DBへの書き込みだけをトランザクションにし、発行はDBがすでに確定した変更を読み取って行えば、この隙間がなくなります。CDCが、その読み取る側です。
どう動くのか
変更を捉える方法は、大きく3つあります。
| 方式 | 原理 | 代償 |
|---|---|---|
| 照会(ポーリング) | updated_at > 마지막 시각(プレースホルダーは最後の時刻です)で定期的に読む |
削除を見られない。同じ時刻の変更や、遅れてコミットされたトランザクションを見逃す。元のDBに照会の負荷がかかる |
| トリガー | テーブルのトリガーが、変更履歴テーブルに1行ずつ書く | 書き込みごとにトリガーのコストがかかる。元のDBにトリガーを仕込む必要がある |
| ログベース | DBのトランザクションログ(PostgreSQLのWAL、MySQLのbinlog)を読む | ログの保存・権限・バージョン互換を運用する必要がある |
ログベースが最も信頼できる理由は、DBがコミット順にすでに記録したものを読むからです。アプリケーションコードがどんな経路で書いても、削除でも、大量更新でも、すべてログに残ります。
代表的なオープンソースの実装が、Debeziumです。PostgreSQLコネクターは、論理デコーディング(logical decoding)でWALを読みます。出力プラグインには、PostgreSQL 10以降に標準で入っているpgoutputか、Debeziumが管理するdecoderbufsを使い、論理デコーディングを使うには、元のDBのwal_levelがlogicalである必要があります(PostgreSQLのドキュメント)。MySQLコネクターは、binlogを読んで行単位のINSERT・UPDATE・DELETEをイベントにします(Debezium MySQL)。
最初の1回はスナップショット、その後はストリーミングです。ログは永遠に残らないので、コネクターは最初につなぐときにテーブル全体を一貫した時点で読み(スナップショット)、イベントとして出力し、その時点のログ位置から続けて変更を流します。ドキュメントは、スナップショット中に読んだログ位置からストリーミングを始めるので、その間の変更を見逃さないと説明しています。
イベントは変更前と変更後を一緒に載せます。Debeziumの変更イベントの本文には、before(変更前の行)、after(変更後の行)、source(どのDB・テーブル・ログ位置から来たか)、op(演算)、ts_ms(処理時刻)があります。opは、cが作成、uが更新、dが削除、rがスナップショットの読み取り、tがテーブルのトランケートです。PostgreSQLでbeforeに何が入るかは、テーブルのREPLICA IDENTITYが決めます。既定値(DEFAULT)なら、更新・削除のイベントに主キー列の以前の値だけが、FULLならすべての列の以前の値が入ります。「残高がいくらからいくらに変わったか」が必要なら、この設定を最初に確認する必要があります。
レプリケーションスロットはログを握ります。PostgreSQLコネクターは、レプリケーションスロットで、自分がどこまで読んだかをDBに残します。コネクターが止まっても、DBはスロットがまだ読んでいないWALを削除しません。そのため、再起動すると、止まった位置から続けて読みます。裏返して言えば、コネクターが数日間死んでいると、元のDBのディスクがWALでいっぱいになります。CDCをつなげた瞬間に、元のDBの運用に監視項目が1つ増えるのです。MySQL側には、逆方向のリスクがあります。binlogは保存期間が過ぎると削除されるので、コネクターがそれより長く止まると、読んでいた位置が消えて、新しいスナップショットが必要になると、ドキュメントは書いています。
アウトボックスパターンと併用します。テーブルの変更をそのまま流すと、コンシューマーが元のテーブルの構造に縛られます(列名を変えるとコンシューマーが壊れます)。そこで、業務トランザクションの中で、発行するイベントをoutboxテーブルに1行一緒に書き、CDCはそのoutboxテーブルだけを読んで出力します。元のスキーマは隠し、二重書き込みの問題はなくします。Debeziumは、outboxテーブルの行をイベントに変換してくれるOutbox Event Router変換を提供しています。
現場での姿
1つ目は、「変更分だけください」をupdated_atの照会で実装したバッチが、削除を永遠に見逃すことです。解約された口座が、情報系には何か月も生き続けています。2つ目は、CDCは変更を運ぶだけで、意味は運ばないことです。元帳テーブルの行の変更3件(出金の行・入金の行・残高の行)が振込1件だという事実は、ログにはありません。業務イベントが必要なら、アウトボックスで意味を書く必要があります。3つ目は、配信が少なくとも1回だということです。コネクターが再起動すると、すでに送ったイベントがまた届くことがあるので、コンシューマーはモジュール8の冪等処理をそのまま行う必要があります。sourceのログ位置やイベントキーが、その根拠になります。4つ目は、スキーマ変更です。元に列が追加されると、イベントの形が変わります。コンシューマーとの契約を、元のテーブルではなくアウトボックスのイベント形式に置く理由が、これです。
EAIとの関係も整理しておきましょう。CDCは中継層を置き換えません。リクエスト・レスポンスが必要な取引(振込承認、限度額照会)は、引き続き同期中継で行います。CDCは、すでに終わったことを別のシステムが知る必要があるとき(情報系への取り込み、検索インデックス、キャッシュの無効化、通知)に合っています。
まとめとクイズ
このモジュールは、ラボなしで概念を整理します。ポーリング・トリガー・ログベースの違い、スナップショットとストリーミング、変更イベントの形、レプリケーションスロットとログ保存の運用上の責任、アウトボックスを、クイズで確認します。