午前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アラートに悩まされることもなくなりました。

