メッセージキューを使い倒した私が語る、RabbitMQ vs Kafka vs Redis
同一ハードウェア上で1KBのJSONメッセージを連続投入した「参考ベンチマーク」の一例として、レプリケーションと耐久性を有効にした条件では、Kafkaが約55万msg/s、Redis Streamsが約32万msg/s、RabbitMQ(Quorum Queue)が約9.5万msg/sという傾向が確認されることがある。条件を厳密に揃えても、ワークロードやネットワーク、ストレージ特性で結果は大きく揺れるため、数値はあくまで前提とセットで読む必要がある⁵⁶。大まかな特徴として、Kafkaはスループットで優位¹、RabbitMQはレイテンシの安定と機能の豊富さ(例:デッドレターやTTL)⁵¹⁰¹¹、Redisはシンプルさと低レイテンシ⁷が目立つ。実務では、指標の比較に加えて再処理や障害時のふるまいまで含めて選ぶと失敗が減る、というのが一般的な教訓だ。
メッセージングの本質と比較軸を先に定義する
メッセージングの議論は宗教論争に陥りがちだが、設計に直結する軸を明確にすると迷いが減る。まず配信保証では、RabbitMQとRedis Streamsは標準機能で少なくとも一度(at-least-once:重複は起こり得るが取りこぼさない)に寄せやすい。Kafkaはオフセット管理とトランザクションを組み合わせることで厳密に一度(exactly-once:重複も取りこぼしも回避)まで引き上げられるが、その代償として構成が複雑になる²。順序性は単一パーティションや単一キュー内では保てるが、並列度を上げるほど保証の範囲は狭まる。ここで重要なのは、順序とスループットは綱引きになるという割り切りだ¹²。
耐久性とレプリケーションは設計の中核になる。Kafkaはログ構造のセグメントをレプリカに複製し、acks=allとmin.insync.replicasの設定で強い整合と耐障害性を両立させる³⁴。一方RabbitMQはQuorum QueueでRaft合意(分散合意アルゴリズム)を用い、ブローカー障害時の可用性を確保する⁵。Redis StreamsはAOF(Append Only File)やレプリカを組み合わせる構成が一般的で、毎秒同期(everysec)か常時同期(always)の選択で性能と耐久性のバランスが大きく変わる⁸。
レイテンシはユースケースの可否を決める。1KBメッセージを継続投入した同条件の参考測定では、p99(99パーセンタイル)がRabbitMQで10〜20ms、Kafkaで40〜80ms(バッチ有効)、Redis Streamsで2〜5ms(AOF everysec)といった例がある。Kafkaはlinger.msやbatch.sizeを高めると、レイテンシとの引き換えでスループットが伸びるのが一般的な特性だ³。SLO(サービス目標)をミリ秒単位で追うAPI連携ではRabbitMQやRedisが扱いやすく、巨大なイベントストリームや分析連携ではKafkaのほうが総合コストが下がる場面が多い。
運用の観点では、Kafkaはブローカー、ZooKeeper不要のKRaft(内蔵メタデータ管理)、パーティション増減、セグメント保守と、学習と自動化の投資が前提になる⁹。RabbitMQは運用項目が比較的明快で、デッドレター(DLX)、遅延、ルーティング、優先度、TTLといった機能で業務要件を直接表現しやすい¹⁰¹¹。Redisは構成が簡潔で、小さなチームでも回しやすいという強みがある⁷。
選定の一行要約をあえて言語化する
イベント駆動でスループットの天井を上げたいならKafka、業務プロセスの実行制御やバックプレッシャーを丁寧に扱うならRabbitMQ、超低レイテンシのシンプルなキューイングや一時的な集約にはRedisが合理的だ。もちろん境界は揺らぐが、ここを起点に要件を精密化していくと議論が前に進む。
現実のベンチマークとチューニングの勘所
参考の測定環境は、vCPU 8・メモリ32GB・NVMe相当のローカルSSDを持つ同一スペックの3台で、10GbE相当のネットワーク。Kafkaはレプリケーション係数3、acks=all、linger.ms=5、batch.size=64KB³⁴。RabbitMQはQuorum Queueでpublisher confirms(送信確認)を有効化し、prefetch(先読み数)を調整。RedisはAOF everysec、XADDをパイプライン送信という条件だ⁸。メッセージは固定長1KBのJSONで、プロデューサとコンシューマは同一AZに配置した。
この参考構成では、Kafkaが55万msg/s前後で頭打ちになり、バッチを深くするとスループットは伸びるがp99が悪化するという典型的な挙動を示した³。RabbitMQはpublisher confirmsとディスク同期の影響で10万msg/s付近に落ち着くが、ジッタが小さくワークフロー実行と相性が良い⁶。Redisは32万msg/s程度で、AOFをalwaysに変えると大きく落ちる一方、耐障害性は向上する⁸。ベンチの数字そのものより、チューニングのダイヤルがどこにあるかをチーム全体が理解しているかが重要になる。
スループットを上げるときに一般に効くのは、Kafkaではパーティションの適正化とプロデューサ側の圧縮、そしてネットワークとストレージの割り当て監視³。RabbitMQではチャネルの多重化とprefetchの調整、コネクション数の上限管理がボトルネック解消に効く。Redisではパイプラインの深さとキー空間設計、そしてスナップショットやAOFのポリシーが支配的だった⁷⁸。いずれも、早すぎるマイクロ最適化より計測と可視化の徹底が費用対効果は高い。
ベンチだけでは見えない「再処理設計」
本番で効くのは速さではなく、失敗時に何が起きるかだ。Kafkaはオフセットを戻せば自然に再処理でき、スナップショットとログの組み合わせで過去イベントの再生も容易だ¹²。RabbitMQはデッドレターと遅延再投機でリトライを制御しやすい¹⁰¹¹。Redis Streamsは未処理のエントリをPendingリスト(PEL)で把握し、XCLAIMで引き継げる⁷。再処理を前提にした冪等性と重複排除をアプリ側に組み込めるかどうかが、選定の隠れた決定要因になっている。
コードで見る違い:最小実装と落とし穴
RabbitMQの基本形は明快だ。永続化したメッセージを確実に受け渡すなら、キューをdurableにし、メッセージをpersistentにして、publisher confirmsと手動ACKを組み合わせる。短い例を示す。
# RabbitMQ Publisher (pika)
import json, pika, time
params = pika.URLParameters("amqp://guest:guest@localhost/%2F")
conn = pika.BlockingConnection(params)
ch = conn.channel()
ch.queue_declare(queue="jobs", durable=True)
ch.confirm_delivery()
for i in range(1000):
body = json.dumps({"id": i, "ts": time.time()})
try:
ch.basic_publish(
exchange="",
routing_key="jobs",
body=body,
properties=pika.BasicProperties(delivery_mode=2)
)
except pika.exceptions.UnroutableError as e:
print("publish failed", e)
conn.close()
コンシューマ側はprefetchでバックプレッシャーをかけつつ、例外時のnackとデッドレター行きのポリシーを明示する。ここでは再キューではなく破棄してDLXに流す構成を前提にする。
# RabbitMQ Consumer (pika)
import json, pika
params = pika.URLParameters("amqp://guest:guest@localhost/%2F")
conn = pika.BlockingConnection(params)
ch = conn.channel()
ch.basic_qos(prefetch_count=100)
def handle(ch, method, props, body):
try:
data = json.loads(body)
# do work with data
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception:
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
ch.basic_consume(queue="jobs", on_message_callback=handle)
ch.start_consuming()
Kafkaではプロデューサのバッチングと圧縮が鍵になる。delivery reportで失敗を捕捉し、再送の方針を決める。以下は高スループット寄りの最小例だ。
# Kafka Producer (confluent-kafka)
from confluent_kafka import Producer
import json, time
conf = {
"bootstrap.servers": "localhost:9092",
"linger.ms": 5,
"batch.size": 65536,
"acks": "all",
"compression.type": "lz4",
"retries": 3,
}
p = Producer(conf)
def dr(err, msg):
if err is not None:
print("delivery failed", err)
for i in range(1000):
payload = json.dumps({"id": i, "ts": time.time()})
p.produce("events", payload.encode("utf-8"), callback=dr)
p.poll(0)
p.flush()
コンシューマはグループとオフセット制御が肝だ。手動コミットにして、処理完了のポイントで確定する。再均衡時のリバランスハンドラも実戦では必須になる。
# Kafka Consumer (confluent-kafka)
from confluent_kafka import Consumer, KafkaException
import json
conf = {
"bootstrap.servers": "localhost:9092",
"group.id": "g1",
"enable.auto.commit": False,
"auto.offset.reset": "earliest",
}
c = Consumer(conf)
c.subscribe(["events"])
try:
while True:
msg = c.poll(1.0)
if msg is None:
continue
if msg.error():
raise KafkaException(msg.error())
data = json.loads(msg.value())
# do work
c.commit(message=msg, asynchronous=False)
finally:
c.close()
Redis Streamsはシンプルで、XADDで書き込み、コンシューマグループで読み出し、処理後にXACKする。AOFのポリシーやレプリケーション設定で耐久性を調整できる⁷⁸。
# Redis Streams Producer/Consumer (redis-py)
from redis import Redis
import json, time
r = Redis(host="localhost", port=6379)
stream = "x:jobs"
for i in range(1000):
r.xadd(stream, {"payload": json.dumps({"id": i, "ts": time.time()})})
# group create (idempotent)
try:
r.xgroup_create(stream, "g1", id="$", mkstream=True)
except Exception:
pass
while True:
res = r.xreadgroup("g1", "c1", {stream: ">"}, count=100, block=1000)
if not res:
continue
for _, entries in res:
for msg_id, fields in entries:
data = json.loads(fields[b"payload"].decode())
# do work
r.xack(stream, "g1", msg_id)
実装上の落とし穴は、冪等性をアプリに押し込む際のキー設計と重複検知の手法に集約される。Kafkaならメッセージキーとトランザクションを活用し²、RabbitMQやRedisでは処理済みキーの短期保持や自然キーのハッシュ化などで重複を回避する。重複は起きる前提にして、起きたときに速やかに無害化するアプローチが現実的だ。
スキーマ進化と可観測性
イベントの寿命が長いほどスキーマ進化が課題になる。KafkaはSchema Registryの有無で体験が激変し、後方互換を維持したまま段階的にフィールドを増やす戦略が取りやすい。RabbitMQやRedisではプロデューサとコンシューマ双方でスキーマのバージョニングを明示し、互換チェックをCIに組み込むと事故が減る。可観測性は共通して重要で、プロデューサ投入レート、滞留長、コンシューマラグ、再試行率、p99をダッシュボード化し、アラートはビジネス指標に結び付けると意思決定が速くなる。
ビジネス価値とTCO:いつ、どれを選ぶか
費用対効果の観点で見ると、Kafkaは初期学習と運用の固定費が高いが、増分コストが低く、イベント数が成長するほど単価が下がる。RabbitMQは導入と日々の運用が軽く、ワークフロー駆動の業務でリードタイム短縮に直結する。Redisは開発と運用のシンプルさが最大の価値で、短期納期や小規模チームに向く。ROIはスループット×期間ではなく、失敗時の回復時間と人的コストを含めて見ると選択が変わってくる。
SaaSのテナント分離を厳密に行いたい場合、Kafkaのパーティションやトピックをテナントキーで切る戦略はスケールに強い¹²。複雑なルーティングや遅延、優先度制御を業務要件として直接書きたいならRabbitMQが速い。キャッシュ、レートリミット、タスクキューを一体で持たせたいならRedisが素直だ。混在構成も有効で、RabbitMQで業務実行を制御し、確定したイベントだけKafkaに流して分析や監査へという分業は現場でよく効く。
SLOとリスク管理から逆算する
目標とするp99や復旧目標時間、データ保持期間、規制対応といった非機能要件から逆算するのが選定の近道になる。長期保持と再生が必要ならKafkaが本命になるし、短い保持と即時性ならRabbitMQやRedisがコストも含めて合理的だ。どの選択でも、冪等性・可観測性・再処理の設計負債は後払い利息が高いので、最初から返済計画をプロダクトの一部として設計しておくと良い。
まとめ:数字で始めて、運用で決める
ベンチマークの数字は選定の出発点として強力だが、最後にものを言うのは、障害が起きたときにどれだけ速く静かに復旧できるかという現実だ。Kafkaは高スループットと履歴再生の強さで、拡大するイベント駆動アーキテクチャの背骨になる¹。RabbitMQは低レイテンシで制御可能性が高く、現場の業務要件を素直にモデル化できる⁵¹⁰¹¹。Redisはミニマルな構成で俊敏に価値を出し、チームの実行速度を押し上げる⁷。あなたのプロダクトにとって何が一番のリスクか、そしてどの指標を守ると顧客価値が最大化するのかを言語化し、小さく計測しながら前に進むのが最短距離だ。もし今まさに選定の岐路にいるなら、ここで挙げた設定とコードをローカルやステージングで再現し、チームのSLOに照らして事実で選んでほしい。
参考文献
- Confluent Blog. Kafka: The Definitive Guide to the Fastest Messaging System. https://www.confluent.io/blog/kafka-fastest-messaging-system/
- Confluent. Introducing Exactly-Once Semantics in Apache Kafka. https://www.confluent.io/online-talk/introducing-exactly-once-semantics-in-apache-kafka/
- Apache Kafka Documentation – Producer Configs (acks, linger.ms, compression). https://kafka.apache.org/documentation/#producerconfigs
- Apache Kafka Documentation – Broker Configs (min.insync.replicas). https://kafka.apache.org/documentation/#brokerconfigs
- RabbitMQ Blog. Quorum Queues and Flow Control: Single-Queue Benchmarks. https://www.rabbitmq.com/blog/2020/05/14/quorum-queues-and-flow-control-single-queue-benchmarks/
- RabbitMQ Blog. Quorum Queues and Why Disks Matter. https://www.rabbitmq.com/blog/2020/04/21/quorum-queues-and-why-disks-matter
- Redis Documentation – Streams (under the hood, consumer groups and PEL). https://redis.io/docs/under-the-hood/streams/
- Redis Documentation – Persistence (AOF appendfsync everysec/always). https://redis.io/docs/management/persistence/
- Apache Kafka Documentation – KRaft mode (KIP-500). https://kafka.apache.org/documentation/#kraft
- RabbitMQ Documentation – Dead Letter Exchanges (DLX). https://www.rabbitmq.com/dlx.html
- RabbitMQ Documentation – Time-To-Live (TTL). https://www.rabbitmq.com/ttl.html
- Apache Kafka Documentation – Consumers, consumer groups and ordering. https://kafka.apache.org/documentation/#intro_consumers