DebeziumとApache KafkaによるChange Data Capture(CDC)実践ガイド:PostgreSQLからElasticsearchへのリアルタイムデータ同期

背景:Dual-writeと定期実行バッチが破綻を招く理由

毎分2,000件の注文が発生するECサイトを運用している場面を想像してみてください。PostgreSQLは信頼できる単一の情報源(Source of Truth)として機能し、ACID特性に準拠して各トランザクションを確実に記録します。同時にプロダクトチームは、商品検索や注文履歴の検索をレスポンス50ms未満でユーザーに提供するため、Elasticsearchを必要としています。

この課題に対して真っ先に思い浮かびがちなのがDual-write(二重書き込み)です。アプリケーションがPostgreSQLへの書き込みを完了した後、続けてElasticsearchのAPIを呼び出して反映する構成です。一見シンプルに思えますが、ネットワーク障害やElasticsearchの3秒のタイムアウトが発生しただけで、2つのデータストア間に即座にデータの不整合が生じます。これをカバーしようとすると、リトライや補償トランザクションの処理が複雑化し、コードベースが肥大化してしまいます。

次に検討されるのが、5分ごとに定期実行するcronバッチです。しかし、テーブルのデータ量が数千万行規模に達すると、SELECT ... WHERE updated_at > ...といったクエリがデータベースのIOPSを枯渇させます。さらに、検索用データには常に最低5分の遅延がつきまといます。

こうした課題を根本から解決するのがChange Data Capture(CDC)アーキテクチャです。データベースに負荷の高い検索クエリを強いるのではなく、CDCはPostgreSQLのWrite-Ahead Log(WAL)を直接読み取ります。追加・更新・削除(INSERT、UPDATE、DELETE)の各操作を、データベースのCPU負荷をほぼゼロに抑えながらリアルタイムに検知します。

Debezium、Kafka、Kafka Connectの3つを組み合わせることで、このパイプラインをスムーズに処理できます。DebeziumがWALをイベントストリームとして読み出し、Kafkaが高信頼な分散メッセージングを担い、Kafka Connectが数百ミリ秒の低遅延でElasticsearchへデータを転送します。

Docker Composeによる環境構築

各ツールをローカル環境に個別にインストールする必要はありません。Docker Composeを使用すれば、わずか数分で一連のクラスタ環境を立ち上げることができます。

1. docker-compose.ymlの準備

作業ディレクトリを作成し、以下の内容でdocker-compose.ymlを保存します。

version: '3.8'
services:
  postgres:
    image: quay.io/debezium/postgres:15-alpine
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=inventory

  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  kafka:
    image: confluentinc/cp-kafka:7.5.0
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

  kafka-connect:
    image: debezium/connect:2.4
    depends_on:
      - kafka
      - postgres
    ports:
      - "8083:8083"
    environment:
      BOOTSTRAP_SERVERS: kafka:9092
      GROUP_ID: 1
      CONFIG_STORAGE_TOPIC: my_connect_configs
      OFFSET_STORAGE_TOPIC: my_connect_offsets
      STATUS_STORAGE_TOPIC: my_connect_statuses

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.10.2
    ports:
      - "9200:9200"
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false

2. クラスタサービスの起動

バックグラウンドでコンテナを起動します:

docker compose up -d

Kafka Connectが各種プラグインを読み込み、ポート8083でREST APIが利用可能になるまで30〜45秒ほど待ちます。

CDCパイプラインの設定

Debeziumがトランザクションログを抽出できるようにするには、PostgreSQLでwal_level = logicalが有効になっている必要があります。今回使用するquay.io/debezium/postgresイメージではあらかじめ設定されていますが、既存のサーバーやAWS RDSなどのPostgreSQLインスタンスを使用する場合は設定値を確認してください。

1. PostgreSQLでの初期データ作成

顧客テーブルcustomersを作成し、サンプルデータを2件投入します:

docker compose exec -it postgres psql -U postgres -d inventory -c "
CREATE TABLE customers (
    id SERIAL PRIMARY KEY,
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    email VARCHAR(255) UNIQUE,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO customers (first_name, last_name, email) VALUES
('Nguyen', 'An', '[email protected]'),
('Tran', 'Binh', '[email protected]');
"

2. Debezium PostgreSQL Source Connectorの登録

ポート8083のKafka Connect REST API経由でConnectorの設定を送信します:

curl -i -X POST -H "Accept:application/json" -H "Content-Type:application/json" \
  http://localhost:8083/connectors/ -d '{
  "name": "postgres-source-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "tasks.max": "1",
    "plugin.name": "pgoutput",
    "database.hostname": "postgres",
    "database.port": "5432",
    "database.user": "postgres",
    "database.password": "postgres",
    "database.dbname": "inventory",
    "topic.prefix": "cdc",
    "table.include.list": "public.customers"
  }
}'

リクエストを受け取ると、Debeziumはまず既存データのスナップショットを取得します。それ以降の変更データは、自動的にKafkaトピックcdc.public.customersへパブリッシュされます。

3. Unwrap SMTによるElasticsearchへのデータ転送

デフォルト状態のDebeziumメッセージには、スキーマ情報や変更前後の状態(before/after)などを含む複雑なエンベロープが含まれています。Elasticsearch側では最新のペイロードのみが必要となるため、ExtractNewRecordState(Unwrap SMT)変換プラグインを使い、インデックス登録前に純粋なデータオブジェクトのみを抽出します:

curl -i -X POST -H "Accept:application/json" -H "Content-Type:application/json" \
  http://localhost:8083/connectors/ -d '{
  "name": "elastic-sink-connector",
  "config": {
    "connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector",
    "tasks.max": "1",
    "topics": "cdc.public.customers",
    "connection.url": "http://elasticsearch:9200",
    "type.name": "_doc",
    "key.ignore": "false",
    "schema.ignore": "true",
    "transforms": "unwrap",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.drop.tombstones": "false"
  }
}'

ConfluentのSinkプラグインを使用しない場合は、PythonやGoなどでKafkaメッセージをコンシュームしてElasticsearchの_bulk APIを呼び出す軽量ワーカーを作成して代用することも可能です。

動作検証と運用ポイント

パイプライン全体の接続が完了しました。実際の同期遅延を検証してみましょう。

1. Connectorの稼働状況を確認

両方のConnectorが正常に動作しているか確認します:

curl -s http://localhost:8083/connectors/postgres-source-connector/status | jq .
curl -s http://localhost:8083/connectors/elastic-sink-connector/status | jq .

stateフィールドの値がRUNNINGであることを確認してください。もしFAILEDになっている場合は、docker compose logs kafka-connectでコンテナログを確認し、認証エラーやプラグインの不足がないかトラブルシューティングを行います。

2. リアルタイムデータ同期の検証

PostgreSQLに新しい顧客レコードを挿入します:

docker compose exec -it postgres psql -U postgres -d inventory -c "
INSERT INTO customers (first_name, last_name, email) VALUES ('Le', 'Cuong', '[email protected]');
"

ほぼ一瞬(通常150ms〜300ms程度の遅延)で、Elasticsearch側で検索クエリを実行してみます:

curl -s http://localhost:9200/cdc.public.customers/_search?q=last_name:Cuong | jq .hits.hits

アプリケーション側に同期コードを一切追加することなく、新規顧客「Le Cuong」のレコードが検索インデックスに反映されていることが確認できます。

3. 本番運用で不可欠な3つの重要メトリクス

CDCを本番環境で運用する際は、以下のリスクを継続的に監視する必要があります:

  • PostgreSQLのレプリケーションスロット遅延(Replication Slot Lag):最も注意すべき落とし穴です。Kafka Connectクラスタが不意に停止した場合、PostgreSQLは未読み出しのWALファイルを蓄積し続けます。これにより、わずか数時間でディスク容量が20GBから200GBへと急増し、プライマリデータベースが停止する危険があります。pg_replication_slotsを監視し、WALの保持容量に対するアラートを必ず設定してください。
  • Kafkaのコンシューマーラグ(Consumer Lag):生成された最新オフセットと、Elasticsearch Sinkが処理したオフセットの差分を計測します。ラグが増加し続けている場合、Elasticsearchへの書き込みがボトルネックになっています。Connectorのタスク数を増やすか、Elasticsearch側のリフレッシュ間隔(refresh interval)のチューニングを検討してください。
  • Kafka ConnectのJMXメトリクス監視:Prometheus JMX Exporterを組み込み、毎秒のレコード読み取りレート(poll rate)やオフセットコミット時間(commit time)を監視します。ユーザーから問い合わせが入る前に、ボトルネックを早期検知できる体制を整えましょう。
Share: