REST API を書き続けてきたエンジニアが Apache Kafka に触れると、最初に感じる違和感の正体は技術的な難しさではない。リクエスト/レスポンスとは根本的に異なる「出来事を記録する」という設計思想が、既存のメンタルモデルと衝突するためだ。
リクエストとイベントは何が違うのか
REST の世界では、サービス A がサービス B を呼び出し、応答が返るまで処理を止める。この構造は直感的だが、サービス数が増えると依存の連鎖が生まれる。注文サービスが在庫サービスを呼び出し、在庫サービスが通知サービスを呼び出す構成では、通知サービスが過負荷になった瞬間に注文サービスまでタイムアウトが波及する。
イベント駆動(event-driven)の設計では、発生した出来事をログに記録するだけで処理を終える。注文サービスは「注文が入った」というイベントを書き込んで次の処理へ進む。在庫サービスと通知サービスはそれぞれ独立して、自分のペースでそのイベントを読み取る。1 つのサービスが遅延しても、他のサービスには波及しない。
Kafka を構成する 3 つの要素
Kafka を動かすのに最初から必要な概念は 3 つに絞られる。
- プロデューサー(Producer): イベントをトピックに書き込む側。コンシューマーが起動しているかどうかを意識しない
- トピック(Topic): イベントが蓄積されるログ。キューとは異なり、読み取ってもイベントは消えない。複数のコンシューマーが同じイベントを独立して読める
- コンシューマー(Consumer): トピックからイベントを読み取る側。オフセット(offset)という読み取り位置を自分で管理し、自分のペースで処理を進める
トピックをキューと混同しやすい点は注意が必要だ。キューはメッセージを取り出すと消えるが、Kafka のトピックは追記専用のログなので、設定した保持期間(デフォルト 7 日)が経過するまでイベントが残り続ける。同じイベントを複数の下流サービスが個別に処理できるのはこの仕組みによる。
実際にプロデューサーを書くと、その軽量さがよくわかる。Python クライアントの confluent-kafka を使った最小構成は以下のとおりだ。
from confluent_kafka import Producer
producer = Producer({'bootstrap.servers': 'localhost:9092'})
def delivery_report(err, msg):
if err:
print(f'Delivery failed: {err}')
else:
print(f'Event delivered to {msg.topic()}[{msg.partition()}]')
producer.produce(
'order-events',
key='order-123',
value='{"status": "placed", "item": "widget"}',
callback=delivery_report
)
producer.flush()コンシューマーが起動しているかどうかに関係なく、プロデューサーはイベントをトピックに書き込んで即座に処理を終える。ハンドシェイクも応答待ちも発生しない。
ローカル環境で 10 分以内に動かせる
Kafka は大規模クラスターが前提という印象を持つエンジニアも多いが、docker-compose で Zookeeper(Kafka が内部的に使うクラスター管理コンポーネント)と Kafka ブローカー(メッセージを受け取り保管するサーバー)を起動するだけでローカル検証が始められる。クラウドアカウントもインフラ担当への依頼も不要だ。
なお、Kafka 3.3 以降では KRaft モード(Zookeeper を使わずに Kafka 単体でクラスター管理を行う方式)が安定版となっており、2024 年時点ではこちらを推奨する構成も増えている。ローカルで試す際は confluent 社が提供する docker-compose サンプルか、Redpanda(Kafka 互換のより軽量な実装)も選択肢になる。
基本的なプロデューサーとコンシューマーを動かし、オフセットが進んでいく様子をコンソールで確認する。この小さなループが、分散システムの理論記事を何本読むよりも設計思想の理解を深める。
REST との使い分けが実務の判断軸になる
「Kafka を導入すれば REST は不要」という話ではない。同期的な応答が必要な場面、たとえば認証トークンの検証やリアルタイムな在庫照会には REST または gRPC が適している。一方、注文確定後の複数下流サービスへの通知、ログ収集、データパイプラインなど、処理の結果を複数のコンシューマーに届けたいケースでは Kafka のアーキテクチャが力を発揮する。
スキーマの変更時の後方互換性確保、パーティション(トピックを分割して並列処理する単位)をまたいだ順序保証、コンシューマー障害時の再処理設計といった課題は、基本モデルを動かしてから初めて現れる。それらは「動いているシステムが成長する過程で直面する問題」であり、最初の一歩を踏み出す障壁ではない。
イベント駆動は設計の選択肢の 1 つに過ぎないが、サービス間の依存を切り離す手段として REST 一択だったコードベースに新しい構造をもたらす。まずローカルで producer と consumer を動かしてみると、その感覚がつかめるはずだ。