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

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
D
Docker
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
Jina AI
Jina AI
小众软件
小众软件
Last Week in AI
Last Week in AI
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
美团技术团队
爱范儿
爱范儿
V
V2EX
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
J
Java Code Geeks
博客园 - 司徒正美
博客园 - 叶小钗
S
SegmentFault 最新的问题
量子位
S
Secure Thoughts
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
O
OpenAI News
L
LINUX DO - 最新话题
罗磊的独立博客
SecWiki News
SecWiki News
雷峰网
雷峰网
Recent Announcements
Recent Announcements
V2EX - 技术
V2EX - 技术
T
Tailwind CSS Blog
H
Hacker News: Front Page
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
云风的 BLOG
云风的 BLOG
Schneier on Security
Schneier on Security
T
The Blog of Author Tim Ferriss
IT之家
IT之家
博客园 - 聂微东
腾讯CDC
N
News | PayPal Newsroom
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Hacker News: Ask HN
Hacker News: Ask HN
aimingoo的专栏
aimingoo的专栏
Webroot Blog
Webroot Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
K
Kaspersky official 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Очереди в микросервисах: 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.