惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
罗磊的独立博客
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Docker
云风的 BLOG
云风的 BLOG
Microsoft Security Blog
Microsoft Security Blog
Blog — PlanetScale
Blog — PlanetScale
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
I
InfoQ
J
Java Code Geeks
博客园 - 聂微东
大猫的无限游戏
大猫的无限游戏
Engineering at Meta
Engineering at Meta
美团技术团队
小众软件
小众软件
Stack Overflow Blog
Stack Overflow Blog
C
Check Point Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Очереди в микросервисах: 5 ошибок, которые приводят к дуб...
sproshchaev · 2026-05-16 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение10 мин

Охват и читатели17

Обзор

Всем привет, меня зовут Сергей Прощаев. Я Tech Lead и руководитель направления Java/Kotlin разработки в FinTech и E‑commerce, а ещё преподаю на курсах архитектуры и разработки в OTUS. Сегодня хочу поговорить о том, на чём спотыкаются почти все, кто начинает строить микросервисную архитектуру с очередями, — о канале «точка‑точка» (Point‑to‑Point Channel), семантике доставки и скрытых граблях, которые превращают надёжную интеграцию в источник ночных инцидентов.

Рис. 1. Проблема дублирования сообщений при неправильном выборе типа канала

Рис. 1. Проблема дублирования сообщений при неправильном выборе типа канала

Представьте: вы запускаете сервис обработки банковских транзакций. Нагрузка растёт, вы добавляете второй экземпляр, чтобы очередь разбиралась быстрее. И вдруг клиенты сообщают о двойных списаниях, а в логах одна и та же транзакция выполняется дважды. Вы лихорадочно проверяете конфигурацию брокера и осознаёте: вместо канала с семантикой «одно сообщение — один получатель» вы использовали publish‑subscribe модель, и теперь каждый consumer получает полную копию сообщения.

Знакомая картина? За годы работы с распределёнными системами я наступил на все возможные грабли, связанные с каналами сообщений, и сегодня разложу по полочкам типичные ошибки и покажу, как правильно строить надёжную доставку.

Ошибка 1: «Я просто добавлю ещё один подписчик»

Самое частое заблуждение — считать, что любое масштабирование слушателей решает проблему производительности. Когда очередь растёт, разработчик добавляет consumers, и если под капотом используется Point‑to‑Point Channel, это действительно работает: каждый новый consumer становится конкурирующим потребителем (Competing Consumer), и сообщения распределяются между ними — одно сообщение достаётся только одному. Но если изначально был настроен канал с семантикой publish‑subscribe, каждый новый подписчик получает свою копию каждого сообщения. Так рождаются дублирующиеся платежи, повторные отправки писем и задвоенные складские операции.

Важнейший нюанс, который надо понимать: модель Queue/Topic радикально отличается в разных брокерах.

В классическом JMS Topic — это всегда publish‑subscribe: каждый подписчик получает копию. Хотите конкурирующих потребителей — используйте Queue. Достаточно перепутать createQueue и createTopic — и модель доставки меняется кардинально:

// JMS: так делать нельзя, если нужна доставка одному получателю
Topic topic = session.createTopic("orders");
MessageProducer producer = session.createProducer(topic);
producer.send(message); // каждый подписчик получит копию

А здесь мы используем класс Queue:

// JMS: правильный вариант для команд, выполняемых однократно
Queue queue = session.createQueue("orders");
MessageProducer producer = session.createProducer(queue);
producer.send(message); // сообщение получит только один consumer

В Kafka Topic с consumer group — это фактически модель конкурирующих потребителей: сообщение обрабатывается одним consumer в пределах партиции. Поэтому для команд ключ партиционирования должен обеспечивать маршрутизацию всех сообщений одного бизнес‑объекта в одну партицию. В RabbitMQ модель competing consumers достигается связкой exchange → queue → consumers. SQS и NATS реализуют схожую модель competing consumers, но с иными гарантиями доставки.

Ошибка возникает не из‑за Topic как такового, а из‑за смешения семантики команд и событий. Именно путаница «отправляю команду» vs «публикую событие» приводит к дублированию.

Пример из классики: в системах биржевой торговли запрос на покупку или продажу ценных бумаг — это команда, которая должна быть выполнена ровно одним экземпляром сервиса. Если команда уйдёт в канал с publish‑subscribe, её получат все торговые роботы, и на биржу улетит несколько одинаковых поручений. Поэтому архитекторы биржевых платформ всегда используют канал с семантикой «одному потребителю» для таких команд.

Ошибка 2: «Брокер подтвердил — значит, всё в порядке»

Вторая системная проблема — настройка режима подтверждения. В JMS по умолчанию часто стоит AUTO_ACKNOWLEDGE. Многие думают, что брокер считает сообщение доставленным сразу при получении, но по спецификации JMS подтверждение связано с успешным завершением обработки сообщения, однако конкретный момент ack зависит от реализации провайдера и настроек prefetch. Если consumer падает во время обработки, сообщение может быть переотправлено — это не DUPS_OK, но и не «подтвердили — всё».

Правильный подход для критичных операций — ручное подтверждение (CLIENT_ACKNOWLEDGE) или транзакционные сессии. Сообщение подтверждается только после того, как бизнес‑логика успешно отработала и зафиксировала результат в базе:

Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(queue);
Message msg = consumer.receive();
try {
    processMessage(msg); // бизнес-логика
    msg.acknowledge();   // подтверждаем только после успешной обработки
} catch (Exception e) {
    // подтверждения нет — сообщение вернётся в очередь
}

Без такого паттерна вы рискуете потерять данные при любом внезапном отказе — особенно если брокер настроен на auto‑ack, а consumer упал сразу после получения.

Ошибка 3: Ни одна очередь не даёт exactly‑once из коробки

Здесь нужно сказать прямо: Queue реализует модель competing consumers, при которой сообщение передаётся одному потребителю за попытку доставки, но не означает, что:

  • не будет redelivery;

  • не возникнет дублирование при сбое сети или ребалансировке consumers;

  • сообщение обработается ровно один раз без дополнительных мер.

В распределённых системах exactly‑once — это иллюзия, требующая усилий с обеих сторон: брокера и consumer. Поэтому для критичных операций обязательна идемпотентность на уровне бизнес‑логики.

Что это значит на практике:

  • Используйте idempotency key — уникальный идентификатор операции.

  • Храните обработанные ключи в базе данных или Redis с TTL, а проверку и сохранение выполняйте атомарно.

  • При повторной доставке consumer должен распознать дубликат и просто подтвердить сообщение, не повторяя бизнес‑операцию.

// Продакшен-вариант идемпотентного consumer — атомарный захват ключа
String idempotencyKey = message.getStringProperty("idempotencyKey");
if (idempotencyRepository.tryAcquire(idempotencyKey)) {
    processMessage(message);   // выполняем только если ключ захвачен
}
message.acknowledge();         // подтверждаем независимо от исхода

В связке с transactional outbox и дедупликацией на уровне брокера (где поддерживается) это даёт семантику, близкую к exactly‑once. Без этого — вы просто надеетесь на удачу.

Ошибка 4: Отсутствие канала для «плохих» сообщений

Даже при идеальной валидации рано или поздно в очереди оказывается сообщение, которое consumer не может обработать: повреждённый формат, несуществующий идентификатор, просроченный токен. Если просто игнорировать ошибку и подтверждать получение — данные потеряются. Если возвращать в очередь — оно будет бесконечно циркулировать, создавая «отравленную» очередь и парализуя обработку.

В реальных продакшен‑системах Dead Letter Queue настраивается на уровне брокера: retry policy, backoff, poison message handling. Consumer не должен сам решать, когда сообщение «плохое» — это делает брокер после исчерпания лимита повторных попыток. В JMS это можно реализовать вручную, но правильнее использовать встроенные механизмы брокера, которые мы настраиваем при развёртывании очереди.

Ошибка 5: Отказ от конкурирующих потребителей ради «строгого порядка»

Иногда команды, боясь нарушения очерёдности, запускают строго один consumer и отказываются от масштабирования. Point‑to‑Point не гарантирует глобальный порядок без дополнительных усилий. Если вам критичен порядок, используйте партиционирование по ключу (например, ID клиента в Kafka), а внутри партиции держите одного потребителя. Для остальных сценариев смело добавляйте конкурирующих потребителей — это основной паттерн горизонтального масштабирования обработки очередей.

Лучшие практики, которые мы внедрили в своих проектах

На основе пережитого я сформулировал несколько правил, которые сейчас использую как чек‑лист при проектировании интеграций.

Разделяйте каналы по типам данных (Datatype Channel). Не валите команды, события и запросы в одну очередь. Для каждого типа сообщения — свой канал, чтобы consumers не ломались на неподходящем формате. У нас был случай, когда в очередь заказов вдруг начали попадать heartbeat‑сообщения от мониторинга — consumer тупо падал с ClassCastException. Разделение каналов решило проблему.

Выбирайте канал под семантику сообщения. Команды — в канал, где сообщение обрабатывается одним потребителем в рамках своей модели доставки (JMS Queue, Kafka Topic + consumer group, RabbitMQ queue). События — в publish‑subscribe. Это архитектурное разделение, а не «Queue хороший, Topic плохой».

Используйте гарантированную доставку избирательно. Для платёжных поручений — постоянное хранение (PERSISTENT) и транзакционность. Для потоковых котировок — допустимо NON_PERSISTENT, потому что скорость важнее, а потеря одного тика некритична.

// Отправка критичного сообщения с гарантированной доставкой
producer.send(message, DeliveryMode.PERSISTENT, Message.DEFAULT_PRIORITY, Message.DEFAULT_TIME_TO_LIVE);

Обязательно настройте Dead Letter Queue и алертинг. Эта пара спасла меня в одном fintech‑проекте, когда поставщик данных внезапно изменил формат сообщения. Все «кривые» сообщения ушли в DLQ, основной поток продолжал работать, а мы спокойно поправили десериализатор.

Мониторьте глубину очереди и возраст сообщений. Если очередь растёт быстрее, чем обрабатывается, — это ранний сигнал, что нужна ещё одна реплика consumer или оптимизация кода.

Ошибка

Признак / Симптом

Что проверить в своей системе

Использование publish‑subscribe для команд

Дубли операций при добавлении новых потребителей

Используется ли Queue (JMS) / consumer group (Kafka) для команд?

Надежда на AUTO_ACKNOWLEDGE

Потеря сообщений при падении consumer'а

Настроен ли CLIENT_ACKNOWLEDGE или транзакционная сессия?

Отсутствие идемпотентности

Повторная обработка при ребалансировке/сбоях

Есть ли idempotency key и атомарная проверка в БД/Redis?

Нет Dead Letter Queue

«Отравленные» сообщения циркулируют, блокируя очередь

Настроена ли DLQ с retry policy и backoff на уровне брокера?

Отказ от конкурирующих потребителей

Очередь не масштабируется, растёт latency

Используются ли competing consumers? При необходимости — партиционирование.

Реальная история: как publish‑subscribe для команд стоил 312 двойных списаний

Как‑то раз меня пригласили на аудит архитектуры платёжного сервиса небольшого финтех‑стартапа. Команда гордилась, что они «легко масштабируются»: добавили второй экземпляр сервиса, и всё завелось. Но через час после деплоя в службу поддержки посыпались обращения от пользователей о двойных списаниях.

Оказалось, разработчики использовали JMS Topic — publish‑subscribe — для команды «executePayment», полагая, что сообщения будут автоматически распределяться. В реальности каждый экземпляр получал полную копию, и 312 платежей за час были проведены дважды. Инцидент разруливали всю ночь: экстренно переключили на Queue, добавили идемпотентность на уровне сервиса и написали скрипт возврата дублей. С тех пор я всегда начинаю ревью архитектуры с вопроса: «Покажите, какие каналы вы используете, под какую семантику и почему».

Схема правильной архитектуры

Описанные практики укладываются в простую, но надёжную схему: один канал «точка‑точка», несколько конкурирующих потребителей, отдельный канал для ошибочных сообщений и идемпотентная обработка (рис. 2).

Рис. 2. Правильная архитектура с конкурирующими потребителями, DLQ и идемпотентностью

Рис. 2. Правильная архитектура с конкурирующими потребителями, DLQ и идемпотентностью

На рисунке 2 показано, как издатель отправляет команду в канал «точка‑точка». Конкурирующие consumers параллельно разбирают очередь, каждый с идемпотентной обработкой. Проблемные сообщения через retry policy брокера попадают в Dead Letter Queue, не блокируя основной поток.

А вот типичный ошибочный сценарий, когда команду отправляют в канал с publish‑subscribe семантикой, и сообщение разлетается всем слушателям:

Рис. 3. Ошибочный сценарий: команда в publish-subscribe канале

Рис. 3. Ошибочный сценарий: команда в publish‑subscribe канале

На рисунке 3 показано, как издатель отправляет сообщение в канал с семантикой publish‑subscribe. Оба consumers получают одно и то же сообщение и обрабатывают его независимо — это неминуемо ведёт к дублированию действий.

Давайте подведём итоги

Канал «точка‑точка» реализует модель competing consumers, при которой сообщение обрабатывается одним потребителем за попытку доставки. Правильный выбор семантики канала под тип сообщения (команда vs событие), ручное подтверждение, Dead Letter Queue, разделение по типам данных и обязательная идемпотентность превращают хрупкую интеграцию в предсказуемую и масштабируемую систему.

Если вы прямо сейчас проектируете обмен сообщениями между сервисами, проведите простой чек: какая семантика у канала, как обрабатываются ошибки, что будет при redelivery и есть ли идемпотентность на уровне бизнес‑логики?

Эти вопросы — минимальный чек‑лист, с которого стоит начинать проектирование любой интеграции на очередях. На самом деле все эти ошибки проверяют один навык: умение различать семантику доставки (команда vs событие) и проектировать обработчики с учётом реальных гарантий брокера.

Если тема очередей в микросервисах для вас сейчас не абстрактная архитектурная теория, а часть реальной продакшен‑задачи, приходите на бесплатные открытые уроки OTUS.

Уроки проходят в рамках онлайн‑курсов, их ведут преподаватели‑практики. На уроках можно познакомиться с экспертами, протестировать формат обучения и задать вопросы по своим сценариям.

А если хотите системно прокачаться в проектировании микросервисов, API, интеграций и распределённых систем — загляните в каталог курсов OTUS по архитектуре. [В каталог]
Там собраны программы для тех, кто хочет разбираться не только в отдельных технологиях, но и в инженерных решениях, на которых держится production.