リクエスト/レスポンス型のシステムでは、図の矢印がそのまま呼び出しグラフになります。イベント駆動のシステムではそうなりません。プロデューサーはイベントをトピックに書き込むだけで、誰がそれを読むのかを知りません。これを素朴に描いて、すべてのサービスを大きな Kafka の箱 1 つにつないでしまうと、必要な情報がすべて図から隠れてしまいます。どんなイベントがあり、誰がそれに反応し、どの順序で処理され、コンシューマーが失敗したら何が起きるのか、という情報です。
このガイドでは、フローを読みやすく保つイベント駆動アーキテクチャ図の記法を紹介し、Kafka 上の注文処理の例を順に見ていきます。キー、コンシューマーグループ、outbox、デッドレターの経路といった、描き忘れがちな細部も扱います。
図で示すべきこと
イベント駆動の図は、別のドキュメントがなくても次の問いに答えられる必要があります。
- どんなイベントがあるか?
OrderPlacedやPaymentFailedのように、過去形で命名します。 - それぞれ誰が発行するか? イベントタイプごとにオーナーがちょうど 1 つ、というのが健全なデフォルトです。
- 誰が、どのコンシューマーグループとして消費するか? Kafka ではすべてのコンシューマーグループがすべてのイベントを受け取るので、グループごとに別々の読み手になります。
- 何が順序を決めるか? Kafka が順序を保つのはパーティション内だけで、どのパーティションに入るかはキーで決まります。
- 失敗したらどうなるか? リトライ、デッドレタートピック、そして誰がリプレイするのか。
うまく機能する記法
| 要素 | 描き方 |
|---|---|
| プロデューサー | サービスのノードからトピックへ破線のエッジを引き、イベントタイプをラベルにする |
| トピック | 独立したノードにし、クラスター上と同じ名前(orders.v1)を付け、ラベルにキーを書く |
| コンシューマーグループ | トピックからコンシューマーへの破線のエッジに、グループ ID をラベルとして付ける |
| イベントとコマンド | イベントは過去形(OrderPlaced)、コマンドは命令形(ReserveStock) |
| デッドレタートピック | 処理をあきらめたコンシューマーが書き込む、別のトピックノード |
| コネクター | トピックと外部ストアの間に置くノード(例: データウェアハウス向けのシンク) |
| ブローカークラスター | トピックを囲むフレーム。ブローカーとパーティションは運用図に描く |
実線のエッジは、決済プロバイダーへの HTTP リクエストのような同期呼び出しだけに使います。そうすれば、フローのどの部分が同じリクエストの中で起き、どの部分が後から起きるのかが一目でわかります。
例: Kafka 上の注文イベント
あるオンラインストアでは、注文をイベントで処理しています。
- orders サービスが、注文と outbox の行を同じデータベーストランザクションで書き込みます。
- outbox relay(Kafka Connect 上の Debezium)がデータベースのログ経由で outbox を読み取り、
OrderPlacedをorder_idをキーにしてorders.v1に発行します。 - 3 つのコンシューマーグループが
orders.v1を読みます。payments はカードに課金し、inventory は在庫を引き当て、warehouse sink はすべてのイベントをデータウェアハウスに取り込みます。 - payments は
PaymentSucceededまたはPaymentFailedを、同じくorder_idをキーにしてpayments.v1に発行し、orders サービスがそれを消費して注文ステータスを更新します。 - inventory は 3 回試してもメッセージを処理できない場合、そのメッセージを
inventory.dlqに書き込んで次へ進みます。
フロー全体を 1 つのキャンバスに描くと、次のようになります。
ダイアグラムの内容
- Orders
- Orders DB (orders + outbox)
- Outbox relay
- Kafka cluster
- payments.v1 · key order_id
- orders.v1 · key order_id
- inventory.dlq
- Payments
- Warehouse sink
- Inventory
- Data warehouse
- order + outbox row, one transaction
- reads WAL
- OrderPlaced
- group: payments
- group: warehouse-sink
- group: inventory
- PaymentSucceeded/Failed
- group: orders
- load
- after 3 failed attempts
この図で何が一目瞭然になるかに注目してください。payments を呼び出している者はいません。4 つ目のコンシューマーグループを追加しても、上流は何も変わりません。そして、どちらのトピックも order_id をキーにしているので、1 つの注文のイベントは各トピックの中で順序が保たれます(Kafka はトピックをまたいだ順序を保証しません)。
イベント駆動の図のための 5 つのルール
1. トピックは矢印ではなくノードにする
orders から payments へ「Kafka」とラベルを付けた矢印を引くと、存在しない呼び出しがあるように見えてしまいます。トピックは両者の間にノードとして描きましょう。トピックは、名前、オーナー、保持期間、スキーマを持つ実体です。
2. 矢印はデータの流れに合わせる
プロデューサーからトピックへ、トピックからコンシューマーへと、データが動く方向に描きます。Kafka のコンシューマーはプル型ですが、コンシューマーからトピックへ矢印が向いた図は、ほとんどの人にとって逆向きに読めてしまいます。プルのセマンティクスが議論に関わるなら、メモを添えましょう。
3. イベントとコンシューマーグループにラベルを付ける
プロデューサー側のエッジにはイベントタイプを、各コンシューマー側のエッジにはコンシューマーグループを書きます。「OrderPlaced をほかに誰が読んでいるのか」と聞かれたら、答えはトピックから出ていくラベル付きエッジの一覧です。
4. 順序が重要ならキーを示す
Kafka はパーティション内の順序を保証し、同じキーのイベントは同じパーティションに入ります。コンシューマーが OrderCancelled より先に OrderPlaced を受け取ることを前提にしているなら、トピックにキーを書いておきます(orders.v1 · key order_id)。ラベルにキーのないトピックは、まさにその点を質問されることになります。
5. 異常系の経路も描く
Kafka のコンシューマーグループには、組み込みのデッドレターキューがありません。何回リトライするか、処理できないメッセージをどこに置くかは、コンシューマーのコードやフレームワークが決めます(Kafka Connect のシンクコネクターには、デッドレタートピックの設定があります)。その判断を図に描きましょう。コンシューマーからエッジでつながるデッドレタートピックと、誰がそれを監視し、メッセージをどうリプレイするかを書いたメモがあれば、インシデントを未然に防げます。
近道ではなく outbox を描く
サービスがデータベースへの書き込みと Kafka への発行を別々の 2 ステップで行うと、その間にクラッシュした場合、イベントが失われるか、コミットされなかった変更のイベントが発行されてしまいます。Outbox パターンはこの二重書き込みを避けます。サービスは同じトランザクションの中でイベントを outbox テーブルに書き込み、リレーがそれを発行します。
図には実際の経路、つまりサービス → データベース → リレー → トピックを描きます。サービスからトピックへの直接の矢印は、別の、より信頼性の低いシステムを表してしまい、人はそれをもとに判断を下します。
コレオグラフィかオーケストレーションか
この例ではコレオグラフィを使っています。各サービスは互いのイベントに反応し、フロー全体を取り仕切る者はいません。代わりにオーケストレーター、つまり ReserveStock のようなコマンドを送って返信を待つワークフローエンジンを使う場合は、オーケストレーターを中央のサービスとして描き、そのエッジにはコマンドとしてラベルを付けます。どちらの場合も、メインフローの手順の番号をエッジに振っておけば、読み手はサーガを順番に追えます。
論理ビューにクラスターを持ち込まない
ブローカー、パーティション、レプリケーションファクター、コントローラーノードは、キャパシティと運用には重要ですが、フローの理解には関係ありません。これらは別の運用図に描きます。論理ビューでは、トピックを「Kafka cluster」とラベル付けしたフレームで囲めば十分です。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- サービスは C4 モデルパックの コンテナ ノードとして、orders のデータベースはデータパックの Postgres として追加します。
- 基本パックの フレーム を追加して
Kafka clusterというラベルを付け、その中にデータパックの トピック ノードを置きます。 - outbox relay と warehouse sink は、「技術」フィールドに
Kafka Connectと入力したコンテナとして追加し、データパックの データウェアハウス ノードも追加します。 - ポート同士を接続し、イベントのエッジを破線にして、イベントタイプとコンシューマーグループのラベルを付けます。
- レビューではプレゼンテーションモードで開き、ライブリンクを共有するか、SVG や PNG で書き出します。
この例で使うノードは 11 個で、Free プランの上限である 15 ノードに収まります。ほかのストリーミング構成は /examples/streaming-data-platform を、似たようなストアの同期処理の側についてはマイクロサービスアーキテクチャ図のガイドを参照してください。
よくある質問
パーティションは描くべきですか? 論理ビューには描きません。重要ならパーティション数をトピックのラベルに書き、パーティションとブローカーは運用図に描きます。
図の上では、キューとトピックはどう違いますか? キューでは各メッセージが 1 つのコンシューマーに届き、Kafka のトピックではコンシューマーグループごとにすべてのメッセージが届きます。SQS や RabbitMQ のようなキューにはキューノードを使い、どちらのセマンティクスを前提にしているかを明記します。
イベントのスキーマはどこに置きますか? メモかトピックのラベルに置き、スキーマレジストリか、スキーマを管理するリポジトリへのリンクを添えます。図で示すべきなのは、契約が存在することと、それがどこにあるかです。
リプレイはどう示しますか?
トピックに保持期間を書き(例: retention 7d)、コンシューマーグループがオフセットをどうリセットするかをメモに書きます。リプレイは設計上の機能なので、目に見えるようにしましょう。
イベントごとに図が必要ですか? いいえ。業務フローごとに 1 つの図にするとうまくいきます。イベントはエッジに列挙し、全体の一覧はイベントカタログで管理します。