なぜ私たちは Debezium を Rust で書き直さなかったのか
ここ数年、コミュニティやフォーラム、さらにはお客様からも、同じ質問を何度もいただきました。
「システムが Rust で書かれているのなら、なぜ Debezium を Rust で書き直さないのですか?」
RisingWaveは、リアルタイムイベントデータの処理・分析・管理を、最もシンプルかつコスト効率の高い形で実現するために設計されたリアルタイムイベントストリーミングプラットフォームです。さらに、Apache Iceberg™のオープンテーブル形式をネイティブにサポートしており、PostgreSQL互換のSQLインターフェースと、DataFrameスタイルのPythonインターフェースの両方を備えています。
RisingWaveは毎秒数百万件のイベントを取り込み、ライブストリームと履歴データを継続的に結合・分析し、低レイテンシでアドホッククエリに応答しながら、Apache Iceberg™やその他の下流システムへ鮮度と整合性のある結果を永続化します。
実際、オープンソースのプロジェクトの中にはこれに挑戦したものも存在します。Rust で Change Data Capture (CDC) エンジンをゼロから実装しようとするものや、Postgres WAL や MySQL binlog を直接パースしようとするものです。
私たちが CDC モジュールの設計を始めた 3 年前、このアイデアを真剣に検討し、いくつもの実験を行いました。しかし複数回の試行(そして失敗)の末、最終的に Debezium を Rust で書き直すことはしないと決断しました。その代わりに、Debezium Embedded Engine を基盤とし、深いカスタマイズを加える道を選びました。
その理由は以下の通りです。
なぜ Rust で書き直さないのか
まず最初に認めておくべきなのは、Rust が性能、安全性、並行制御の面で確かな利点を提供するということです。実際、私たち自身のコアとなるコンピュートエンジンやストレージエンジンを Rust で実装したのも、まさにその理由からです。
しかし CDC はエコシステムへの依存度が極めて高い領域であり、Debezium はすでに業界におけるデファクトスタンダード となっています。Postgres、MySQL、SQL Server、さらにはあまり一般的でないデータベースに至るまで、Debezium には成熟したコネクタ、よく検証されたログパーサー、そして活発なコミュニティがすでに存在します。
もし私たちが Rust で書き直すとしたら、それは次のような意味を持ちます。
-
プロトコルのパースをゼロから実装する必要がある
データベースごとにログフォーマットは異なります。Postgres WAL と MySQL binlog はそれぞれ独自の構造やフィールドエンコーディング、エッジケースを持っており、広範な適応とデバッグが求められます。Debezium がすでに解決してきたパース上の落とし穴を、私たちは再び自力で解決しなければなりません。 -
非常に長い互換性テストサイクル
データベースのバージョン差、設定、文字セット、タイムゾーン、DDL の挙動など、すべてが CDC のパース精度に影響します。Debezium のコミュニティではほぼ毎日のようにバグ修正の PR が投稿されています。もし私たちが別の Rust 実装を維持するなら、その長期的な保守コストをすべて自分たちで背負う必要が出てきます。 -
データベースの新機能へのキャッチアップが遅れる
データベースは毎年のように新しい機能を追加します。Postgres の論理デコードプラグインの更新や、MySQL の GTID の挙動変更などです。Debezium のコミュニティはこれらの変更を積極的に追跡し、適応しています。もし書き直してしまえば、それらに追随するためだけに専任チームを割かねばなりません。
つまり、理論上パフォーマンスで Debezium を上回れる可能性があったとしても、それを捨ててしまうことは、容易に「10 年規模のエンジニアリングプロジェクト」に取り組むことを意味します。
RisingWave のコア機能を素早く反復開発する必要があるこのフェーズにおいては、それはまったく合理的なトレードオフではありません。
なぜ Debezium Server ではないのか
もし書き直さないのであれば、Debezium Server ではなく、なぜ Embedded Engine を使うのか?という疑問が出てきます。
両者の主な違いは次の通りです。
- Debezium Server はスタンドアロンのプロセスです。ソースデータベースと下流のターゲット(Kafka、Pulsar など)を設定ファイルで指定すると、それが常時稼働します。いわば汎用的な CDC 転送ツールです。
- Embedded Engine は Java ライブラリであり、自分のプロセスに直接組み込んで利用します。CDC イベントを API を通じて受け取り、それをどのように消費・キャッシュ・転送・エラーから回復するかを制御できます。
私たちが Embedded Engine を選んだのは、主に以下の 3 つの理由によります。
-
きめ細かな制御
Server モードはブラックボックスであり、データ処理、メモリ管理、バックプレッシャー、エラー回復といったデフォルトの挙動を変更するにはハックが必要です。Embedded であれば、コード内でエンジンを直接制御できるため、任意のポイントにフックを挿入できます。 -
コンピュートエンジンとの深い統合
RisingWave のコンピュートノードは独自のスケジューリング、チェックポイント、DAG トポロジーを持っています。CDC データはこれらの仕組みに密接に統合される必要があります。Embedded モードであれば CDC の入口でロジックを挿入でき、Server モードではそれが不可能です。 -
運用のシンプルさ
Server モードでは、Debezium プロセスを別途デプロイ・監視・アップグレードし、そのフェイルオーバーや設定管理を行う必要があります。Embedded モードでは、CDC が同じデプロイメント内で稼働し、同じ監視・ログ・アラートの仕組みを共有できるのです。
Debezium Embedded Engine に対して行った主要な拡張
Debezium を使い続けるといっても、そのまま利用しているわけではありません。リアルタイムコンピュートエンジンに完全に適合させるため、Server モードでは不可能な方法で Embedded Engine に深いカスタマイズを施しました。
私たちは Debezium Embedded Engine をカスタマイズし、RisingWave におけるストリーミング取り込みに活用しています。また Apache Iceberg へのデータ書き込み時には Apache DataFusion を使ってコンパクションを実行しています。詳細はこちら: https://risingwave.com/blog/postgres-cdc-iceberg-production-lessons/
1. ロックフリー・スナップショット + 並列インクリメンタル結合
Debezium のデフォルトのスナップショットはテーブルにロックをかけ、ソース DB の操作に影響を与える可能性があります。私たちのアプローチは、まずスナップショット境界(高水位 LSN や binlog ポジション)を記録し、その後スナップショットと WAL/binlog イベントを同時にストリーミングするというものです。下流では重複を排除し必要に応じて上書きすることで、一貫性を保証しつつ、長時間のロックを避け、より迅速にリアルタイム同期に移行できます。
2. 並列バックフィル
標準の Debezium における初期スナップショットは、単一スレッドで単一テーブルを処理するため、非常に遅くなる可能性があります。バージョン 2.2 以降、Debezium は snapshot.max.threads
設定を導入し、複数テーブルを並列にスナップショットできるようになりました。— しかしこれは単一テーブルのバックフィルを高速化するものではありません。私たちの実践では、大規模な単一テーブルに対して主キー範囲を分割し、複数スレッドで並列バックフィルを実行しました。またソース DB の負荷に基づいてスレッド数を動的に調整し、ソースシステムへの過剰な負担を避けつつ大幅な性能向上を実現しました。
3. マルチクラウド対応のスキーマ履歴
Debezium の標準スキーマ履歴ストレージは Kafka、ローカルファイル、S3、Azure Blob などをサポートしていますが、必要なものをすべてカバーしているわけではありません(例: GCS や Alibaba OSS は非対応)。RisingWave では、S3、GCS、Azure Blob、OSS などをサポートするプラグイン型スキーマ履歴レイヤーを実装しました。これによりマルチクラウド環境でのシームレスなデプロイや、移行中でも一貫したスキーマ履歴の維持が可能となります。
4. スキーマ履歴のメモリおよびバックプレッシャー制御
Debezium の標準スキーマ履歴では、履歴ファイル全体が一度にメモリへロードされます。特に DDL 変更が頻繁に発生する環境では、時間の経過とともに過剰なメモリ使用や、極端な場合は OOM(メモリ不足)を引き起こします。
私たちはストレージを改修し、履歴を時間またはサイズごとのセグメントに分割し、現在の処理に必要な部分のみをロードするようにしました。これによりメモリ使用量のピークを抑制し、大規模・長時間稼働・マルチクラウド環境での安定性を改善しました。
5. TOAST カラム値の補完
PostgreSQL で非 TOAST カラムが更新され、TOAST カラムが変更されなかった場合、Debezium はその TOAST カラムに完全な値ではなく、プレースホルダー値(例: __debezium_unavailable_value
)を送信します。私たちの CDC 取り込み処理では、履歴値をクエリしてプレースホルダーを実データに置き換え、新しいイベントとマージしています。これにより下流システムが常に完全かつ利用可能なレコードを受け取れるようにしています。
6. PostgreSQL スキーマ変更の検知
標準の Debezium ではスキーマ進化の処理が限定的であり、特に Embedded Engine モードではカラム追加や削除を検知するために定期的なメタデータポーリングに依存することが多くあります。この手法は高いレイテンシーを招くだけでなく、上流データベースへの負荷も増大させます。
私たちは PostgreSQL のレプリケーションプロトコルに含まれる R
(Relation)メッセージを活用してスキーマ変更を直接検知しています。論理レプリケーションストリームに R
メッセージが現れた場合、それは対応するテーブルのスキーマが変更されたこと(例: ADD COLUMN
や DROP COLUMN
)を意味します。このイベントを受け取ると RisingWave は直ちにスキーマ更新ロジックを起動し、カラム追加・削除を自動的に完了し、更新されたスキーマメタデータを下流へ同期します。これにより手動介入の必要がなくなり、データ不整合のリスクを減らせます。
これらの改修が可能なのは、私たちが Embedded Engine モードで動作させているからです。Server モードでは重要なポイントでデータを傍受・変換する手段がほとんどありません。
結論
Debezium を Rust で書き直すという発想は魅力的に聞こえるかもしれません。しかし実際には、そのエコシステムの成熟度、安定性、バージョン互換性、テストの深さは、短期間で追いつけるものではありません。私たちに必要なのは、RisingWave にとって安定的で制御可能かつ深く統合された CDC のエントリポイントであり、数年単位に及ぶ再構築プロジェクトではないのです。
Debezium Embedded Engine を選択することで、成熟したエコシステムの恩恵を享受しつつ、自分たちのワークロードにとって最も重要な部分を最適化できます。これはエンジニアリング効率、安定性、保守性の間で取り得る最良のバランスだと私たちは考えています。
Discussion