1. Эффект «сломалось всё» после добавления RabbitMQ
Когда в Compose‑стеке было два сервиса, ошибки обычно выглядели честно и прямолинейно: «не могу подключиться к базе» или «порт занят». Но когда в одном окружении живут app + postgres + redis + rabbitmq, мозг новичка начинает работать как браузер со ста вкладками: вроде каждая вкладка важная, но почему-то все одновременно мешают. Отсюда ощущение «сломался Spring», хотя на самом деле сломалось что-то конкретное.
В многосервисном стенде поломка часто маскируется. Например, приложение может не стартовать из‑за Flyway (PostgreSQL), а вы в этот момент смотрите на RabbitMQ UI и думаете: «Ну broker-то жив, значит проблема где-то в коде…». Или наоборот: приложение стартовало, API отвечает, PostgreSQL и Redis счастливы, но сообщений нет — и кажется, что RabbitMQ «не работает», хотя на самом деле профиль messaging просто не активен, и ваш listener никогда не создавался.
Ключевая мысль для сегодняшней лекции очень инженерная и очень взрослая: мы не «лечим симптомы рандомом», мы задаём короткий набор вопросов в правильном порядке. В идеале — до того, как открыли IDE. Потому что большинство проблем RabbitMQ в учебном Compose‑стеке лечатся не Java‑кодом, а двумя строчками конфигурации: неправильный host, перепутанный порт, неактивный профиль или несовпавшие креденшелы.
2. Рабочий baseline перед диагностикой
Прежде чем искать поломку, полезно коротко зафиксировать, что именно считаем нормой. Иначе вы будете чинить не отклонение от рабочего состояния, а несколько разных версий стенда, собранных по памяти.
| Что уже должно быть | Как это выглядит в нашем baseline |
|---|---|
| RabbitMQ как сервис стенда | В compose.yaml есть service , management UI опубликован на 15672 |
| Режим приложения | app стартует с SPRING_PROFILES_ACTIVE=postgres,cache,messaging |
| Подключение приложения к broker’у | SPRING_RABBITMQ_HOST=rabbitmq, SPRING_RABBITMQ_PORT=5672, SPRING_RABBITMQ_USERNAME=catalog, SPRING_RABBITMQ_PASSWORD=catalog |
| Минимальная топология | Queue bean декларирует catalog.events, sender и listener смотрят в одно и то же имя |
| Наблюдаемый smoke‑сигнал | После одного POST /api/catalog/items в логах есть Sent to catalog.events: ... и Received from catalog.events: ... |
Дальше считаем, что именно этот baseline хотя бы один раз был собран. Теперь можно разбирать, где именно он рвётся.
3. Минимальная карта диагностики: быстрый «алгоритм без паники»
В Docker‑мире легко утонуть в командах и флагах, а в RabbitMQ‑мире — в терминах и настройках. Нам сейчас не нужно ни то, ни другое. Нам нужен короткий, почти «школьный» алгоритм: увидеть симптом → локализовать слой → проверить одну гипотезу → только потом лезть глубже. Такой подход экономит часы и, что важно, сохраняет психику разработчика в рабочем состоянии.
Ниже — простая «карта» принятия решений. Она не про идеальную эксплуатацию RabbitMQ и не про production‑гарантии. Она про то, как в учебном (и очень похожем на реальный) Compose‑стеке быстро понять: где именно разорвалась цепочка.
flowchart TD
A["Симптом: приложение не стартует или «сообщения не доходят»"] --> B{RabbitMQ контейнер жив?}
B -->|Нет| C["docker compose ps + logs rabbitmq"]
B -->|Да| D{Профиль messaging активен?}
D -->|Нет| E["SPRING_PROFILES_ACTIVE + логи Boot"]
D -->|Да| F{Приложение подключилось к broker?}
F -->|Нет| G["host/port/user/pass + service name"]
F -->|Да| H{Есть очередь и есть consumer?}
H -->|Нет| I["имена очереди, Queue bean, @RabbitListener"]
H -->|Да| J["смотрим отправку/получение в логах и в очереди"]
Чтобы эта схема была не абстрактной, держите рядом маленькую табличку «что проверяю и где». Она помогает не перескакивать на третий шаг, пока не сделан первый.
| Что проверяем | Где / чем проверяем | Что считаем «нормой» |
|---|---|---|
| RabbitMQ контейнер не падает | docker compose ps | статус Up, без вечного Restarting |
| RabbitMQ реально стартовал | docker compose logs rabbitmq | в конце логов видно, что broker завершил старт |
| Приложение запущено в нужном режиме | docker compose logs app | в логах видно postgres,cache,messaging |
| Приложение смотрит на правильный host/port | compose.yaml и app logs | host=rabbitmq, port=5672 |
| Аутентификация совпадает | compose.yaml + application-messaging.yml | одни и те же catalog/catalog с обеих сторон |
| Очередь существует и совпадает по имени | management UI или rabbitmqctl | есть очередь catalog.events |
| Есть наблюдаемый след отправки/получения | app logs | Sent ... и Received ... для одного и того же сообщения |
4. RabbitMQ как контейнер: проверка старта
Проверка RabbitMQ начинается не с Spring и не с Java. Она начинается с грубого факта: контейнер брокера вообще запустился или он красиво умер сразу после docker compose up. Это скучный шаг, но именно он чаще всего экономит время. Docker Compose умеет поднять вам «красивую картинку» из сервисов, но если один из них постоянно перезапускается, всё остальное будет выглядеть как загадка, хотя это просто авария в одном месте.
Самый прямой старт — посмотреть статус сервисов:
docker compose ps # смотрим, какие сервисы Up/Restarting/Exited
docker compose logs --tail=80 rabbitmq # читаем причину падения именно у rabbitmq
Если RabbitMQ в состоянии Exited или бесконечно Restarting, в логах почти всегда будет честная причина. Частый бытовой сценарий — конфликт портов на host‑машине. Вы публикуете "5672:5672" и "15672:15672", но какой‑то другой RabbitMQ уже живёт у вас локально (или порт занят другой программой). В этом случае Compose не «подумает за вас» — он просто скажет, что порт уже занят, и сервис не стартует.
Ещё один типичный сценарий — ошибка на уровне Compose‑конфигурации: неправильный image, опечатка в переменной, которую вы подставляете через ${RABBITMQ_IMAGE}, или забытый .env, из‑за чего Compose вообще не смог собрать итоговую конфигурацию. Это не RabbitMQ‑проблема, а Compose‑проблема, и лечится она в YAML/переменных, а не в коде приложения.
Отдельно важно не переоценивать management UI как «истину». Да, если UI открывается на http://localhost:15672, это сильный сигнал, что контейнер хотя бы жив и RabbitMQ поднялся. Но UI — это HTTP‑сервис, а наш путь «отправка/получение» идёт через AMQP на порту 5672. Ситуация «UI доступен, но приложение не подключается» вполне реальна, и к ней мы сейчас и перейдём.
5. Приложение не подключается: host/port, service name и профили
Когда RabbitMQ контейнер жив, следующая большая группа проблем — приложение смотрит «не туда» или смотрит «не теми глазами». То есть либо неправильный host/port, либо неправильные креденшелы, либо профиль messaging не активирован и нужные компоненты вообще не создаются. Самое неприятное тут то, что эти ошибки выглядят «по‑разному», и новичок часто начинает менять Java‑код, хотя виновата одна строка конфигурации.
Начнём с самого частого: localhost внутри Compose. Это классика уровня «первый день Docker Compose», которая всё равно всплывает именно на RabbitMQ, потому что рядом есть «веб‑морда», и руки тянутся к локальному браузерному опыту.
Вот так выглядит типичная неправильная настройка:
# ❌ неправильно внутри Compose
SPRING_RABBITMQ_HOST: localhost # внутри контейнера это "я сам", а не RabbitMQ-сервис
SPRING_RABBITMQ_PORT: 15672 # это порт UI (HTTP), а не AMQP
А вот так — правильная для контейнера приложения:
# ✅ правильно внутри Compose
SPRING_PROFILES_ACTIVE: postgres,cache,messaging
SPRING_RABBITMQ_HOST: rabbitmq
SPRING_RABBITMQ_PORT: 5672
SPRING_RABBITMQ_USERNAME: ${RABBITMQ_USER}
SPRING_RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD}
Почему это важно проговорить «ещё раз, но уже на RabbitMQ»: порт 15672 — это management UI, он нужен вашему браузеру на host‑машине. Spring Boot же должен подключаться к AMQP‑порту 5672. Если вы подставили 15672 в spring.rabbitmq.port, приложение будет честно пытаться говорить по AMQP с HTTP‑портом. Это как пытаться заказать пиццу по номеру пожарной части: трубку, может, и возьмут, но дальше разговор будет странный.
Теперь про профили. В нашем проекте messaging‑часть ограничена профилем, чтобы не ломать приложение в режимах без broker’а. Значит, если вы забыли messaging в SPRING_PROFILES_ACTIVE, то возможны два варианта поведения. Либо приложение просто не создаст Queue, sender и listener, и «ничего не произойдёт», либо какие‑то ваши компоненты зависят от messaging‑слоя, и тогда приложение упадёт на старте с ошибкой про отсутствующий bean. В обоих случаях первый шаг — смотреть в логи старта Spring Boot и найти строку с активными профилями.
Очень частая отдельная боль — креденшелы. Вы задаёте в RabbitMQ контейнере:
environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER} # создаём пользователя в самом RabbitMQ при старте контейнера
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} # задаём пароль этому пользователю (для AMQP и UI)
…а приложение должно использовать те же значения через SPRING_RABBITMQ_USERNAME/PASSWORD. Если оно продолжает пытаться подключиться как guest/guest или с каким-то другим набором, вы увидите ошибки аутентификации. Чтобы это было предсказуемо и управляемо, свойства на стороне Spring Boot лучше держать так:
spring:
rabbitmq:
host: ${SPRING_RABBITMQ_HOST:rabbitmq} # берём host из env, но по умолчанию — имя сервиса
port: ${SPRING_RABBITMQ_PORT:5672} # AMQP-порт (не 15672)
username: ${SPRING_RABBITMQ_USERNAME:catalog} # логин должен совпасть с тем, что создали в контейнере
password: ${SPRING_RABBITMQ_PASSWORD:catalog} # пароль должен совпасть с тем, что создали в контейнере
Если вы видите в логах приложения ошибки вида access refused, login refused или authentication failed, почти всегда это именно рассинхрон между тем, какого пользователя вы создали в контейнере RabbitMQ, и тем, какого пользователя Spring Boot пытается использовать.
И последнее в этой группе — ошибка DNS/имени. Если вы назвали сервис rabbitmq, но в конфиге написали rabbit, или наоборот, то приложение не сможет разрешить имя и упадёт с чем-то вроде UnknownHostException. Это выглядит пугающе, но по сути это просто опечатка: внутри Compose DNS работает по именам сервисов, а не по вашей интуиции.
6. Подключение есть, а сообщений нет: очередь, отправитель и listener
Самая коварная стадия — когда RabbitMQ жив, приложение вроде бы стартовало, и даже management UI радостно показывает «всё зелёное», но вы не видите главного: сообщения не приходят. В этот момент хочется обвинить «магический broker», но в учебном минимальном сценарии причины почти всегда бытовые. Либо вы не отправляете сообщение, либо отправляете не туда, либо слушаете не там, либо listener вообще не создан из-за профиля.
Поскольку у нас сейчас нарочно простой контракт «одна очередь, строковое сообщение», диагностика тоже должна быть простой и наблюдаемой. Первое, что помогает, — один источник правды по имени очереди. У нас это уже CatalogQueues.CATALOG_EVENTS, и именно на него должны смотреть и publisher, и listener.
Для диагностики особенно полезны две короткие строки‑маркера внутри уже существующих RabbitCatalogEventPublisher и CatalogEventListener:
amqpTemplate.convertAndSend(CatalogQueues.CATALOG_EVENTS, message);
log.info("Sent to {}: {}", CatalogQueues.CATALOG_EVENTS, message);
@RabbitListener(queues = CatalogQueues.CATALOG_EVENTS)
public void onMessage(String message) {
log.info("Received from {}: {}", CatalogQueues.CATALOG_EVENTS, message);
}
Если вы видите Sent..., но не видите Received..., это означает, что отправка случилась, а потребление нет. Тогда проверяем, что listener реально создан, и что он слушает ту же очередь.
Дальше можно сделать одну «инфраструктурную» проверку: существует ли очередь и есть ли в ней сообщения. Да, у нас есть management UI, но в CLI иногда быстрее. Внутри контейнера RabbitMQ можно посмотреть очереди так:
docker compose exec rabbitmq rabbitmqctl list_queues name messages
# имя очереди + сколько сообщений лежит внутри
Если вы видите очередь catalog.events и у неё растёт число messages, это означает «сообщения накапливаются, но consumer их не съедает». Если же очередь вообще не существует, значит либо Queue bean не создаётся, либо приложение не смогло подключиться и объявить очередь, либо вы просто отправляете в другое имя.
И важный психологический момент. В момент «сообщений нет» не надо сразу додумывать сложные вещи вроде «RabbitMQ потерял сообщения» или «AMQP сломался». В нашем текущем уровне курса чаще всего виновата не физика broker’а, а банальная рассинхронизация: профиль не активен, имя очереди разное, отправка не вызывается тем endpoint’ом, который вы тестируете.
7. RabbitMQ vs PostgreSQL/Redis: чтение логов
В полном Compose‑стеке есть неприятная особенность: самая громкая ошибка часто не та, которую вы сейчас пытаетесь починить. Например, вы «добавили RabbitMQ», а приложение падает из‑за миграции Flyway, потому что вы случайно подняли стенд на старых данных в volume. Или Redis не доступен, потому что у вас активен профиль cache, но сервис Redis не поднят. В итоге RabbitMQ становится «подозреваемым по умолчанию», хотя он вообще ни при чём.
Чтобы не превращаться в детектива на кофеине, полезно уметь быстро «распознавать подпись» зависимости по исключению. Это не магия, это просто практика чтения логов: в stacktrace часто есть понятные пакеты.
| Что видите в ошибке / логах | Чаще всего это про… |
|---|---|
| org.postgresql…, PSQLException…, Flyway… | PostgreSQL / миграции / datasource |
| io.lettuce…, RedisConnectionException… | Redis (кэш-профиль, хост/порт) |
| org.springframework.amqp…, com.rabbitmq.client…, AmqpConnectException… | RabbitMQ (host/port/user/pass, доступность) |
Ещё один полезный приём — смотреть момент времени, когда упало приложение. Если оно падает очень рано, ещё до нормальной строки Started ... in ... seconds, то часто причина в обязательной зависимости (PostgreSQL в профиле postgres, миграции Flyway, конфигурация datasource). RabbitMQ в нашем сценарии может проявляться и позже: например, когда вы впервые пытаетесь отправить сообщение, или когда listener начинает подниматься.
И да, здесь снова работает правило Compose‑диагностики: читайте логи несколькими потоками, но осознанно. Не обязательно включать все сразу в «водопад на пол экрана». Часто достаточно держать рядом логи приложения и логи RabbitMQ:
docker compose logs --tail=120 app # ошибки/подключения/профили на стороне приложения
docker compose logs --tail=120 rabbitmq # старт broker'а и его состояние
Если приложение пишет «не могу подключиться к rabbitmq», а в логах RabbitMQ в этот момент видно, что он вообще ещё стартует, то проблема может быть банальным таймингом. Если же RabbitMQ давно жив, а приложение всё равно не подключается, значит это конфигурация: host/port/user/pass.
8. Интеграция RabbitMQ: наблюдаемость и маркерные логи
RabbitMQ‑интеграция, как и любая инфраструктурная интеграция, становится в разы проще, когда она «оставляет следы». Но тут легко впасть в другую крайность: включить DEBUG на всё, получить десять экранов логов на один запрос и потом гордо ничего не понять. Наша цель — добавить ровно столько наблюдаемости, чтобы видеть путь «подключение → отправка → получение», и не больше.
Самый спокойный путь — настроить уровень логирования для AMQP‑части в рамках профиля messaging, чтобы лишний шум не появлялся в режимах без RabbitMQ:
logging:
level:
org.springframework.amqp: INFO # базовые события Spring AMQP без лишнего шума
com.rabbitmq.client: INFO # клиент RabbitMQ на уровне "всё важное видно"
Этого обычно достаточно, чтобы увидеть ключевые события подключения и работы listener’ов без превращения логов в «матрицу». Если хочется совсем аккуратно, часто лучше не повышать общий уровень библиотек, а добавить свои маркерные логи, которые вы контролируете. Например, те самые Sent to ... и Received from ... из предыдущего раздела. В учебном проекте это работает почти как тест: если вы не видите обе строки, путь разорван, и вы идёте по диагностической карте выше.
Отдельно проговорим «чего не делать», хотя это звучит как банальность. Не логируйте пароли и connection strings с секретами, даже если это учебный стенд. Привычки, которые вы натренируете на учебном проекте, потом очень легко уедут в рабочий репозиторий. Лучше уж пошутить в логе («Подключаемся к RabbitMQ, держите морковку для кролика») — но без секретов.
И ещё маленькая прагматика. Если у вас ещё остался System.out.println() из самого первого эксперимента, он тоже попадёт в docker compose logs app. Это нормально для демонстрации, но как только вы начинаете отличать «просто вывод» от «инженерного лога», лучше переходить на нормальный логгер. Тогда у вас будет единый формат, уровни и управляемая видимость.
9. Типичные ошибки при диагностике RabbitMQ в Compose‑стеке
Ошибки RabbitMQ в учебном Compose‑стеке часто не «сложные», а просто очень обидные: вы всё почти правильно сделали, но одна мелочь обнулила результат. Поэтому этот раздел полезно читать не как «стыдный список», а как прививку. Почти каждый разработчик хотя бы раз проходил через эти грабли. Некоторые — по кругу. Я не осуждаю: я тоже.
Ошибка №1: использовать localhost как host для RabbitMQ внутри контейнера приложения.
Это выглядит логично, потому что вы открываете UI в браузере на localhost:15672 и мозг говорит «ну значит и приложению туда же». Но контейнер приложения живёт в своей сетевой реальности, и localhost там указывает на него самого. В результате вы видите либо Connection refused, либо попытки подключиться «в пустоту». Лечится это дисциплиной: внутри Compose используем service name (rabbitmq), а не localhost.
Ошибка №2: перепутать порт AMQP и порт management UI.
Порт 15672 нужен человеку (браузеру) и обслуживает HTTP‑интерфейс управления. Порт 5672 нужен приложению и обслуживает AMQP. Если вы подставили 15672 в spring.rabbitmq.port, вы по сути просите Spring Boot говорить AMQP на HTTP‑порту. Это не «сложный RabbitMQ», это просто два разных порта для двух разных задач.
Ошибка №3: считать management UI доказательством того, что Spring Boot подключился.
UI может быть доступен, но приложение при этом может не подключиться из‑за неправильного пользователя/пароля, неправильного host/port или неактивного профиля. UI показывает, что broker жив. А то, что приложение к нему подключилось, доказывают либо логи приложения, либо факт, что очередь объявилась и сообщения проходят через sender/listener.
Ошибка №4: забыть активировать профиль messaging и потом искать «почему сообщений нет».
В нашем проекте messaging‑компоненты включаются профилем, чтобы не ломать режимы без RabbitMQ. Если профиль не активен, listener может вообще не существовать, а Rabbit‑реализация publisher’а не будет активна. В итоге вы отправляете запросы в API, сервис работает, а messaging‑часть как будто «мертва». Это не поломка RabbitMQ — это просто другой runtime‑режим приложения.
Ошибка №5: рассинхронизировать логин/пароль между RabbitMQ контейнером и Spring Boot.
RabbitMQ в Compose часто создаёт пользователя через RABBITMQ_DEFAULT_USER/PASS, а Spring Boot должен использовать те же значения через SPRING_RABBITMQ_USERNAME/PASSWORD. Тогда host и port могут быть правильными, контейнер живой, а подключение всё равно не происходит. Лечится просто: держите один источник правды и следите, чтобы значения совпадали с обеих сторон.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ