10日で覚えるSystem DesignDay 5: メッセージキューと非同期処理

Day 5: メッセージキューと非同期処理

今日学ぶこと

  • 同期処理と非同期処理の違い
  • メッセージキューの基本概念(Producer、Consumer、Topic、Partition)
  • Kafka vs RabbitMQ vs SQS の比較
  • イベント駆動アーキテクチャ
  • Pub/Subパターン
  • メッセージ配信保証(Exactly-once、At-least-once、At-most-once)

同期 vs 非同期

flowchart LR
    subgraph Sync["同期処理"]
        C1["クライアント"] -->|"リクエスト"| S1["サービス A"]
        S1 -->|"待機..."| S2["サービス B"]
        S2 -->|"待機..."| S3["サービス C"]
        S3 -->|"レスポンス"| S2
        S2 -->|"レスポンス"| S1
        S1 -->|"レスポンス"| C1
    end
    style Sync fill:#ef4444,color:#fff
flowchart LR
    subgraph Async["非同期処理"]
        C2["クライアント"] -->|"リクエスト"| SA["サービス A"]
        SA -->|"即座に応答"| C2
        SA -->|"メッセージ"| MQ["メッセージキュー"]
        MQ -->|"非同期処理"| SB["サービス B"]
        MQ -->|"非同期処理"| SC["サービス C"]
    end
    style Async fill:#22c55e,color:#fff
    style MQ fill:#f59e0b,color:#fff
比較項目 同期処理 非同期処理
レスポンス時間 全処理完了まで待つ 即座に応答可能
結合度 密結合(直接通信) 疎結合(キュー経由)
障害の影響 連鎖障害のリスク 障害が局所化
スケーリング 全サービスを同時にスケール 個別にスケール可能
複雑さ シンプル メッセージ管理が必要
適するケース 即座に結果が必要 バックグラウンド処理、重い処理

非同期処理が適するユースケース

  • メール/通知の送信
  • 画像・動画のエンコーディング
  • データ分析・集計
  • 注文処理(支払い確認後の後続処理)
  • ログ収集

メッセージキューの基本概念

flowchart LR
    subgraph Producers["プロデューサー"]
        P1["Producer 1"]
        P2["Producer 2"]
    end
    subgraph Queue["メッセージキュー"]
        subgraph T1["Topic: orders"]
            Part1["Partition 0"]
            Part2["Partition 1"]
            Part3["Partition 2"]
        end
    end
    subgraph Consumers["コンシューマー"]
        CG["Consumer Group"]
        C1["Consumer 1"]
        C2["Consumer 2"]
        C3["Consumer 3"]
    end
    P1 --> Part1
    P2 --> Part2
    Part1 --> C1
    Part2 --> C2
    Part3 --> C3
    style Queue fill:#3b82f6,color:#fff
    style T1 fill:#8b5cf6,color:#fff
    style Producers fill:#22c55e,color:#fff
    style Consumers fill:#f59e0b,color:#fff

主要な用語

用語 説明
Producer メッセージを送信する側
Consumer メッセージを受信・処理する側
Topic メッセージのカテゴリ(チャンネル)
Partition Topic内のデータ分割単位
Consumer Group 同じTopicを協力して処理するConsumerの集合
Offset Partition内のメッセージの位置(Kafka固有)
Acknowledgment (ACK) メッセージ処理完了の通知

Kafka vs RabbitMQ vs SQS

flowchart TB
    subgraph Kafka["Apache Kafka"]
        K1["分散ログストリーム"]
        K2["高スループット"]
        K3["メッセージ永続化"]
    end
    subgraph RabbitMQ["RabbitMQ"]
        R1["従来型メッセージブローカー"]
        R2["柔軟なルーティング"]
        R3["豊富なプロトコル"]
    end
    subgraph SQS["Amazon SQS"]
        S1["フルマネージド"]
        S2["無限スケール"]
        S3["運用負荷ゼロ"]
    end
    style Kafka fill:#3b82f6,color:#fff
    style RabbitMQ fill:#8b5cf6,color:#fff
    style SQS fill:#f59e0b,color:#fff
比較項目 Kafka RabbitMQ SQS
アーキテクチャ 分散ログ メッセージブローカー マネージドキュー
スループット 非常に高い(100万msg/s) 中程度(数万msg/s) 高い(自動スケール)
メッセージ保持 設定期間保持(リプレイ可) 消費後削除 最大14日間
順序保証 Partition内で保証 キュー内で保証 FIFO版で保証
配信保証 At-least-once / Exactly-once At-least-once At-least-once / Exactly-once(FIFO)
ルーティング Topic/Partitionベース Exchange/Bindingで柔軟 シンプルなキュー
運用コスト 高い(自己管理) 中程度 低い(マネージド)
適するケース ログ収集、イベントストリーミング タスクキュー、複雑なルーティング AWSネイティブ、シンプルなキュー

選択のガイドライン

flowchart TB
    Start["メッセージキューの選択"] --> Q1{"イベントストリーミング\nやログ収集?"}
    Q1 -->|"はい"| Kafka["Kafka"]
    Q1 -->|"いいえ"| Q2{"複雑なルーティング\nが必要?"}
    Q2 -->|"はい"| RabbitMQ["RabbitMQ"]
    Q2 -->|"いいえ"| Q3{"AWSを\n使っている?"}
    Q3 -->|"はい"| SQS["SQS"]
    Q3 -->|"いいえ"| Q4{"スループットが\n非常に高い?"}
    Q4 -->|"はい"| Kafka
    Q4 -->|"いいえ"| RabbitMQ
    style Start fill:#3b82f6,color:#fff
    style Kafka fill:#22c55e,color:#fff
    style RabbitMQ fill:#8b5cf6,color:#fff
    style SQS fill:#f59e0b,color:#fff

イベント駆動アーキテクチャ

イベント駆動アーキテクチャでは、サービス間の通信をイベント(事実の記録)で行います。

flowchart TB
    subgraph OrderService["注文サービス"]
        OS["注文作成"]
    end
    OS -->|"OrderCreated\nイベント"| EB["イベントバス\n(Kafka)"]
    EB --> PS["決済サービス\n支払い処理"]
    EB --> IS["在庫サービス\n在庫確保"]
    EB --> NS["通知サービス\nメール送信"]
    EB --> AS["分析サービス\nデータ記録"]
    style EB fill:#f59e0b,color:#fff
    style OrderService fill:#3b82f6,color:#fff
    style PS fill:#8b5cf6,color:#fff
    style IS fill:#22c55e,color:#fff
    style NS fill:#ef4444,color:#fff
    style AS fill:#22c55e,color:#fff

コマンド vs イベント

項目 コマンド イベント
意味 「〜してください」(命令) 「〜が起きました」(事実)
SendEmail OrderCreated
方向 1対1 1対多
結合度 送信者が受信者を知っている 送信者は受信者を知らない

Pub/Subパターン

Pub/Sub(Publish/Subscribe)は、メッセージの送信者と受信者を完全に分離するパターンです。

flowchart TB
    subgraph Publishers["パブリッシャー"]
        Pub1["ユーザーサービス"]
        Pub2["注文サービス"]
    end
    subgraph Topics["トピック"]
        T1["user-events"]
        T2["order-events"]
    end
    subgraph Subscribers["サブスクライバー"]
        Sub1["通知サービス"]
        Sub2["分析サービス"]
        Sub3["検索サービス"]
    end
    Pub1 --> T1
    Pub2 --> T2
    T1 --> Sub1
    T1 --> Sub2
    T2 --> Sub1
    T2 --> Sub2
    T2 --> Sub3
    style Topics fill:#3b82f6,color:#fff
    style Publishers fill:#22c55e,color:#fff
    style Subscribers fill:#8b5cf6,color:#fff

Pub/Subのメリット

メリット 説明
疎結合 PublisherはSubscriberの存在を知らない
スケーラビリティ Subscriberを独立にスケール可能
拡張性 新しいSubscriberの追加が容易
障害分離 1つのSubscriberの障害が他に影響しない

メッセージ配信保証

分散システムでのメッセージ配信には3つの保証レベルがあります。

flowchart TB
    subgraph Delivery["メッセージ配信保証"]
        AtMost["At-Most-Once\n最大1回\n(メッセージ損失の可能性)"]
        AtLeast["At-Least-Once\n最低1回\n(重複の可能性)"]
        Exactly["Exactly-Once\n正確に1回\n(理想的だが困難)"]
    end
    style AtMost fill:#22c55e,color:#fff
    style AtLeast fill:#f59e0b,color:#fff
    style Exactly fill:#ef4444,color:#fff
保証レベル 仕組み リスク 適するケース
At-Most-Once 送信後ACKを待たない メッセージ損失 ログ、メトリクス
At-Least-Once 処理完了までリトライ 重複処理 通知、メール送信
Exactly-Once べき等性 + トランザクション 実装が複雑、性能低下 決済、在庫管理

Exactly-Onceの実現方法

真のExactly-Onceは分散システムでは非常に困難です。一般的にはAt-Least-Once + べき等性で実現します。

# Idempotent consumer pattern
def process_order(message):
    order_id = message["order_id"]

    # Check if already processed (idempotency key)
    if db.exists(f"processed:{order_id}"):
        return  # Skip duplicate

    # Process the order
    db.execute_transaction([
        ("INSERT INTO orders ...", order_data),
        ("INSERT INTO processed VALUES (%s)", order_id),
    ])

べき等性(Idempotency)

操作 べき等か 理由
x = 5 はい 何回実行しても結果は同じ
x = x + 1 いいえ 実行回数で結果が変わる
DELETE FROM orders WHERE id = 123 はい 2回目以降は何も起きない
INSERT INTO orders VALUES (...) いいえ 重複レコードが生成される

実践:注文処理システムの設計

flowchart TB
    Client["クライアント"] -->|"注文リクエスト"| API["API Gateway"]
    API --> OrderSvc["注文サービス"]
    OrderSvc -->|"OrderCreated"| Kafka["Kafka"]

    Kafka --> PaymentSvc["決済サービス"]
    PaymentSvc -->|"PaymentCompleted"| Kafka

    Kafka --> InventorySvc["在庫サービス"]
    InventorySvc -->|"InventoryReserved"| Kafka

    Kafka --> ShippingSvc["配送サービス"]
    ShippingSvc -->|"ShippingScheduled"| Kafka

    Kafka --> NotificationSvc["通知サービス"]

    subgraph DLQ["Dead Letter Queue"]
        Failed["処理失敗メッセージ"]
    end
    PaymentSvc -->|"リトライ上限超過"| DLQ
    InventorySvc -->|"リトライ上限超過"| DLQ

    style Kafka fill:#3b82f6,color:#fff
    style OrderSvc fill:#8b5cf6,color:#fff
    style PaymentSvc fill:#22c55e,color:#fff
    style InventorySvc fill:#f59e0b,color:#fff
    style ShippingSvc fill:#22c55e,color:#fff
    style NotificationSvc fill:#8b5cf6,color:#fff
    style DLQ fill:#ef4444,color:#fff

設計のポイント

要素 設計判断 理由
メッセージキュー Kafka 高スループット、イベントリプレイが可能
配信保証 At-Least-Once + べき等性 注文の損失を防ぎつつ重複を安全に処理
Dead Letter Queue 失敗メッセージの退避先 リトライ上限超過時の安全策
Saga パターン 分散トランザクション管理 決済失敗時に在庫を戻す補償処理

Sagaパターン(補償トランザクション)

正常フロー:
注文作成 → 決済処理 → 在庫確保 → 配送手配

補償フロー(在庫確保失敗時):
在庫確保失敗 → 決済キャンセル → 注文キャンセル

まとめ

今日のポイント

トピック キーポイント
同期 vs 非同期 即時応答不要ならば非同期で疎結合に
メッセージキュー Producer→Topic→Partition→Consumer の流れ
Kafka vs RabbitMQ vs SQS ストリーミング→Kafka、ルーティング→RabbitMQ、マネージド→SQS
イベント駆動 コマンド(命令)ではなくイベント(事実)で通信
Pub/Sub Publisher と Subscriber を完全に分離
配信保証 At-Least-Once + べき等性が現実的な選択

面接での使い方

1. 「この処理は非同期で良いか?」を常に問いかける
2. 非同期処理にはメッセージキューを提案
3. 配信保証レベルを明示する
4. 失敗時のリトライとDLQを忘れずに
5. 分散トランザクションにはSagaパターン

練習問題

基礎レベル

問題: 以下の処理について、同期と非同期のどちらが適切か理由と共に答えてください。

  1. ユーザーログイン認証
  2. 注文確認後のメール送信
  3. 画像のサムネイル生成
  4. 商品価格の取得

中級レベル

問題: 以下の要件を満たすメッセージングシステムを設計してください。

  • ECサイトの注文イベントを処理
  • 日次100万件の注文
  • 決済、在庫、通知の3つのサービスが注文イベントを処理
  • 決済は必ず成功させる必要がある(損失不可)
  • 通知は多少の遅延・損失は許容
  • どのメッセージキューを選択するか、配信保証レベルは何か

チャレンジレベル

問題: Uberのようなライドシェアサービスの注文処理を設計してください。

  • ユーザーが配車をリクエスト
  • 近くのドライバーにマッチング
  • ドライバーがリクエストを承認/拒否
  • リアルタイムの位置情報更新
  • 乗車完了後の決済処理
  • 同期処理と非同期処理をどこで使い分けるか
  • メッセージキューをどのように活用するか
  • 障害時の対策(ドライバーアプリがクラッシュした場合等)

参考リンク


次回予告

Day 6: API設計とプロトコル では、REST、GraphQL、gRPCの使い分け、レート制限、APIバージョニングなど、システムのインターフェース設計を学びます。面接では必ず問われるAPIの設計方法を身につけましょう。