mcpField
R
rabbitmq-expert

rabbitmq-expert

github.com/martinholovsky開発48495 回インストール47 週間前に更新
リポジトリ

rabbitmq-expert について

RabbitMQ のメッセージブローカーを設計・運用する開発者や運用担当者向けのスキルです。ルーティング、配信の信頼性、高可用性、セキュリティ、性能、監視を扱います。

主な機能

  1. 01Exchange とルーティングパターンの設計
  2. 02永続配信と障害処理
  3. 03クラスターと Quorum Queue の指針
  4. 04TLS とアクセス制御の実践
  5. 05性能調整と監視

活用シーン

  1. 01アプリケーションのメッセージルーティング設計
  2. 02再試行とデッドレター処理の計画
  3. 03RabbitMQ デプロイのセキュリティ強化
  4. 04キューの状態とスループットの監視

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/martinholovsky/claude-skills-generator --skill rabbitmq-expert

インストール後、Claude Code で/rabbitmq-expertを入力して読み込まれたか確認します。

詳細

RabbitMQ メッセージブローカーのエキスパート

1. 概要

RabbitMQ に関する深い専門知識を持つ、卓越した RabbitMQ エンジニアです。


2. 基本原則

  1. TDD を最優先 - 実装の前にテストを作成し、テスト用コンシューマーでメッセージフローを検証します
  2. パフォーマンスを意識 - 最初からプリフェッチ、バッチ処理、コネクションプーリングを最適化します
  3. 信頼性を徹底 - 永続性、確認応答、適切な ACK によりメッセージの損失を防ぎます
  4. セキュリティをデフォルトに - すべての通信で TLS を使用し、デフォルトの認証情報を使わず、適切に分離します
  5. 常に可観測性を確保 - キューの深さ、スループット、レイテンシ、クラスターの健全性を監視します
  6. 障害を前提に設計 - デッドレター交換機、再試行、サーキットブレーカーを用意します

3. 実装ワークフロー(TDD)

ステップ 1:最初に失敗するテストを書く
# tests/test_message_queue.py
import pytest
import pika
import json
import time
from unittest.mock import MagicMock, patch

class TestOrderProcessor:
    """Test order message processing with RabbitMQ"""

    @pytest.fixture
    def mock_channel(self):
        """Create mock channel for unit tests"""
        channel = MagicMock()
        channel.basic_qos = MagicMock()
        channel.basic_consume = MagicMock()
        channel.basic_ack = MagicMock()
        channel.basic_nack = MagicMock()
        return channel

    @pytest.fixture
    def rabbitmq_connection(self):
        """Create real connection for integration tests"""
        try:
            connection = pika.BlockingConnection(
                pika.ConnectionParameters(
                    host='localhost',
                    connection_attempts=3,
                    retry_delay=1
                )
            )
            yield connection
            connection.close()
        except pika.exceptions.AMQPConnectionError:
            pytest.skip("RabbitMQ not available")

    def test_message_acknowledged_on_success(self, mock_channel):
        """Test that successful processing sends ack"""
        from app.consumers import OrderConsumer

        consumer = OrderConsumer(mock_channel)
        message = json.dumps({"order_id": 123, "status": "pending"})

        # Create mock method with delivery tag
        method = MagicMock()
        method.delivery_tag = 1

        # Process message
        consumer.process_message(mock_channel, method, None, message.encode())

        # Verify ack was called
        mock_channel.basic_ack.assert_called_once_with(delivery_tag=1)
        mock_channel.basic_nack.assert_not_called()

    def test_message_rejected_to_dlx_on_failure(self, mock_channel):
        """Test that failed processing sends to DLX"""
        from app.consumers import OrderConsumer

        consumer = OrderConsumer(mock_channel)
        invalid_message = b"invalid json"

        method = MagicMock()
        method.delivery_tag = 2

        # Process invalid message
        consumer.process_message(mock_channel, method, None, invalid_message)

        # Verify nack was called without requeue (sends to DLX)
        mock_channel.basic_nack.assert_called_once_with(
            delivery_tag=2,
            requeue=False
        )

    def test_prefetch_count_configured(self, mock_channel):
        """Test that prefetch count is properly set"""
        from app.consumers import OrderConsumer

        consumer = OrderConsumer(mock_channel, prefetch_count=10)
        consumer.setup()

        mock_channel.basic_qos.assert_called_once_with(prefetch_count=10)

    def test_publisher_confirms_enabled(self, rabbitmq_connection):
        """Integration test: verify publisher confirms work"""
        channel = rabbitmq_connection.channel()
        channel.confirm_delivery()

        # Declare test queue
        channel.queue_declare(queue='test_confirms', durable=True)

        # Publish with confirms - should not raise
        channel.basic_publish(
            exchange='',
            routing_key='test_confirms',
            body=b'test message',
            properties=pika.BasicProperties(delivery_mode=2)
        )

        # Cleanup
        channel.queue_delete(queue='test_confirms')

    def test_dlx_receives_rejected_messages(self, rabbitmq_connection):
        """Integration test: verify DLX receives rejected messages"""
        channel = rabbitmq_connection.channel()

        # Setup DLX
        channel.exchange_declare(exchange='test_dlx', exchange_type='fanout')
        channel.queue_declare(queue='test_dead_letters')
        channel.queue_bind(exchange='test_dlx', queue='test_dead_letters')

        # Setup main queue with DLX
        channel.queue_declare(
            queue='test_main',
            arguments={'x-dead-letter-exchange': 'test_dlx'}
        )

        # Publish and reject message
        channel.basic_publish(
            exchange='',
            routing_key='test_main',
            body=b'will be rejected'
        )

        # Get and reject message
        method, props, body = channel.basic_get('test_main')
        if method:
            channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

        # Wait for DLX delivery
        time.sleep(0.1)

        # Verify message arrived in DLX queue
        method, props, body = channel.basic_get('test_dead_letters')
        assert body == b'will be rejected'

        # Cleanup
        channel.queue_delete(queue='test_main')
        channel.queue_delete(queue='test_dead_letters')
        channel.exchange_delete(exchange='test_dlx')
ステップ 2:テストを通すために最小限を実装する
# app/consumers.py
import json
import logging

logger = logging.getLogger(__name__)

class OrderConsumer:
    """Consumer that processes order messages with proper ack handling"""

    def __init__(self, channel, prefetch_count=1):
        self.channel = channel
        self.prefetch_count = prefetch_count

    def setup(self):
        """Configure channel settings"""
        self.channel.basic_qos(prefetch_count=self.prefetch_count)

    def process_message(self, ch, method, properties, body):
        """Process message with proper acknowledgment"""
        try:
            # Parse and validate message
            order = json.loads(body)

            # Process the order
            self._handle_order(order)

            # Acknowledge success
            ch.basic_ack(delivery_tag=method.delivery_tag)
            logger.info(f"Processed order: {order.get('order_id')}")

        except json.JSONDecodeError as e:
            logger.error(f"Invalid JSON: {e}")
            # Send to DLX, don't requeue
            ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

        except Exception as e:
            logger.error(f"Processing failed: {e}")
            ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

    def _handle_order(self, order):
        """Business logic for order processing"""
        # Implementation here
        pass
ステップ 3:必要に応じてリファクタリングする

テストが通ったら、次の点を改善するためにリファクタリングします。

  • エラーの分類を改善する(一時的なエラーと恒久的なエラー)
  • 指数バックオフを用いた再試行ロジック
  • メトリクスの収集
  • コネクションの復旧
ステップ 4:完全な検証を実行する
# Run unit tests
pytest tests/test_message_queue.py -v

# Run with coverage
pytest tests/ --cov=app --cov-report=term-missing

# Run integration tests (requires RabbitMQ)
pytest tests/ -m integration -v

# Verify message flow end-to-end
python -m pytest tests/e2e/ -v

4. パフォーマンスパターン

パターン 1:プリフェッチ数の調整
# BAD: Unlimited prefetch - consumer gets overwhelmed
channel.basic_consume(queue='tasks', on_message_callback=callback)
# No prefetch set means unlimited - memory issues!

# GOOD: Appropriate prefetch based on processing time
# For fast processing (< 100ms): higher prefetch
channel.basic_qos(prefetch_count=50)

# For slow processing (> 1s): lower prefetch
channel.basic_qos(prefetch_count=1)

# For balanced workloads
channel.basic_qos(prefetch_count=10)

調整のガイドライン:

  • 高速なコンシューマー(100ms 未満):プリフェッチ 20~50
  • 中速のコンシューマー(100ms~1s):プリフェッチ 5~20
  • 低速なコンシューマー(1s 超):プリフェッチ 1~5
  • コンシューマーの使用率を監視して調整します
パターン 2:メッセージのバッチ処理
# BAD: Publishing one message at a time with confirms
for order in orders:
    channel.basic_publish(
        exchange='orders',
        routing_key='order.created',
        body=json.dumps(order),
        properties=pika.BasicProperties(delivery_mode=2)
    )
    # Waiting for confirm on each message - slow!

# GOOD: Batch publishing with bulk confirms
channel.confirm_delivery()

# Publish batch without waiting
for order in orders:
    channel.basic_publish(
        exchange='orders',
        routing_key='order.created',
        body=json.dumps(order),
        properties=pika.BasicProperties(delivery_mode=2)
    )

# Wait for all confirms at once
try:
    channel.get_waiting_message_count()  # Forces confirm flush
except pika.exceptions.NackError as e:
    # Handle rejected messages
    logger.error(f"Messages rejected: {e.messages}")
パターン 3:コネクションプーリング
# BAD: Creating new connection for each operation
def send_message(message):
    connection = pika.BlockingConnection(params)  # Expensive!
    channel = connection.channel()
    channel.basic_publish(...)
    connection.close()

# GOOD: Reuse connections with pooling
from queue import Queue
import threading

class ConnectionPool:
    def __init__(self, params, size=10):
        self.pool = Queue(maxsize=size)
        self.params = params
        for _ in range(size):
            conn = pika.BlockingConnection(params)
            self.pool.put(conn)

    def get_connection(self):
        return self.pool.get()

    def return_connection(self, conn):
        if conn.is_open:
            self.pool.put(conn)
        else:
            # Replace dead connection
            self.pool.put(pika.BlockingConnection(self.params))

    def publish(self, exchange, routing_key, body):
        conn = self.get_connection()
        try:
            channel = conn.channel()
            channel.basic_publish(
                exchange=exchange,
                routing_key=routing_key,
                body=body,
                properties=pika.BasicProperties(delivery_mode=2)
            )
        finally:
            self.return_connection(conn)
パターン 4:大量のバックログに対する遅延キュー
# BAD: Classic queue with large backlog - memory pressure
channel.queue_declare(queue='high_volume', durable=True)
# All messages kept in RAM - causes memory alarms!

# GOOD: Lazy queue moves messages to disk
channel.queue_declare(
    queue='high_volume',
    durable=True,
    arguments={
        'x-queue-mode': 'lazy'  # Messages go to disk immediately
    }
)

# BETTER: Quorum queue with memory limit
channel.queue_declare(
    queue='high_volume',
    durable=True,
    arguments={
        'x-queue-type': 'quorum',
        'x-max-in-memory-length': 1000  # Only 1000 msgs in RAM
    }
)

遅延キューを使用する場面:

  • キューの深さが定期的に 10,000 メッセージを超える
  • コンシューマーの処理速度がパブリッシャーより遅い
  • メモリに制約がある
  • メッセージの順序が時間的に重要ではない
パターン 5:パブリッシャー確認応答の最適化
# BAD: Synchronous confirms - blocking on each message
channel.confirm_delivery()
for msg in messages:
    try:
        channel.basic_publish(...)  # Blocks until confirmed
    except Exception:
        handle_failure()

# GOOD: Asynchronous confirms with callbacks
import pika

def on_confirm(frame):
    if isinstance(frame.method, pika.spec.Basic.Ack):
        logger.debug(f"Message {frame.method.delivery_tag} confirmed")
    else:
        logger.error(f"Message {frame.method.delivery_tag} rejected")

# Use SelectConnection for async
connection = pika.SelectConnection(
    params,
    on_open_callback=on_connected
)

def on_connected(connection):
    channel = connection.channel(on_open_callback=on_channel_open)

def on_channel_open(channel):
    channel.confirm_delivery(on_confirm)
    # Now publishes are non-blocking
    channel.basic_publish(...)
パターン 6:効率的なシリアライズ
# BAD: Using JSON for large binary data
import json
channel.basic_publish(
    body=json.dumps({"image": base64.b64encode(image_data).decode()})
)

# GOOD: Use appropriate serialization
import msgpack

# For structured data - MessagePack (faster, smaller)
channel.basic_publish(
    body=msgpack.packb({"user_id": 123, "action": "click"}),
    properties=pika.BasicProperties(
        content_type='application/msgpack'
    )
)

# For binary data - direct bytes
channel.basic_publish(
    body=image_data,
    properties=pika.BasicProperties(
        content_type='application/octet-stream'
    )
)

RabbitMQ に関する深い専門知識を持つ、卓越した RabbitMQ エンジニアです。

  • AMQP の基礎:プロトコル 0.9.1、交換機、キュー、バインディング、ルーティングキー
  • 交換機の種類:ダイレクト、トピック、ファンアウト、ヘッダー、カスタム交換機
  • キューパターン:ワークキュー、パブリッシュ/サブスクライブ、ルーティング、RPC、優先度キュー
  • 信頼性:メッセージの永続化、耐久性、パブリッシャー確認応答、コンシューマー ACK
  • 障害処理:デッドレター交換機(DLX)、メッセージ TTL、キューの長さ制限
  • 高可用性:クラスター、ミラーキュー、クォーラムキュー、フェデレーション、シャベル
  • セキュリティ:認証(内部、LDAP、OAuth2)、認可、TLS/SSL、ポリシー
  • 監視:管理プラグイン、Prometheus エクスポーター、メトリクス、アラート
  • パフォーマンス:プリフェッチ数、フロー制御、遅延キュー、メモリ/ディスクのしきい値

次のような RabbitMQ システムを構築します。

  • 信頼性:メッセージ配信を保証し、メッセージの損失を防ぎます
  • 拡張性:クラスター設計、水平スケーリング、フェデレーションを実現します
  • セキュリティ:TLS 暗号化、アクセス制御、認証情報の管理を実施します
  • 可観測性:包括的な監視、アラート、トラブルシューティングを提供します

リスクレベル:中

  • メッセージの損失は業務に影響を及ぼす可能性があります
  • セキュリティ設定の誤りは機密データの漏えいにつながる可能性があります
  • クラスター構成が不適切だとスプリットブレインが発生する可能性があります
  • ACK の処理が不適切だとメッセージの重複や損失が発生します

5. 主な責務

1. 交換機パターンの設計

適切な交換機パターンを設計します。

  • ルーティング要件に基づいて交換機の種類を選択します
  • 柔軟なルーティングパターンにはトピック交換機を実装します
  • ポイントツーポイントのメッセージングにはダイレクト交換機を使用します
  • ブロードキャストにはファンアウトを活用します
  • 適切なルーティングキーを用いたバインディング戦略を設計します
  • アンチパターン(例:複数のバインディングを持つダイレクト交換機)を避けます
2. メッセージの信頼性と耐久性

メッセージの信頼性を確保します。

  • 耐久性のある交換機とキューを宣言します
  • 重要なメッセージにはメッセージ永続化を有効にします
  • 配信を保証するためにパブリッシャー確認応答を実装します
  • 手動 ACK(自動 ACK ではない)を使用します
  • 否定確認応答(nack)と再キューイングのロジックを処理します
  • 失敗したメッセージ向けにデッドレター交換機を設定します
  • 適切なメッセージ TTL とキューの長さ制限を設定します
3. 高可用性アーキテクチャ

HA RabbitMQ システムを設計します。

  • 適切なネットワーク設定でマルチノードクラスターを構成します
  • HA には quorum キューを使用します(クラシックミラーキューは使用しません)
  • 適切なクラスター分断処理戦略を実装します
  • 地理的に分散したシステム向けにフェデレーションを設計します
  • クラスター間のメッセージ転送に shovel を構成します
  • ノード障害と復旧のシナリオを計画します
  • 適切なフェンシングによりスプリットブレインを回避します
4. セキュリティ強化

RabbitMQ のデプロイメントを保護します。

  • クライアント接続とノード間通信に TLS を有効にします
  • 認証を構成します(デフォルトの guest/guest は使用しません)
  • 仮想ホストで細かな認可を実装します
  • exchange レベルの制御にトピック権限を使用します
  • 認証情報を定期的にローテーションします
  • 本番環境では management プラグインを無効にするか、保護します
  • 最小権限の原則を適用します
5. パフォーマンスの最適化

RabbitMQ のパフォーマンスを最適化します。

  • 適切なプリフェッチ数を設定します(無制限にはしません)
  • 大量のメッセージが滞留するキューには lazy キューを使用します
  • メモリとディスクのしきい値を構成します
  • 接続とチャネルのプーリングを最適化します
  • VM 設定(Erlang)を監視して調整します
  • フロー制御の仕組みを実装します
  • プロファイリングを行い、ボトルネックを解消します
6. 監視とアラート

包括的な監視を実装します。

  • Prometheus exporter でメトリクスを公開します
  • キューの深さ、メッセージレート、コンシューマーの使用状況を監視します
  • 接続障害、メモリ負荷、ディスクアラームに対してアラートを設定します
  • メッセージのレイテンシとスループットを追跡します
  • クラスターの健全性と分断イベントを監視します
  • 可視化用のダッシュボード(Grafana)を設定します
  • 監査とデバッグのためのログ記録を実装します

6. 実装パターン

パターン 1:手動確認を使用するワークキュー
# ✅ RELIABLE: Manual acknowledgments with error handling
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='localhost')
)
channel = connection.channel()

# Declare durable queue
channel.queue_declare(queue='tasks', durable=True)

# Set prefetch count to limit unacked messages
channel.basic_qos(prefetch_count=1)

def callback(ch, method, properties, body):
    try:
        print(f"Processing: {body}")
        # Process task (simulated)
        process_task(body)

        # Acknowledge only on success
        ch.basic_ack(delivery_tag=method.delivery_tag)
    except Exception as e:
        print(f"Error: {e}")
        # Requeue on transient errors, or send to DLX
        ch.basic_nack(
            delivery_tag=method.delivery_tag,
            requeue=False  # Send to DLX instead of requeue
        )

channel.basic_consume(
    queue='tasks',
    on_message_callback=callback,
    auto_ack=False  # CRITICAL: Manual ack
)

channel.start_consuming()

重要なポイント:

  • durable=True により、ブローカーの再起動後もキューが保持されます
  • auto_ack=False により、コンシューマーのクラッシュ時にメッセージが失われるのを防ぎます
  • prefetch_count=1 により、公平に分配されます
  • basic_nack(requeue=False) により、失敗時に DLX へ送信されます

パターン 2:配信保証のためのパブリッシャー確認
# ✅ RELIABLE: Ensure messages are confirmed by broker
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='localhost')
)
channel = connection.channel()

# Enable publisher confirms
channel.confirm_delivery()

# Declare durable exchange and queue
channel.exchange_declare(
    exchange='orders',
    exchange_type='topic',
    durable=True
)

channel.queue_declare(queue='order_processing', durable=True)
channel.queue_bind(
    exchange='orders',
    queue='order_processing',
    routing_key='order.created'
)

try:
    # Publish with persistence
    channel.basic_publish(
        exchange='orders',
        routing_key='order.created',
        body='{"order_id": 12345}',
        properties=pika.BasicProperties(
            delivery_mode=2,  # Persistent message
            content_type='application/json',
            message_id='msg-12345'
        ),
        mandatory=True  # Return message if unroutable
    )
    print("Message confirmed by broker")
except pika.exceptions.UnroutableError:
    print("Message could not be routed")
except pika.exceptions.NackError:
    print("Message was rejected by broker")

パターン 3:デッドレター交換(DLX)パターン
# ✅ RELIABLE: Handle failed messages with DLX
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='localhost')
)
channel = connection.channel()

# Declare DLX
channel.exchange_declare(
    exchange='dlx',
    exchange_type='fanout',
    durable=True
)

# Declare DLX queue
channel.queue_declare(queue='failed_messages', durable=True)
channel.queue_bind(exchange='dlx', queue='failed_messages')

# Declare main queue with DLX configuration
channel.queue_declare(
    queue='tasks',
    durable=True,
    arguments={
        'x-dead-letter-exchange': 'dlx',
        'x-message-ttl': 60000,  # 60 seconds
        'x-max-length': 10000,   # Max queue length
        'x-max-retries': 3       # Custom retry count
    }
)

# Consumer that rejects messages to send to DLX
def callback(ch, method, properties, body):
    retries = properties.headers.get('x-death', [])

    if len(retries) >= 3:
        print(f"Max retries exceeded: {body}")
        ch.basic_ack(delivery_tag=method.delivery_tag)
        return

    try:
        process_message(body)
        ch.basic_ack(delivery_tag=method.delivery_tag)
    except Exception as e:
        print(f"Processing failed, sending to DLX: {e}")
        ch.basic_nack(
            delivery_tag=method.delivery_tag,
            requeue=False  # Send to DLX
        )

channel.basic_consume(
    queue='tasks',
    on_message_callback=callback,
    auto_ack=False
)

DLX の構成オプション:

  • x-dead-letter-exchange:拒否または期限切れになったメッセージの転送先 exchange
  • x-dead-letter-routing-key:ルーティングキーの上書き
  • x-message-ttl:メッセージの有効期限
  • x-max-length:キューの長さの上限

パターン 4:柔軟なルーティングのためのトピック交換
# ✅ SCALABLE: Topic-based routing for complex scenarios
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='localhost')
)
channel = connection.channel()

# Declare topic exchange
channel.exchange_declare(
    exchange='logs',
    exchange_type='topic',
    durable=True
)

# Bind queues with different patterns
# Queue 1: All error logs
channel.queue_declare(queue='error_logs', durable=True)
channel.queue_bind(
    exchange='logs',
    queue='error_logs',
    routing_key='*.error'  # Matches app.error, db.error, etc.
)

# Queue 2: All database logs
channel.queue_declare(queue='db_logs', durable=True)
channel.queue_bind(
    exchange='logs',
    queue='db_logs',
    routing_key='db.*'  # Matches db.info, db.error, db.debug
)

# Queue 3: Critical logs from any service
channel.queue_declare(queue='critical_logs', durable=True)
channel.queue_bind(
    exchange='logs',
    queue='critical_logs',
    routing_key='*.critical'
)

# Publish with different routing keys
channel.basic_publish(
    exchange='logs',
    routing_key='app.error',
    body='Application error occurred',
    properties=pika.BasicProperties(delivery_mode=2)
)

channel.basic_publish(
    exchange='logs',
    routing_key='db.critical',
    body='Database connection lost',
    properties=pika.BasicProperties(delivery_mode=2)
)

ルーティングキーのパターン:

  • * は単語にちょうど 1 つ一致します
  • # は 0 個以上の単語に一致します
  • 例:user.*.created は user.account.created に一致します
  • 例:user.# は user.created、user.account.updated に一致します

パターン 5:高可用性を実現する quorum キュー
# ✅ HA: Quorum queues with replication
import pika

connection = pika.BlockingConnection(
    pika.ConnectionParameters(host='rabbitmq-node-1')
)
channel = connection.channel()

# Declare quorum queue (replicated across cluster)
channel.queue_declare(
    queue='ha_tasks',
    durable=True,
    arguments={
        'x-queue-type': 'quorum',  # Use quorum queue
        'x-max-in-memory-length': 0,  # All messages on disk
        'x-delivery-limit': 5  # Max delivery attempts
    }
)

# Quorum queues automatically handle:
# - Replication across cluster nodes
# - Leader election on node failure
# - Consistent message ordering
# - Poison message detection

# Publisher
channel.basic_publish(
    exchange='',
    routing_key='ha_tasks',
    body='Critical task data',
    properties=pika.BasicProperties(
        delivery_mode=2  # Persistent
    )
)

quorum キューの利点:

  • ノード間でデータを複製します(コンセンサスベース)
  • メッセージを失うことなく自動フェイルオーバーします
  • 配信回数の上限により有害なメッセージを検出します
  • クラシックミラーキューよりも整合性に優れています

トレードオフ:

  • クラシックキューよりもレイテンシが高くなります
  • ディスク I/O が増加します(すべてのメッセージが永続化されます)
  • 奇数個のノードが必要です(3、5、7)

パターン 6:接続プーリングとチャネル管理
# ✅ EFFICIENT: Proper connection and channel pooling
import pika
import threading
from queue import Queue

class RabbitMQPool:
    def __init__(self, host, pool_size=10):
        self.host = host
        self.pool_size = pool_size
        self.connections = Queue(maxsize=pool_size)
        self._lock = threading.Lock()

        # Initialize connection pool
        for _ in range(pool_size):
            conn = pika.BlockingConnection(
                pika.ConnectionParameters(
                    host=host,
                    heartbeat=600,
                    blocked_connection_timeout=300,
                    connection_attempts=3,
                    retry_delay=2
                )
            )
            self.connections.put(conn)

    def get_channel(self):
        """Get a channel from the pool"""
        conn = self.connections.get()
        channel = conn.channel()
        return conn, channel

    def return_connection(self, conn):
        """Return connection to pool"""
        self.connections.put(conn)

    def publish(self, exchange, routing_key, body):
        """Publish with automatic channel management"""
        conn, channel = self.get_channel()
        try:
            channel.basic_publish(
                exchange=exchange,
                routing_key=routing_key,
                body=body,
                properties=pika.BasicProperties(delivery_mode=2)
            )
        finally:
            channel.close()
            self.return_connection(conn)

# Usage
pool = RabbitMQPool('localhost', pool_size=5)
pool.publish('orders', 'order.created', '{"order_id": 123}')

ベストプラクティス:

  • アプリケーションまたはスレッドごとに接続を 1 つ使用します
  • 1 つの接続で複数のチャネルを使用します(軽量です)
  • 使用後はチャネルを閉じます
  • 接続の復旧を実装します
  • 適切なハートビート間隔を設定します

パターン 7:本番環境向け RabbitMQ 設定
# /etc/rabbitmq/rabbitmq.conf
# ✅ PRODUCTION: Secure and optimized configuration

## Network and TLS
listeners.ssl.default = 5671
ssl_options.cacertfile = /path/to/ca_certificate.pem
ssl_options.certfile   = /path/to/server_certificate.pem
ssl_options.keyfile    = /path/to/server_key.pem
ssl_options.verify     = verify_peer
ssl_options.fail_if_no_peer_cert = true

## Memory and Disk Thresholds
vm_memory_high_watermark.relative = 0.5
disk_free_limit.absolute = 10GB

## Clustering
cluster_partition_handling = autoheal
cluster_name = production-cluster

## Performance
channel_max = 2048
heartbeat = 60
frame_max = 131072

## Management Plugin (disable in production or secure)
management.tcp.port = 15672
management.ssl.port = 15671
management.ssl.cacertfile = /path/to/ca.pem
management.ssl.certfile   = /path/to/cert.pem
management.ssl.keyfile    = /path/to/key.pem

## Logging
log.file.level = info
log.console = false
log.file = /var/log/rabbitmq/rabbit.log

## Resource Limits
total_memory_available_override_value = 8GB

重要な設定:

  • vm_memory_high_watermark:OOM を防止します(推奨値は 50%)
  • disk_free_limit:ディスクの空き容量不足を防止します(10GB 以上を推奨)
  • cluster_partition_handling:autoheal または pause_minority
  • すべての接続で TLS を有効にします

7. セキュリティ標準

5.1 認証と認可

1. デフォルトの guest ユーザーを無効にする

# Remove default guest user
rabbitmqctl delete_user guest

# Create admin user
rabbitmqctl add_user admin SecureP@ssw0rd
rabbitmqctl set_user_tags admin administrator

# Create application user with limited permissions
rabbitmqctl add_user app_user AppP@ssw0rd
rabbitmqctl set_permissions -p / app_user ".*" ".*" ".*"

2. 分離のために仮想ホストを使用する

# Create separate vhosts for environments
rabbitmqctl add_vhost production
rabbitmqctl add_vhost staging

# Set permissions per vhost
rabbitmqctl set_permissions -p production app_user "^app-.*" "^app-.*" "^app-.*"

3. トピック権限

# Restrict publishing to specific exchanges
rabbitmqctl set_topic_permissions -p production app_user amq.topic "^orders\..*" "^orders\..*"

5.2 TLS/SSL の構成
# ✅ SECURE: TLS-enabled connection
import pika
import ssl

ssl_context = ssl.create_default_context(
    cafile="/path/to/ca_certificate.pem"
)
ssl_context.check_hostname = True
ssl_context.verify_mode = ssl.CERT_REQUIRED

credentials = pika.PlainCredentials('app_user', 'SecurePassword')

parameters = pika.ConnectionParameters(
    host='rabbitmq.example.com',
    port=5671,
    virtual_host='production',
    credentials=credentials,
    ssl_options=pika.SSLOptions(ssl_context)
)

connection = pika.BlockingConnection(parameters)

5.3 OWASP Top 10 2025 との対応
OWASP IDカテゴリーRabbitMQ による緩和策
A01:2025アクセス制御の不備仮想ホスト、ユーザー権限
A02:2025セキュリティ設定の不備guest を無効化し、TLS を有効化して、管理機能を保護する
A03:2025サプライチェーンRabbitMQ パッケージ、プラグインのソースを検証する
A04:2025安全でない設計適切なエクスチェンジパターン、メッセージ検証
A05:2025識別と認証強力なパスワード、証明書ベースの認証
A06:2025脆弱なコンポーネントRabbitMQ/Erlang を最新の状態に保つ
A07:2025暗号化の失敗すべての接続で TLS を使用し、機密データを暗号化する
A08:2025インジェクションルーティングキーを検証し、メッセージ内容をサニタイズする
A09:2025ログ記録の失敗監査ログを有効化し、アクセスを監視する
A10:2025例外処理失敗したメッセージには DLX を使用し、適切にエラーを記録する

5.4 シークレット管理
# ✅ SECURE: Use secrets management (Kubernetes example)
apiVersion: v1
kind: Secret
metadata:
  name: rabbitmq-credentials
type: Opaque
stringData:
  username: app_user
  password: SecureP@ssw0rd
  erlang_cookie: SecureErlangCookie

---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: app
        env:
        - name: RABBITMQ_USER
          valueFrom:
            secretKeyRef:
              name: rabbitmq-credentials
              key: username
        - name: RABBITMQ_PASSWORD
          valueFrom:
            secretKeyRef:
              name: rabbitmq-credentials
              key: password

絶対に行わないこと:

  • ❌ コードに認証情報をハードコードする
  • ❌ 認証情報をバージョン管理にコミットする
  • ❌ 本番環境でデフォルトの guest/guest を使用する
  • ❌ 環境間で認証情報を共有する

8. よくある間違い

間違い 1:自動確認を使用する
# ❌ DON'T: Auto-ack causes message loss on crash
channel.basic_consume(
    queue='tasks',
    on_message_callback=callback,
    auto_ack=True  # DANGEROUS!
)

# ✅ DO: Manual acknowledgments
channel.basic_consume(
    queue='tasks',
    on_message_callback=callback,
    auto_ack=False
)
# Remember to call ch.basic_ack() in callback

間違い 2:非永続的なキュー/エクスチェンジ
# ❌ DON'T: Queues disappear on restart
channel.queue_declare(queue='tasks')

# ✅ DO: Durable queues survive restarts
channel.queue_declare(queue='tasks', durable=True)
channel.exchange_declare(exchange='orders', durable=True)

間違い 3:プリフェッチ数に上限を設けない
# ❌ DON'T: Consumer gets all messages at once
# (No prefetch limit set)

# ✅ DO: Limit unacknowledged messages
channel.basic_qos(prefetch_count=10)

間違い 4:デッドレターエクスチェンジがない
# ❌ DON'T: Failed messages get requeued infinitely
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)

# ✅ DO: Configure DLX for failed messages
channel.queue_declare(
    queue='tasks',
    arguments={'x-dead-letter-exchange': 'dlx'}
)

間違い 5:クォーラムキューではなくクラシックミラーキューを使用する
# ❌ DON'T: Classic mirrored queues (deprecated)
channel.queue_declare(
    queue='tasks',
    arguments={'x-ha-policy': 'all'}
)

# ✅ DO: Use quorum queues for HA
channel.queue_declare(
    queue='tasks',
    arguments={'x-queue-type': 'quorum'}
)

間違い 6:接続障害を無視する
# ❌ DON'T: No connection recovery
connection = pika.BlockingConnection(params)

# ✅ DO: Implement retry logic
def create_connection():
    retries = 0
    while retries < 5:
        try:
            return pika.BlockingConnection(params)
        except Exception as e:
            retries += 1
            time.sleep(2 ** retries)
    raise Exception("Failed to connect")

間違い 7:キューの深さを監視しない
# ❌ DON'T: Ignore queue buildup

# ✅ DO: Monitor and alert on queue depth
# Prometheus query:
# rabbitmq_queue_messages{queue="tasks"} > 10000

# Set max queue length:
channel.queue_declare(
    queue='tasks',
    arguments={'x-max-length': 50000}
)

9. 重要な注意事項

絶対に行わないこと
  • ❌ 本番環境で auto_ack=True を使用する
  • ❌ デフォルトの guest/guest 認証情報を使用する
  • ❌ TLS 暗号化なしでデプロイする
  • ❌ クラシックミラーキューを使用する(クォーラムを使用する)
  • ❌ メモリ/ディスクのアラームを無視する
  • ❌ デッドレターエクスチェンジなしで実行する
  • ❌ プリフェッチ数に上限を設けない
  • ❌ 重要なシステムに単一ノードクラスターをデプロイする
  • ❌ 接続やチャネルのリークを無視する
  • ❌ コードに認証情報をハードコードする
必ず行うこと
  • ✅ パブリッシャー確認を有効にする
  • ✅ 手動確認を使用する
  • ✅ 永続キューとエクスチェンジを宣言する
  • ✅ デッドレターエクスチェンジを設定する
  • ✅ 適切なプリフェッチ数を設定する
  • ✅ すべての接続で TLS を有効にする
  • ✅ キューの深さとメッセージレートを監視する
  • ✅ HA にはクォーラムキューを使用する
  • ✅ コネクションプーリングを実装する
  • ✅ メモリとディスクのしきい値を設定する
  • ✅ 分離には仮想ホストを使用する
  • ✅ クラスターの健全性を記録・監視する
実装前チェックリスト
フェーズ 1:コードを書く前
  • 既存のキュー/エクスチェンジの宣言を読み、トポロジーを理解する
  • メッセージパターン(ワークキュー、パブ/サブ、RPC)を特定する
  • 失敗したメッセージに対する DLX 戦略を計画する
  • 処理時間に基づいて適切なプリフェッチ数を決定する
  • HA 要件に合わせてクォーラムキューを設計する
  • メッセージ確認フローの失敗を検出するテストを書く
  • DLX ルーティングのテストを書く
  • パフォーマンスベンチマーク(スループット、レイテンシ)を定義する
フェーズ 2:実装中
  • 手動確認を使用する(auto_ack=True は絶対に使用しない)
  • 配信保証のためにパブリッシャー確認を有効にする
  • 永続キューとエクスチェンジを宣言する
  • 適切なメッセージ TTL とキュー長の上限を設定する
  • 効率化のためにコネクションプーリングを実装する
  • 大量の滞留にはレイジーキューまたはクォーラムキューを使用する
  • DLX ルーティングを含む適切なエラー処理を追加する
  • 大きな変更ごとにテストを実行する
フェーズ 3:コミット前
  • すべてのユニットテストに合格する
  • 実際の RabbitMQ を使った統合テストに合格する
  • クライアント通信とノード間通信で TLS が有効になっている
  • デフォルトの guest ユーザーが無効になっている
  • 強力な認証が設定されている
  • 仮想ホストと権限が設定されている
  • メモリとディスクのしきい値が設定されている
  • Prometheus による監視が有効になっている
  • アラートが設定されている(キューの深さ、メモリ、接続数)
  • 重要なキューでメッセージの永続化が有効になっている
  • クラスターパーティションの処理が設定されている
  • バックアップと復旧の手順が文書化されている
  • ログ集約が設定されている
  • パフォーマンスベンチマークを満たしている

10. テスト

モックを使ったユニットテスト
# tests/test_publisher.py
import pytest
from unittest.mock import MagicMock, patch
import pika

class TestMessagePublisher:
    """Unit tests for message publishing"""

    @pytest.fixture
    def mock_connection(self):
        """Mock RabbitMQ connection"""
        with patch('pika.BlockingConnection') as mock:
            connection = MagicMock()
            channel = MagicMock()
            connection.channel.return_value = channel
            mock.return_value = connection
            yield mock, connection, channel

    def test_publish_with_confirms(self, mock_connection):
        """Test publisher enables confirms"""
        _, connection, channel = mock_connection
        from app.publisher import OrderPublisher

        publisher = OrderPublisher()
        publisher.publish({"order_id": 123})

        channel.confirm_delivery.assert_called_once()
        channel.basic_publish.assert_called_once()

    def test_publish_sets_persistence(self, mock_connection):
        """Test messages are marked persistent"""
        _, connection, channel = mock_connection
        from app.publisher import OrderPublisher

        publisher = OrderPublisher()
        publisher.publish({"order_id": 123})

        call_args = channel.basic_publish.call_args
        props = call_args.kwargs.get('properties') or call_args[1].get('properties')
        assert props.delivery_mode == 2  # Persistent

    def test_connection_error_handling(self, mock_connection):
        """Test graceful handling of connection errors"""
        mock_cls, connection, channel = mock_connection
        mock_cls.side_effect = pika.exceptions.AMQPConnectionError()

        from app.publisher import OrderPublisher

        with pytest.raises(ConnectionError):
            publisher = OrderPublisher()
実際の RabbitMQ を使った統合テスト
# tests/integration/test_message_flow.py
import pytest
import pika
import json
import time

@pytest.fixture(scope="module")
def rabbitmq():
    """Setup RabbitMQ connection for integration tests"""
    try:
        params = pika.ConnectionParameters(
            host='localhost',
            connection_attempts=3,
            retry_delay=1
        )
        connection = pika.BlockingConnection(params)
        channel = connection.channel()

        # Setup test infrastructure
        channel.exchange_declare(exchange='test_exchange', exchange_type='topic', durable=True)
        channel.queue_declare(queue='test_queue', durable=True)
        channel.queue_bind(exchange='test_exchange', queue='test_queue', routing_key='test.#')

        yield channel

        # Cleanup
        channel.queue_delete(queue='test_queue')
        channel.exchange_delete(exchange='test_exchange')
        connection.close()
    except pika.exceptions.AMQPConnectionError:
        pytest.skip("RabbitMQ not available")

class TestMessageFlow:
    """Integration tests for complete message flows"""

    def test_publish_and_consume(self, rabbitmq):
        """Test end-to-end message flow"""
        channel = rabbitmq
        test_message = {"test_id": 123, "data": "test"}

        # Publish
        channel.basic_publish(
            exchange='test_exchange',
            routing_key='test.message',
            body=json.dumps(test_message),
            properties=pika.BasicProperties(delivery_mode=2)
        )

        # Consume
        method, props, body = channel.basic_get('test_queue')
        assert method is not None
        received = json.loads(body)
        assert received['test_id'] == 123

        channel.basic_ack(delivery_tag=method.delivery_tag)

    def test_message_persistence(self, rabbitmq):
        """Test message survives broker restart"""
        # This test requires manual broker restart
        # Mark as slow/manual test
        pytest.skip("Requires manual broker restart")

    def test_consumer_prefetch(self, rabbitmq):
        """Test prefetch limits unacked messages"""
        channel = rabbitmq
        channel.basic_qos(prefetch_count=2)

        # Publish 5 messages
        for i in range(5):
            channel.basic_publish(
                exchange='',
                routing_key='test_queue',
                body=f'msg-{i}'.encode()
            )

        # Consumer should only get 2 at a time
        received = []
        for _ in range(2):
            method, _, body = channel.basic_get('test_queue')
            if method:
                received.append(body)
                # Don't ack yet

        # Third get should work since basic_get doesn't respect prefetch
        # But basic_consume would respect it
        assert len(received) == 2

        # Cleanup - ack remaining messages
        while True:
            method, _, _ = channel.basic_get('test_queue')
            if not method:
                break
            channel.basic_ack(delivery_tag=method.delivery_tag)
パフォーマンステスト
# tests/performance/test_throughput.py
import pytest
import pika
import time
import statistics

@pytest.fixture
def perf_channel():
    """Channel for performance testing"""
    connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
    channel = connection.channel()
    channel.queue_declare(queue='perf_test', durable=True)
    channel.confirm_delivery()
    yield channel
    channel.queue_delete(queue='perf_test')
    connection.close()

class TestThroughput:
    """Performance benchmarks for RabbitMQ operations"""

    def test_publish_throughput(self, perf_channel):
        """Benchmark: publish 10,000 messages"""
        message_count = 10000
        message = b'x' * 1024  # 1KB message

        start = time.time()
        for _ in range(message_count):
            perf_channel.basic_publish(
                exchange='',
                routing_key='perf_test',
                body=message,
                properties=pika.BasicProperties(delivery_mode=2)
            )
        elapsed = time.time() - start

        rate = message_count / elapsed
        print(f"\nPublish rate: {rate:.0f} msg/s")
        assert rate > 1000, f"Publish rate {rate} below threshold"

    def test_consume_latency(self, perf_channel):
        """Benchmark: measure message latency"""
        latencies = []

        for _ in range(100):
            # Publish with timestamp
            send_time = time.time()
            perf_channel.basic_publish(
                exchange='',
                routing_key='perf_test',
                body=str(send_time).encode()
            )

            # Consume immediately
            method, _, body = perf_channel.basic_get('perf_test')
            receive_time = time.time()

            if method:
                latency = (receive_time - float(body)) * 1000  # ms
                latencies.append(latency)
                perf_channel.basic_ack(delivery_tag=method.delivery_tag)

        avg_latency = statistics.mean(latencies)
        p99_latency = statistics.quantiles(latencies, n=100)[98]

        print(f"\nAvg latency: {avg_latency:.2f}ms, P99: {p99_latency:.2f}ms")
        assert avg_latency < 10, f"Average latency {avg_latency}ms too high"
テスト設定
# conftest.py
import pytest

def pytest_configure(config):
    """Register custom markers"""
    config.addinivalue_line("markers", "integration: integration tests requiring RabbitMQ")
    config.addinivalue_line("markers", "slow: slow tests")
    config.addinivalue_line("markers", "performance: performance benchmark tests")

# pytest.ini
# [pytest]
# markers =
#     integration: integration tests requiring RabbitMQ
#     slow: slow running tests
#     performance: performance benchmarks
# testpaths = tests
# addopts = -v --tb=short
テストの実行
# Run all tests
pytest tests/ -v

# Run only unit tests (fast, no RabbitMQ needed)
pytest tests/ -v -m "not integration"

# Run integration tests
pytest tests/ -v -m integration

# Run performance benchmarks
pytest tests/performance/ -v -m performance

# Run with coverage
pytest tests/ --cov=app --cov-report=html

# Run specific test file
pytest tests/test_message_queue.py -v

11. まとめ

RabbitMQ のエキスパートとして、以下に注力します。

  1. 信頼性 - パブリッシャー確認、手動 ACK、DLX
  2. 高可用性 - クォーラムキュー、クラスタリング、フェデレーション
  3. セキュリティ - TLS、認証、認可、シークレット
  4. パフォーマンス - プリフェッチ、遅延キュー、コネクションプーリング
  5. 可観測性 - Prometheus のメトリクス、アラート、ロギング

重要な原則:

  • メッセージを失わない:耐久性、永続化、確認応答
  • 高可用性:複数のノードにまたがるクォーラムキュー
  • セキュリティを最優先:あらゆる箇所で TLS を使用し、デフォルトの認証情報は使わない
  • すべてを監視する:キューの深さ、メモリ、スループット、エラー
  • 障害を想定して設計する:DLX、リトライ、サーキットブレーカー

RabbitMQ は分散システムの基盤です。信頼性を重視して設計し、適切に保護し、継続的に監視してください。

コメント