非同期メッセージングが必要な理由
キューによってプロデューサーとコンシューマーを分離する方法を学びます。
「非同期メッセージングが必要な理由」は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フィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 非同期メッセージングが必要な理由
- PHPでRabbitMQを扱う
- PHPでApache Kafkaを扱う
- イベント駆動ワークフローの構築