- PVSM.RU - https://www.pvsm.ru -
Kafka часто воспринимается как система, гарантирующая доставку сообщений и Exactly Once Semantics. Однако в реальных распределённых системах эти гарантии заканчиваются на границе брокера.
Сообщение может потеряться между записью в базу данных и публикацией события, а может быть обработано повторно при сбое сервиса.
В этой статье разберём:
где именно теряются гарантии Kafka
почему Exactly Once не работает на уровне всей системы
и как паттерны Outbox / Inbox помогают решить эту проблему.
Мы используем микросервисную архитектуру: каждый сервис отвечает за свою предметную область и развивается независимо.
Для взаимодействия между сервисами применяем два подхода:
Синхронный — через HTTP, когда нужен немедленный ответ;
Асинхронный — через события, когда важно просто передать информацию, не дожидаясь результата.
Асинхронность:
уменьшает связанность;
помогает справляться с пиковыми нагрузками;
В качестве транспорта мы выбрали Apache Kafka — надёжную и масштабируемую платформу.
Но вместе с преимуществами асинхронность приносит и новые вызовы: потеря сообщений, дублирование и несогласованность данных между сервисами.
Kafka предоставляет мощные механизмы надёжной доставки сообщений:
подтверждение записи (acks=all);
идемпотентный продюсер (enable.idempotence=true);
ручной коммит offset'а после обработки (ack.acknowledge()).
При правильной конфигурации Kafka может обеспечить Exactly Once Semantics — но только в рамках своей экосистемы: между продюсером, брокером и консьюмером. Однако в реальных распределённых системах взаимодействие выходит за пределы Kafka — и именно на этих границах возникают риски потерь, дублирования и нарушения согласованности.
Kafka гарантирует доставку внутри себя, но не может контролировать внешние системы — базы данных, сторонние API и т.д.
Сценарий:
Сервис успешно сохраняет заказ в БД;
После этого публикует событие в Kafka.
public void saveOrder(Oreder oreder) {
orederRepository.save(oreder);
String message = KafkaMessage.createPayload(oreder);
kafkaTemplate.send(message);
}
Проблема: Сервис падает до отправки события.
Результат: Бизнес-операция завершена, а событие утеряно.
Сценарий:
Сервис получает сообщение из Kafka;
Обрабатывает его и сохраняет данные в БД;
После этого коммитит offset.
@KafkaListener(topics = "order")
public void listen(String message, Acknowledgment ack) {
KafkaMessage kafkaMessage = NonNullObjectMapper.convertJsonToObject(message,KafkaMessage.class);
Order order = Order.from(kafkaMessage.getPayload);
oredrRepository.save(order);
ack.acknowledge();
}
Проблема: Сервис падает после записи в БД, но до коммита offset
Результат: Kafka считает, что сообщение не обработано, и отправляет его повторно. Если бизнес-операция не идемпотентна — возникает повторное выполнение действия: двойной платёж, повторное письмо, дублирующая запись.
Kafka предоставляет надёжную доставку сообщений внутри своей инфраструктуры, но не может гарантировать целостность данных на уровне всей системы.
Гарантии теряются на стыке:
между бизнес-операцией и публикацией события;
между получением события и записью в БД.
Именно здесь и появляются риски:
Потери сообщений;
Дублирование действий;
Нарушение согласованности между сервисами.
Чтобы их избежать, нужны архитектурные решения — такие как Outbox / Inbox, которые позволяют расширить зону надёжности за пределы Kafka и обеспечить гарантированную доставку в рамках всей системы.
Что такое Outbox и Inbox?
|
Паттерн |
Что делает? |
Зачем нужен? |
|
Outbox |
Сохраняет событие в локальную таблицу в рамках бизнес-транзакции |
Гарантирует, что событие будет создано только если бизнес-операция успешна |
|
Inbox |
Сохраняет входящее сообщение перед обработкой |
Позволяет безопасно обрабатывать события и избегать потери при сбоях |
Когда сервис выполняет бизнес-операцию (например, создаёт заказ), он:
сохраняет бизнес-данные в свою таблицу (например, orders);
в той же транзакции сохраняет событие в таблицу outbox.
Это гарантирует, что событие будет создано только если бизнес-операция прошла успешно.
Далее компонент публикации событий:
регулярно проверяет, какие события готовы к отправке;
отправляет их в Kafka;
при успешной отправке помечает событие как отправленное;
если отправка не удалась — компонент публикации попытается отправить событие повторно.
@Scheduled(fixedRateString = "...")public void postMessage() { List<OutboxMessage> forDelivery = outboxRepository.getMessagesForDelivery(...); for (OutboxMessage msg : forDelivery) { try { kafkaTemplate.send(topic, msg.payload()).get(); outboxRepository.updateStatusTo(msg.messageId(), SENT); } catch (Exception e) { outboxRepository.resetStatus(msg.messageId()); } } }
Логика повторной отправки инкапсулирована в outboxRepository. Репозиторий управляет статусами сообщений (NEW, IN_PROGRESS, SENT) и гарантирует, что сообщения, отправка которых завершилась ошибкой или долго висят в SENT, будут автоматически возвращены в очередь на повторную обработку.
Inbox — это хранилище входящих сообщений. Вместо того чтобы сразу обрабатывать событие, сервис:
Сохраняет сообщение в таблицу Inbox;
Фиксирует offset в Kafka (сообщает, что сообщение получено);
Позже обрабатывает сообщение из базы, а не напрямую из Kafka.
Такой подход дает два ключевых преимущества:
Защита от потерь
Защита от дублирования - каждое сообщение имеет уникальный message_id. Перед сохранением в Inbox сервис проверяет: есть ли уже такое сообщение.
@KafkaListener(...) public void listen(String message, Acknowledgment ack) { InboxMessage msg = InboxMessage.from(message); inboxRepository.saveToInbox(msg); ack.acknowledge(); // ручной commit }
Чтобы обеспечить ещё более надёжную доставку, мы используем механизм подтверждения доставки. После того как получатель сохранил сообщение в свою таблицу inbox, он отправляет подтверждение обратно отправителю. Зачем это нужно? Механизм подтверждения позволяет компоненту публикации знать, что сообщение было успешно получено и сохранено получателем. Чтобы избежать повторной отправки одного и того же события при отсутствии подтверждения.
Outbox / Inbox — это не просто паттерн, а архитектурное соглашение, которое позволяет:
проектировать надёжные интеграции;
минимизировать риски потерь и дублирования;
стандартизировать подход к обмену событиями;
Пишите код, который не теряет сообщения.
Архитектура — это не только про «как», но и про «что будет, если...».
Автор: mr_green773
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/soglasovannost/447204
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1012512/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1012512
Нажмите здесь для печати.