メッセージキュー入門|RabbitMQ・SQS・Kafkaの違いと非同期処理への導入判断

kento_morota 14分で読めます
社内のAI・IT活用、技術がわかる相手に無料相談。 相談 →
目次

「注文完了のメール送信に時間がかかって、画面がなかなか返らない」「外部APIが落ちたら自社の処理まで全部エラーになった」「メッセージキューを入れるべきだと言われたが、RabbitMQとKafkaとSQSの何が違うのかわからない」――Webアプリが育ってくると、こうした悩みは必ず出てきます。

この記事では、なぜ同期処理だけでは限界が来るのかという仕組みの話から、キューとPub/Subの違い、at-least-once/at-most-onceという配信保証の意味、RabbitMQ・SQS・Kafka・Redis Streamsそれぞれの性格の違いを整理します。最後に、「小規模ならDBのテーブルをキューにするだけで十分」という現実的な判断基準まで示しますので、自分のプロジェクトに何が必要かを判断できるようになります。

同期処理の限界と、メッセージキューが解決すること

「リクエストの中で全部やる」設計はどこで破綻するか

典型的なWebアプリは、リクエストを受けてDBに書き込み、レスポンスを返す、という同期的な流れで動いています。ここに「メールを送る」「サムネイルを作る」「会計SaaSのAPIに売上を登録する」といった処理を素直に追加していくと、レスポンス時間は各処理の合計になります。しかも、外部のメールサービスが3秒詰まればユーザーは3秒待たされ、外部APIがタイムアウトすれば注文自体が失敗したように見えます。

問題の本質は「密結合」です。注文を確定させる処理と、メールを送る処理は本来独立していて、後者が数秒遅れてもビジネス上は問題ありません。それなのに同じリクエストの中に置いたことで、片方の遅延や障害がもう片方を道連れにしています。

キューを挟むと「受け取る側」と「処理する側」が切り離せる

メッセージキューは、処理の依頼(メッセージ)を一時的に溜めておく仕組みです。Webアプリは「注文IDが123の確認メールを送ってほしい」というメッセージをキューに入れたらすぐレスポンスを返し、別プロセスのワーカーがキューからメッセージを取り出してメールを送ります。これで次の3つが手に入ります。

  1. 応答速度:重い処理をリクエストから追い出せるため、ユーザーの待ち時間はDB書き込み分だけになる。
  2. 耐障害性:外部サービスが落ちていてもメッセージはキューに残り、復旧後に処理できる。
  3. 負荷平準化:瞬間的に1,000件の依頼が来ても、ワーカーは自分のペースで処理できる(バーストの吸収)。

これが「疎結合」です。ワーカー側の設計(リトライや冪等性)はバックグラウンドジョブの設計で扱っているので、本記事はキュー(ブローカー)そのものの選び方に絞ります。

キューとPub/Sub:メッセージの届き方が違う

メッセージングには大きく2つのモデルがあり、製品によって得意な方が異なります。

キュー(ポイントツーポイント)

1つのメッセージを、複数いるワーカーのうちどれか1つが受け取って処理します。「処理を分担する」ためのモデルで、メール送信や画像変換のような「1回やればよい仕事」の分配に向きます。ワーカーを増やせば処理能力が上がるのが特徴です。

Pub/Sub(パブリッシュ/サブスクライブ)

1つのメッセージ(イベント)を、購読しているすべての受信者に届けます。「注文が確定した」というイベントを、メール送信サービス・在庫サービス・分析基盤がそれぞれ受け取る、という形です。送る側は誰が受け取るかを知らなくてよいため、後から受信者を増やしても送信側のコードは変わりません。イベント駆動の設計全体についてはイベント駆動アーキテクチャ入門で解説しています。

実務では両方を組み合わせることが多く、「イベントをPub/Subで配り、各受信者は自分のキューに溜めてワーカーで処理する」という構成(SNS+SQSのファンアウトなど)が定番です。

配信保証:at-least-once と at-most-once

ブローカーを選ぶうえで必ず理解しておくべきなのが、「メッセージが何回届きうるか」という配信保証です。

なぜ「ちょうど1回」が難しいのか

ワーカーがメッセージを取り出し、処理し、「処理済み」をブローカーに通知(ACK)する、という流れを考えます。処理が終わった直後、ACKを送る前にワーカーが落ちたらどうなるでしょうか。ブローカーからはACKが来ていないので「未処理」と判断して別のワーカーに再配信します。結果、同じメッセージが2回処理されます。逆に、取り出した瞬間にACKを返す方式にすると、処理中にワーカーが落ちたメッセージは失われます。

  • at-least-once(少なくとも1回):失わない代わりに重複しうる。ほとんどのキューのデフォルトで、受信側を冪等(同じメッセージを2回処理しても結果が変わらない)にすることが前提。
  • at-most-once(多くとも1回):重複しない代わりに失いうる。ログやメトリクスなど「1件くらい欠けても困らない」データ向け。
  • exactly-once:Kafkaのトランザクション機能など限定的な範囲で実現できるが、外部API呼び出しのようにブローカーの外に副作用がある処理では成立しない。実務上は「at-least-once+冪等な処理」で設計するのが基本。

つまり、どのブローカーを選んでも「同じメッセージが2回来る前提でワーカーを書く」ことは避けられません。この点を最初に受け入れておくと、後の設計が楽になります。

RabbitMQ・SQS・Kafka・Redis Streamsの特性比較

代表的な4つを、「何を得意とする道具か」という観点で比較します。

項目RabbitMQAmazon SQSApache KafkaRedis Streams
モデルキュー中心(Exchangeで柔軟なルーティング)キュー(Pub/SubはSNSと併用)分散ログ(Pub/Sub+コンシューマグループ)ログ+コンシューマグループ
メッセージの保持ACKされたら削除ACK(削除)されたら削除。最長14日保持設定した期間・容量まで保持し、何度でも読み直せる明示的に削除するまで保持(メモリ上)
順序保証1キュー内で概ね順序どおり(再配信で崩れる)標準キューは順序なし。FIFOキューはグループ内で順序保証パーティション内で厳密に順序保証ストリーム内で順序保証
運用自前運用(Amazon MQなどのマネージドあり)フルマネージド。サーバー管理不要自前運用は重い。MSK・Confluentなどのマネージドが現実的Redisが既にあれば追加コストほぼゼロ
向いている用途タスク分配、複雑なルーティング、RPC的な用途AWS上のジョブキュー全般大量イベントのストリーミング、複数システムへの配信、再処理小〜中規模のジョブキュー、既存Redisの活用

RabbitMQ:伝統的で柔軟な「ブローカー」

AMQPというプロトコルを実装したメッセージブローカーで、Exchange(振り分け役)とQueue(溜める場所)を組み合わせて、「このルーティングキーならこのキューへ」といった細かい制御ができます。優先度付きキュー、TTL、デッドレターキューなどキューとして必要な機能が揃っており、Celeryのブローカーとしても定番です。自前で運用する場合はクラスタ構成とディスク監視が必要です。

Amazon SQS:運用ゼロで始められるキュー

AWSのフルマネージドキューで、サーバーもクラスタも意識せずに使えます。ワーカーがメッセージを受信すると「可視性タイムアウト」(デフォルト30秒)の間そのメッセージは他のワーカーから見えなくなり、時間内に削除されなければ再び見えるようになります。これがat-least-onceを実現する仕組みです。処理に30秒以上かかるジョブでは、可視性タイムアウトを延ばさないと二重実行の原因になります。標準キューはスループット無制限の代わりに順序を保証せず、順序が必要ならFIFOキューを選びます。Lambdaのトリガーとして使うと、ワーカーすらサーバーレスにできます(AWS Lambdaの使い方)。

Kafka:キューではなく「追記型のログ」

Kafkaは他の3つと発想が根本的に異なり、メッセージを削除するのではなく、トピックという追記専用のログに書き溜め、消費者(コンシューマ)が「どこまで読んだか(オフセット)」を自分で管理します。このため、同じデータを複数のシステムがそれぞれのペースで読めますし、バグ修正後に「昨日の分から読み直す」こともできます。秒間数十万件のイベントも扱える一方、ZooKeeperまたはKRaftを含むクラスタ運用は重く、パーティション設計を誤ると順序やスケーラビリティの問題が出ます。「1回処理して終わり」のジョブキュー用途に選ぶのは過剰です。

Redis Streams:すでにRedisがあるなら有力

Redis 5.0で追加されたデータ型で、Kafkaに似たログ構造とコンシューマグループを持ちながら、単一のRedisで手軽に使えます。ACKされていないメッセージはXPENDINGで確認でき、落ちたワーカーの分をXCLAIMで他のワーカーが引き取れます。BullMQ(Node.js)など多くのジョブライブラリはRedisをバックエンドにしており、キャッシュ用に既にRedisがある小〜中規模のシステムでは最初の選択肢になります。メモリ上のデータなので、永続化設定(AOF)とメモリ上限には注意が必要です。Redisの基本はRedis入門を参照してください。

小規模ならDBテーブルのキューで十分という判断基準

新しいミドルウェアを増やすコストを正しく見積もる

ブローカーを1つ足すということは、監視対象・障害点・バックアップ対象・セキュリティ更新対象が1つ増えるということです。1日数千件程度のジョブで、遅延が数秒〜数十秒許容できるなら、既に運用しているRDBのテーブルをキューとして使う方が総コストは低いことが多いです。トランザクションの中でジョブを登録できるため、「注文はコミットされたのにジョブだけ登録に失敗した」というブローカー特有の不整合も起きません。

PostgreSQLではFOR UPDATE SKIP LOCKEDを使うと、複数ワーカーが同じ行を取り合わずに安全にジョブを取り出せます。

CREATE TABLE jobs (
  id          bigserial PRIMARY KEY,
  queue       text        NOT NULL DEFAULT 'default',
  payload     jsonb       NOT NULL,
  run_at      timestamptz NOT NULL DEFAULT now(),
  attempts    int         NOT NULL DEFAULT 0,
  locked_at   timestamptz,
  finished_at timestamptz
);
CREATE INDEX ON jobs (queue, run_at) WHERE finished_at IS NULL;

-- ワーカーが1件取り出す(他ワーカーがロック中の行は飛ばす)
WITH next AS (
  SELECT id FROM jobs
   WHERE queue = 'default' AND finished_at IS NULL AND run_at <= now()
     AND (locked_at IS NULL OR locked_at < now() - interval '10 minutes')
   ORDER BY run_at
   FOR UPDATE SKIP LOCKED
   LIMIT 1
)
UPDATE jobs SET locked_at = now(), attempts = attempts + 1
  FROM next WHERE jobs.id = next.id
RETURNING jobs.*;

ワーカーは処理後にfinished_atを更新し、失敗したらrun_atを未来にずらしてlocked_atを戻します。ロックの仕組みについてはトランザクション分離レベルとデッドロックも併せて読むと理解が深まります。

専用ブローカーに移行すべきサイン

次のいずれかに当てはまったら、専用ブローカーの導入を検討する段階です。

  • ジョブテーブルのポーリング(数秒おきのSELECT)がDBの負荷として無視できなくなった
  • 1つのイベントを複数のシステムがそれぞれ受け取る必要が出てきた(Pub/Subが必要)
  • 秒間数百件以上のメッセージを継続的にさばく必要がある
  • 過去のイベントを再処理・再集計したい(Kafkaのような保持型が必要)

AWS上なら最初の移行先はSQSが無難で、追加の運用がほぼありません。Node.jsでSQSにメッセージを送る例を示します。

import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({ region: "ap-northeast-1" });

export async function enqueueOrderMail(orderId) {
  await sqs.send(new SendMessageCommand({
    QueueUrl: process.env.ORDER_MAIL_QUEUE_URL,
    MessageBody: JSON.stringify({ type: "order_confirmation", orderId }),
    // 同じ注文の重複投入を5分間デデュープ(FIFOキューの場合)
    // MessageGroupId: `order-${orderId}`,
    // MessageDeduplicationId: `order-mail-${orderId}`,
  }));
}

トラブル事例:SQSで同じメールが2通届く

症状

注文確認メールをSQS+ワーカーで送る構成に変えたところ、「同じメールが2通届いた」という問い合わせが散発的に発生しました。ワーカーのログを見ると、同じメッセージIDが2回処理されています。

原因

PDFの請求書生成を含むメール処理に平均40秒かかっており、SQSの可視性タイムアウト(デフォルト30秒)を超えていました。30秒経過した時点でメッセージが再び「見える」状態になり、別のワーカーが同じメッセージを取得して処理を始めていたのです。1台目は処理完了後に正常に削除していましたが、そのときには2台目が既に送信を終えていました。at-least-onceの典型的な現れ方です。

対処

  1. キューの可視性タイムアウトを処理時間の最大値に余裕を持たせた5分に変更した。
  2. ワーカー側で、処理開始時にDBのsent_mailsテーブルに注文IDをユニークキーで挿入し、既に存在すれば送信をスキップするよう冪等化した。
  3. 長時間ジョブ用に、処理中はChangeMessageVisibilityで定期的にタイムアウトを延長するハートビートを追加した。
aws sqs set-queue-attributes \
  --queue-url "$ORDER_MAIL_QUEUE_URL" \
  --attributes VisibilityTimeout=300

タイムアウトの調整だけでは根本解決にならず、冪等化を組み合わせて初めて重複が止まった点が重要です。

まとめ

メッセージキューは、リクエストの中に詰め込まれた処理を切り離し、応答速度・耐障害性・負荷平準化を手に入れるための道具です。キューは「仕事の分配」、Pub/Subは「イベントの配布」というモデルの違いを押さえ、どのブローカーを選んでも「同じメッセージが2回来る」前提で受信側を冪等に作るのが基本です。

製品選びは、AWS上でジョブキューが欲しいならSQS、既にRedisがあるならRedis Streams(やBullMQ)、複雑なルーティングやCeleryとの相性ならRabbitMQ、大量イベントの配信と再処理ならKafka、という整理で大きく外れません。そして、1日数千件規模で遅延が許容できるなら、まずはDBテーブルのキューで始めて、ポーリング負荷やPub/Subの必要性が見えてから専用ブローカーに移るのが堅実な進め方です。

非同期処理の導入やブローカー選定で、自社の規模に合った構成を一緒に検討したい場合は、Harmonic Societyのシステム開発・インフラ支援までご相談ください。

#メッセージキュー#RabbitMQ#Kafka#非同期処理

Harmonic Society

この記事の内容、自社の業務でも活かせそうですか?

ローカルLLM・AI・クラウドなどの技術導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK。まずは30分の無料相談から。売り込みはしません。

共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

関連記事

Related / 9 articles

  1. プログラミング

    DDoS攻撃の仕組みと対策入門|レイヤー別の防御とCDN・クラウドの活用

    DDoS攻撃をボリューム型・プロトコル型・アプリ層に分けて仕組みを解説し、自前サーバーで防げない理由、CloudflareやAWS Shieldの標準防御、オリジンIPの隠し方、レートリミットとBot対策、攻撃を受けたときの初動、費用が跳ね上がるDenial of Walletへの備えまでわかります。

  2. プログラミング

    WAFとは?仕組み・導入パターン・誤検知対策|Webアプリを攻撃から守る実践ガイド

    WAFがファイアウォールやIDSと何が違うのか、シグネチャとマネージドルールの仕組み、Cloudflare WAF・AWS WAF・ModSecurityの比較、フォーム送信がブロックされる誤検知の調査と例外設定、ログ監視、WAFが代替できないことまで実践的に解説します。

  3. プログラミング

    セキュリティヘッダー入門|CSP・HSTS・X-Frame-Optionsの設定と効果を実践解説

    CSP・HSTS・X-Frame-Options・X-Content-Type-Optionsなど主要セキュリティヘッダーが防ぐ攻撃と、CSPのReport-Onlyからの段階導入、nonce/hash、HSTS preloadの不可逆リスク、Nginx・Next.js・Astroでの設定例、確認方法を解説します。

  4. プログラミング

    クラウドの通信費(Egress)入門|データ転送量課金の仕組みと転送コストを抑える設計

    クラウドの「受信無料・送信有料」の原則、AZ間・リージョン間・インターネット向けの単価差、NATゲートウェイ処理料の罠、CDNで転送量を減らす方法、バックアップやログ転送の見落とし、請求書で転送料を特定する手順を解説。想定外の請求を防げます。

  5. プログラミング

    秘密情報をGitに入れない仕組み|.gitignore・git-secrets・履歴から漏れた鍵の削除

    APIキーや.envをGitにコミットしてしまう典型経路と、.gitignore・.env.exampleの運用、pre-commitでのgitleaks検知、GitHub secret scanningの活用、漏れた鍵の無効化と履歴書き換え(git filter-repo)の手順を解説。仕組みで再発を防げます。

  6. プログラミング

    開発・ステージング・本番環境の分離設計|環境差分をなくす構成とアクセス制御

    開発・ステージング・本番それぞれの目的と、構成をコードで揃える方法、環境別の設定注入、本番データを使わないテストデータ戦略、ステージングの保護(Basic認証・IP制限・noindex)、コストを抑える運用までを解説。環境差分による本番障害を防げます。

  7. プログラミング

    ngrok・Cloudflare Tunnelでローカルを公開|Webhook開発とデモ環境の作り方

    NAT内のローカル環境にStripeやLINEのWebhookを届けるトンネリングの仕組みを解説。ngrok・Cloudflare Tunnel・localtunnelの比較、固定ドメインと認証、リクエスト検査、公開時のセキュリティ、自宅サーバー公開への応用までわかります。

  8. プログラミング

    ローカル開発環境のHTTPS化|mkcert・hostsファイル・自己署名証明書の正しい使い方

    ローカル開発をHTTPS前提にすべき理由(Secure Cookie・Service Worker・OAuth)と、mkcertでローカルCAを作りhostsで独自ドメインを割り当ててVite・Next.js・Dockerで使う手順を解説。証明書警告を無視する癖の危険も理解できます。

  9. プログラミング

    localhost・0.0.0.0・127.0.0.1の違い|ポートとUnixソケットを理解して「つながらない」を解決

    localhost・127.0.0.1・0.0.0.0の意味の違い、Dockerで外から接続できない原因、host.docker.internal、ポート競合の調べ方、Unixソケットの利点と権限、1024未満ポートの制約を解説。「つながらない」を仕組みから解決できます。

Harmonic Society

「読んで終わり」にせず、自社の業務で試してみませんか?

AI・ローカルLLM・クラウドの導入を、要件整理からPoC・社内展開まで代表エンジニアが伴走します。オンライン対応・全国OK・売り込みなし。

無料・30分・オンラインOK|1営業日以内に返信します