RabbitMQ в Docker Compose: поднимаем брокер за 2 минуты

RabbitMQ в Docker Compose: поднимаем брокер

RabbitMQ - один из самых популярных message broker'ов. Поддерживает AMQP 0-9-1, а для Go есть отличная библиотека amqp091-go. См. также bus vs broker.

Docker Compose: минимальный сетап

services:
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: rabbitmq
    ports:
 - "5672:5672"    # AMQP протокол
 - "15672:15672"  # Management UI
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    volumes:
 - rabbitmq_data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  rabbitmq_data:

После docker compose up -d админка доступна на http://localhost:15672 (guest/guest).

Без healthcheck зависимые сервисы могут стартовать раньше, чем RabbitMQ готов принимать соединения. `rabbitmq-diagnostics ping` проверяет именно готовность AMQP, а не просто что процесс запущен.

Три ключевых понятия

Topic exchange распределяет одно сообщение по очередям по routing key

┌──────────┐    routing key    ┌───────────┐
│ Producer │ ──────────────→  │ Exchange  │
└──────────┘                   └─────┬─────┘
                                     │ binding
                               ┌─────▼─────┐
                               │   Queue    │ ──→ Consumer
                               └───────────┘
  • Exchange - принимает сообщения от producer и маршрутизирует их в очереди
  • Queue - хранит сообщения до обработки consumer'ом
  • Routing key - ключ маршрутизации: exchange решает, в какую очередь отправить

Producer никогда не отправляет напрямую в queue - всегда через exchange.

Типы Exchange

Тип        Маршрутизация                    Пример
────────   ──────────────────────────       ─────────────────────
direct     Точное совпадение routing key    routing_key="order.created"
                                            → queue с таким же binding

topic      Паттерн с * и #                 "lesson.*" → lesson.completed,
                                                         lesson.started
                                           "*.error"  → payment.error,
                                                         auth.error

fanout     Во ВСЕ привязанные очереди      Broadcast: уведомления,
           (routing key игнорируется)       логирование

headers    По заголовкам сообщения          Редко используется

Для event-driven архитектуры topic exchange - самый гибкий выбор.

Topic Exchange: паттерны маршрутизации

Символ   Значение              Пример
──────   ──────────            ─────────────────────
*        Одно слово            lesson.* → lesson.completed ✓
                                        → lesson.started ✓
                                        → lesson.quiz.passed ✗

#        Ноль или более слов   lesson.# → lesson.completed
                                        → lesson.quiz.passed ✓
                                        → lesson ✓

Пример конфигурации:

Exchange: events (topic)
├── Binding: lesson.*       → queue: progress-worker
├── Binding: lesson.*       → queue: analytics-worker
├── Binding: *.completed    → queue: achievements-worker
└── Binding: #              → queue: audit-log (всё)

Настройка из кода

import amqp "github.com/rabbitmq/amqp091-go"

func SetupRabbitMQ(conn *amqp.Connection) (*amqp.Channel, error) {
    ch, err := conn.Channel()
    if err != nil {
        return nil, fmt.Errorf("open channel: %w", err)
    }

    // Создаём exchange
    err = ch.ExchangeDeclare(
        "events",  // name
        "topic",   // type
        true,      // durable (переживёт рестарт)
        false,     // auto-deleted
        false,     // internal
        false,     // no-wait
        nil,       // arguments
    )
    if err != nil {
        return nil, fmt.Errorf("declare exchange: %w", err)
    }

    // Создаём очереди
    queues := []struct {
        name       string
        routingKey string
    }{
        {"progress-worker", "lesson.*"},
        {"analytics-worker", "lesson.*"},
        {"email-worker", "lesson.completed"},
    }

    for _, q := range queues {
        queue, err := ch.QueueDeclare(
            q.name,
            true,  // durable
            false, // auto-delete
            false, // exclusive
            false, // no-wait
            nil,
        )
        if err != nil {
            return nil, fmt.Errorf("declare queue %s: %w", q.name, err)
        }

        err = ch.QueueBind(
            queue.Name,
            q.routingKey,
            "events",
            false,
            nil,
        )
        if err != nil {
            return nil, fmt.Errorf("bind queue %s: %w", q.name, err)
        }
    }

    return ch, nil
}
<?php
declare(strict_types=1);

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Channel\AMQPChannel;

final class RabbitMqTopology
{
    public function __construct(
        private readonly AMQPStreamConnection $conn,
    ) {}

    public function setup(): AMQPChannel
    {
        $ch = $this->conn->channel();

        // Создаём exchange
        $ch->exchange_declare(
            exchange: 'events',
            type: 'topic',
            passive: false,
            durable: true,   // переживёт рестарт
            auto_delete: false,
        );

        // Создаём очереди + биндинги
        $queues = [
            ['name' => 'progress-worker',  'routingKey' => 'lesson.*'],
            ['name' => 'analytics-worker', 'routingKey' => 'lesson.*'],
            ['name' => 'email-worker',     'routingKey' => 'lesson.completed'],
        ];

        foreach ($queues as $q) {
            $ch->queue_declare(
                queue: $q['name'],
                passive: false,
                durable: true,
                exclusive: false,
                auto_delete: false,
            );
            $ch->queue_bind(
                queue: $q['name'],
                exchange: 'events',
                routing_key: $q['routingKey'],
            );
        }

        return $ch;
    }
}

В Symfony Messenger вместо ручной декларации топологии используют AmqpTransport (config/packages/messenger.yaml) - он сам создаёт exchange и очереди при первой публикации.

`durable: true` на queue/exchange означает, что они переживут рестарт RabbitMQ. Но сами сообщения выживут только если отправлены с `DeliveryMode: amqp.Persistent`. Это два разных флага!

Management UI

Админка на localhost:15672 показывает:

Overview - общая статистика: сообщений в очередях, скорость публикации
Connections - кто подключён, сколько каналов
Channels - активные каналы, prefetch count
Exchanges - список exchange, биндинги
Queues - очереди, количество сообщений, consumer'ы

Полезные действия в UI:

  • Опубликовать тестовое сообщение в exchange
  • Посмотреть сообщение из очереди (Get Messages)
  • Проверить биндинги между exchange и queue
  • Мониторить скорость publish/deliver

Подключение из сервиса

func ConnectRabbitMQ(url string) (*amqp.Connection, error) {
    // url формат: amqp://guest:guest@localhost:5672/
    conn, err := amqp.Dial(url)
    if err != nil {
        return nil, fmt.Errorf("dial rabbitmq: %w", err)
    }
    return conn, nil
}

// В docker-compose зависимый сервис:
// services:
//   app:
//     depends_on:
//       rabbitmq:
//         condition: service_healthy
//     environment:
//       RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672/
<?php
declare(strict_types=1);

use PhpAmqpLib\Connection\AMQPStreamConnection;

final class RabbitMqConnector
{
    public function connect(string $host, int $port, string $user, string $pass): AMQPStreamConnection
    {
        // dsn-эквивалент: amqp://guest:guest@rabbitmq:5672/
        return new AMQPStreamConnection($host, $port, $user, $pass);
    }
}

// В docker-compose зависимый сервис:
// services:
//   app:
//     depends_on:
//       rabbitmq:
//         condition: service_healthy
//     environment:
//       MESSENGER_TRANSPORT_DSN: amqp://guest:guest@rabbitmq:5672/%2f/events

Мини-задание

  • Подними RabbitMQ через Docker Compose с healthcheck
  • Открой админку, создай exchange events (type: topic)
  • Создай две очереди: analytics и notifications
  • Привяжи analytics по ключу lesson.*, а notifications по ключу lesson.completed
  • Опубликуй тестовое сообщение через UI и проверь, в какие очереди оно попало

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.