Docker Composeを使ったApache Kafka(KRaftモード)とKafka UIの構築手順

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

午前2時15分の障害発生

Prometheusのアラート通知が鳴り響きました。ステージング環境のKafkaクラスタで広範囲の接続障害が発生。注文処理やメール通知を行う全Workerプロセスが一斉にハングアップし、45,000件以上のメッセージがキューに滞留してしまいました。

急いでラップトップを開き、コンテナの状態を確認するコマンドを実行しました。

docker compose ps
docker compose logs --tail=100 zookeeper

ZookeeperコンテナはOOMKilled (Exit Code 137)というエラーコードを出して停止していました。Quorum(定足数)を喪失したことでメタデータの同期が途絶え、クラスタ全体がスプリットブレイン状態に陥っていました。BrokerはパーティションのLeaderを選出できず、クライアントからの接続をすべて拒否。ステージング用VPSの4GBのRAMを3ノード構成のZookeeperが食いつぶしたせいで、徹夜での対応を余儀なくされました。

Zookeeperという重荷

Kafka 2.8以前(正式に安定版となったのは3.3以降)では、メタデータの保持、Controllerの選出、トピック設定の管理にZookeeperが不可欠でした。しかし、この2層構造には主に3つの大きな課題がありました。

  • リソースの浪費: 本番環境で最小構成のクラスタを運用するだけでも、3ノードのZookeeperと3ノードのKafka Brokerが必要になります。ハートビートを維持するだけでも、Zookeeperだけで2GB〜4GBのRAMを消費してしまいます。
  • スケール時のボトルネック: パーティション数が100,000を超えるとメタデータが肥大化します。Zookeeperから大量の状態(State)を同期する必要があるため、Broker再起動時のリカバリ時間が2分から10分に及ぶこともあります。
  • 運用の複雑さ: 2つの異なる分散システムを同時に管理しなければなりません。ネットワークポート、セキュリティ設定ファイル(SASL/TLS)、ログ出力元がそれぞれ2重に存在することになります。

解決策の比較・検討

インフラの最適化にあたり、私たちのチームでは次の3つのアプローチを検討しました。

1. Zookeeperへのメモリ増設とJVMチューニング

ノードごとの-Xmxを1GBから4GBに引き上げ、スナップショットパラメータを調整する方法です。しかし、これは対症療法に過ぎず、肥大化したアーキテクチャのままインフラコストだけが増加してしまいます。

2. RabbitMQやRedis Streamsへの移行

小規模なサービスには適していますが、今回のシステムでは長期的なイベントログ保存(30日間の保持期間)、障害発生時のメッセージ再送(リプレイ)、20,000 msg/sのスループットが求められるため、RabbitMQでは要件を十分に満たせませんでした。

3. Kafka KRaft(Kafka Raft Metadata)への移行

これが最も抜本的で理想的なアプローチです。KRaftはKafka Broker内部に直接Raft合意アルゴリズムを組み込みます。メタデータは内部トピック@metadataに記録されるため、Controllerのフェイルオーバー時間は30秒から500ミリ秒未満に短縮され、メモリ消費量も約50%削減できます。

Docker ComposeによるKafka KRaft + Kafka UIの構築

以下は、ローカルまたはステージング環境での検証に適した、単一ノード構成のKafka KRaft(BrokerとControllerを兼任)および管理UIであるKafka UIの設定例です。

ステップ1: docker-compose.ymlの準備

作業用ディレクトリを作成して移動します。

mkdir -p ~/kafka-kraft-stack && cd ~/kafka-kraft-stack
nano docker-compose.yml

以下の設定内容をファイルに貼り付けます。

services:
  kafka:
    image: apache/kafka:3.7.0
    container_name: kafka-kraft
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_CLUSTER_ID: "4L622nShTUiBenVKBpahAg"
      KAFKA_PROCESS_ROLES: "broker,controller"
      
      # ホストマシン用リスナー(9092)およびコンテナ間内部通信用リスナー(29092, 9093)の設定
      KAFKA_LISTENERS: "PLAINTEXT://:9092,CONTROLLER://:9093,PLAINTEXT_INTERNAL://:29092"
      KAFKA_ADVERTISED_LISTENERS: "PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://kafka:29092"
      KAFKA_CONTROLLER_LISTENER_NAMES: "CONTROLLER"
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT"
      KAFKA_CONTROLLER_QUORUM_VOTERS: "1@kafka:9093"
      
      # 1ノードクラスタ用のレプリケーション設定
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_LOG_DIRS: "/tmp/kraft-combined-logs"
    volumes:
      - kafka_data:/tmp/kraft-combined-logs
    networks:
      - kafka-net

  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name: kafka-ui
    ports:
      - "8080:8080"
    environment:
      KAFKA_CLUSTERS_0_NAME: "kraft-local-cluster"
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: "kafka:29092"
      DYNAMIC_CONFIG_ENABLED: "true"
    depends_on:
      - kafka
    networks:
      - kafka-net

volumes:
  kafka_data:
    driver: local

networks:
  kafka-net:
    driver: bridge

ステップ2: サービスの起動

Docker Compose v2を使用してコンテナを起動します。

docker compose up -d

Kafkaの起動ログを確認します。

docker compose logs -f kafka

ログに[KafkaRaftServer id=1] Kafka Server startedと出力されれば、Brokerの準備は完了です。起動にかかる時間はわずか3〜5秒程度です。

ステップ3: メッセージの送受信テスト(Produce / Consume)

テスト用にパーティション数3のorder-eventsというトピックを作成します。

docker compose exec -it kafka /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server localhost:9092 \
  --create \
  --topic order-events \
  --partitions 3 \
  --replication-factor 1

別のターミナルタブを開き、メッセージを購読(Consume)します。

docker compose exec -it kafka /opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server localhost:9092 \
  --topic order-events \
  --from-beginning

さらにもう1つのターミナルタブを開いて、メッセージを送信(Produce)します。

docker compose exec -it kafka /opt/kafka/bin/kafka-console-producer.sh \
  --bootstrap-server localhost:9092 \
  --topic order-events

> {"order_id": 1001, "status": "PAID", "amount": 250000}
> {"order_id": 1002, "status": "PENDING", "amount": 120000}

Enterキーを押すと、Consumer側のターミナルに即座にJSONデータが表示されます。

ステップ4: Kafka UIによるGUI管理

ブラウザでhttp://localhost:8080にアクセスします。Kafka UIでは以下の操作が可能です。

  • ControllerおよびBroker ID 1の稼働ステータス確認
  • トピックの新規作成、パーティション数の変更、保持期間(Retention Time)の直接変更
  • パーティションごとのメッセージペイロードの確認や、Workerのボトルネック検知に役立つConsumer Group Lagのモニタリング

Zookeeperを廃止してKRaftへ移行して以来、ステージング環境はわずか約700MBのメモリで安定稼働するようになり、深夜のOOMアラートに悩まされることもなくなりました。

Share: