0Pricing
PHP Academy · レッスン

非同期メッセージングが必要な理由

キューによってプロデューサーとコンシューマーを分離する方法を学びます。

「非同期メッセージングが必要な理由」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

非同期メッセージングを使う理由

同期的なリクエストでは、PHPプロセスはメールゲートウェイ、決済処理サービス、または下流サービスと通信している間、ブロックされます。負荷が高い状況では、呼び出すすべての依存先にレイテンシーと可用性が左右されます。非同期メッセージングはこの連鎖を断ち切ります。プロデューサーはキューにメッセージを入れてすぐに戻り、コンシューマーは後から自分のペースで処理します。

このレッスンでは、RabbitMQやKafkaに触れる前に理解しておくべき、なぜ非同期にするのか、つまり分離、バッファリング、配信保証に関するトレードオフを扱います。

時間的・空間的な分離

キューは、次の2つの軸でプロデューサーとコンシューマーを分離します。

  • 空間的 — どちらも相手のアドレスを知る必要はなく、ブローカーだけを認識します。
  • 時間的 — プロデューサーが発行した時点でコンシューマーが停止していても、メッセージは待機します。

以下の同期版では、Webリクエストがメールサーバーの稼働状況と処理速度に依存します。

<?php
// Synchronous: the HTTP request blocks on SMTP
function registerUser(string $email): void {
    saveUser($email);
    // If SMTP is slow or down, the user waits or the request fails
    sendWelcomeEmail($email); // 800ms blocking call
    echo "Registered\n";
}

function saveUser(string $e): void { /* ... */ }
function sendWelcomeEmail(string $e): void { usleep(1); }

registerUser('dev@example.com');

発行して次へ進む

非同期版では、ユーザーを永続化し、UserRegisteredメッセージを発行してから戻ります。メールは別のワーカーが送信します。これでHTTPのレイテンシーは、SMTPとの往復ではなく、高速なローカルブローカーへの発行処理だけに依存します。

<?php
function registerUser(string $email): void {
    saveUser($email);
    // Publish a lightweight event; worker handles email later
    publish('user.registered', json_encode(['email' => $email]));
    echo "Registered (email queued)\n";
}

function saveUser(string $e): void { /* ... */ }
function publish(string $routingKey, string $payload): void {
    echo "-> queued $routingKey: $payload\n";
}

registerUser('dev@example.com');

負荷平準化(バッファリング)

トラフィックの急増は不均一に発生しますが、処理能力は一定です。キューはバッファとして機能し、10,000件のメッセージの急増を吸収して、ワーカーのプールが持続可能な速度で処理できるようにします。キューがなければ、その急増がデータベースや下流APIを直接圧迫してしまいます。

これが負荷平準化です。メッセージがしばらく待機するというレイテンシーと引き換えに、システム全体が停止しない安定性を得ます。

配信保証

すべてのメッセージングシステムは、何らかの配信を保証します。自分がどの保証を利用しているのかを把握してください。

  • At-most-once — 送信して忘れる方式で、メッセージは失われる可能性がありますが、重複はしません。
  • At-least-once — 確認応答されるまで再配信されるため、重複する可能性があります。一般的なデフォルトです。
  • Exactly-once — 損失も重複もありませんが、コストが高く、アプリケーション層では部分的な見せかけにすぎないことも多くあります。

現実のシステムの多くはat-least-onceであるため、コンシューマーは同じメッセージを2回受け取っても処理できなければなりません。

冪等なコンシューマー

at-least-onceで発生する重複への対策が冪等性です。メッセージを2回処理しても、1回処理した場合と同じ結果になります。一般的な方法は、重複排除キー(メッセージID)を一意インデックスに保存することです。

<?php
function handle(array $msg, PDO $pdo): void {
    $pdo->beginTransaction();
    try {
        // Unique constraint on message_id makes the insert the dedup gate
        $stmt = $pdo->prepare(
            'INSERT INTO processed_messages (id) VALUES (?)'
        );
        $stmt->execute([$msg['id']]);
    } catch (PDOException $e) {
        $pdo->rollBack();
        echo "Duplicate {$msg['id']} skipped\n";
        return; // already handled
    }
    chargeCustomer($msg['amount']);
    $pdo->commit();
}
function chargeCustomer(int $a): void {}

確認応答と再配信

コンシューマーはackで成功を通知します。ackを送る前にクラッシュすると、ブローカーは別のコンシューマーにメッセージを再配信します。これがat-least-onceを実現する仕組みですが、つまりackは副作用がコミットされた後に送らなければならず、決して前ではいけません。

Ackが早すぎると、クラッシュ時にメッセージが失われます。Ackが遅すぎると、クラッシュ時に重複が発生しますが、これは冪等性のレイヤーで吸収できます。

<?php
// Pseudocode of the consumer contract
function consumeLoop($channel): void {
    while ($msg = $channel->get()) {
        try {
            processSideEffect($msg);   // commit DB write first
            $channel->ack($msg);       // only then ack
        } catch (\Throwable $e) {
            $channel->nack($msg, requeue: true); // let it redeliver
        }
    }
}
function processSideEffect($m): void {}

順序性にはコストがかかる

コンシューマーをスケールアウトすると、キューはグローバルな順序性を保証しません。同じキューから2つのワーカーがメッセージを取得して並行処理するため、メッセージBがメッセージAより先に完了する可能性があります。

順序が重要な場合(口座残高の増減など)は、関連するすべてのメッセージが1つのコンシューマーに順番どおり届くように、キーでパーティション分割する必要があります。Kafkaではパーティションによってこれをネイティブに実現できます。RabbitMQでは、一貫性ハッシュを使ってキーごとのキューにルーティングします。

デッドレターキュー

形式が不正なペイロードや、削除済みの行への参照など、決して成功しないメッセージもあります。それらを永遠に再試行すると、キューが詰まってしまいます(poison message)。そのため、デッドレターキュー(DLQ)というパターンを使います。N回失敗したメッセージを再配信し続けるのではなく、調査用に別の場所へ振り分けます。

<?php
function consume(array $msg, $channel): void {
    $attempts = ($msg['headers']['x-attempt'] ?? 0) + 1;
    try {
        process($msg);
        $channel->ack($msg);
    } catch (\Throwable $e) {
        if ($attempts >= 5) {
            $channel->deadLetter($msg); // park in DLQ
        } else {
            $channel->republish($msg, ['x-attempt' => $attempts]);
        }
    }
}
function process(array $m): void {}

キューを使わない場合

非同期メッセージングには、実際の運用コストが伴います。ブローカーの運用、結果整合性をプロダクト側に説明すること、プロセス境界をまたぐことでデバッグが難しくなることなどです。

処理が遅い、急増しやすい、再試行可能、またはfire-and-forgetの場合にキューを使ってください。呼び出し側が本当に結果を今すぐ必要とする場合(ユーザーに表示する必要がある確定的な価格など)は同期処理のままにします。回答が必要な処理をキューに入れても、レイテンシーと複雑さが増すだけです。

キューとログの比較

これらのパターンを支えるブローカーには、大きく2種類の形があり、このコースの後半では両方を使います。

  • タスクキュー(RabbitMQ)は、メッセージがackされると削除します。競合するコンシューマーのプールへの作業分散、メッセージごとのルーティング、TTLの利用に適しています。
  • コミットログ(Kafka)は、保持期間に応じてメッセージを保持します。各コンシューマーが独自のオフセットを追跡して履歴を再生でき、多数の独立したコンシューマーグループが同じストリームを読み取れます。

作業の分散にはキューを、高スループットのイベントストリームと再生にはログを選びます。

<?php
$useCase = 'replay events for a new analytics service';
$broker = str_contains($useCase, 'replay') || str_contains($useCase, 'stream')
    ? 'Kafka (commit log)'
    : 'RabbitMQ (task queue)';
echo $broker . "\n";

確認問題

配信保証について考えます。

まとめ

これで、非同期メッセージングの基本的な考え方を理解できました。

  • キューは空間的・時間的な分離を実現し、負荷平準化のためのバッファとして機能します。
  • ほとんどのシステムはat-least-onceであるため、コンシューマーは冪等でなければなりません。
  • 副作用がコミットされた後にAckし、失敗した場合は再配信させます。
  • 順序性にはキーによるパーティション分割が必要で、poison messageにはDLQが必要です。
  • 呼び出し側が同期的な回答を必要とする処理をキューに入れてはいけません。

次は、PHPでRabbitMQを使ってこれを実践します。

よくある質問

「非同期メッセージングが必要な理由」レッスンは無料ですか?

はい。「非同期メッセージングが必要な理由」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「非同期メッセージングが必要な理由」で何を学びますか?

キューによってプロデューサーとコンシューマーを分離する方法を学びます。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「非同期メッセージングが必要な理由」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. 非同期メッセージングが必要な理由
  2. PHPでRabbitMQを扱う
  3. PHPでApache Kafkaを扱う
  4. イベント駆動ワークフローの構築
← PHP Academyに戻る