Почему отправка сообщения в Kafka — это двухфазный коммит, а не fire-and-forget
Команды думают: отправил сообщение — работа сделана. Но система становится консистентной только когда получатель обработал и подтвердил. До этого момента она в рассогласованном состоянии — и это может длиться секунды или часы.
Проблема
Разработчик отправил сообщение в Kafka — и считает, что задача выполнена. Но получатель ещё не обработал. У него своя база, своя бизнес-логика, он может отклонить изменение или упасть на середине. Отправитель об этом не знает. Система в этот момент рассогласована — и это не аномалия, а нормальное состояние асинхронной архитектуры, которое нужно проектировать осознанно.
Методика
Две фазы распределённого коммита — это не DB и брокер, а отправитель и получатель.
- 1. Что такое две фазы на самом деле. Фаза 1 — сервис А сохранил изменение и отправил сообщение в брокер. Фаза 2 — сервис Б получил, обработал и зафиксировал у себя. Система событийно консистентна только после фазы 2. Всё время между ними — окно рассогласования, которое может длиться секунды или часы.
- 2. Получатель — не тупой relay. У сервиса Б своя бизнес-логика и валидация. Он может отклонить изменение — недостаточно прав, нарушение правил, несовместимый формат. Сервис А об этом не знает в момент отправки. Это архитектурное свойство хореографии, не баг реализации.
- 3. Два архитектурных выбора — и оба требуют разговора с бизнесом. Либо А отправляет и доверяет брокеру и Б — fire-and-forget с явным принятием eventual consistency. Либо А ждёт подтверждения от Б — система синхронна по бизнес-факту, но сложнее по реализации. Какой бы вариант ни выбрали, бизнес-заказчик должен понимать: между отправкой и подтверждением система находится в рассогласованном состоянии. Это может длиться секунды или часы — и это нормально, если принято осознанно. Если не проговорить заранее, это всплывёт как «баг» в проде.
-
4. Инфраструктурный prerequisite: надёжная фаза 1.
Прежде чем думать о фазе 2, нужно чтобы сообщение вообще дошло до брокера.
Две системы (БД и брокер) не коммитятся атомарно — нужен outbox в той же транзакции.
Как выглядят обе фазы вместе:
# Фаза 1: сервис А фиксирует изменение и отправляет with db.transaction(): # атомарно — или оба, или ничего order.status = "placed" order_repo.save(order) outbox.publish("orders.created", order) # ← А завершил; система рассогласована до завершения фазы 2 # Фаза 2: сервис Б обрабатывает асинхронно def handle_order_created(event): if not inventory.reserve(event.items): outbox.publish("orders.rejected", event.order_id) return # Б отклонил — А об этом пока не знает fulfillment_repo.create(event) outbox.publish("orders.confirmed", event.order_id) # Фаза 1 завершается: А получает подтверждение от Б def handle_order_confirmed(event): order_repo.update(event.order_id, status="confirmed") # ← только теперь оба узла согласованы по бизнес-факту def handle_order_rejected(event): order_repo.update(event.order_id, status="rejected") # ← или оба знают об отказе
Два паттерна, которые делают этот цикл управляемым на практике: Correlation ID — уникальный идентификатор, который А присваивает запросу при отправке и передаёт через все события; Б возвращает его в confirmation, А матчит ответ обратно к исходному запросу. Без него А не знает, какое именно подтверждение пришло. Saga — управление цепочкой из нескольких шагов с компенсирующими транзакциями: если шаг N упал, сага откатывает шаги 1..N−1 через явные compensating events. Актуально когда в цикл вовлечены три сервиса и больше.
Артефакт
Видео «Почему отправка сообщения в Кафку — это двухфазный коммит» на канале @IT-Head. Две фазы здесь — это отправитель и получатель: система событийно консистентна только тогда, когда сервис А знает, что сервис Б успешно обработал сообщение. До этого момента система в состоянии рассогласования — и это может длиться секунды или часы. Howto разбирает уровень ниже: как не потерять сообщение по дороге от БД до брокера.
Где ломается
- Бизнес не готов к окну рассогласования. Команда выбрала fire-and-forget, но не объяснила заказчику: между отправкой и обработкой система в несогласованном состоянии. Когда это всплывает в проде — начинается «у нас баг», хотя это спроектированное поведение.
- Получатель отклонил — отправитель не знает. Сервис Б упал или вернул ошибку бизнес-логики, а сервис А уже зафиксировал изменение у себя. Система рассогласована, инцидент проявится позже и непредсказуемо.
- Dual write без outbox — БД закоммитилась, публикация упала: фаза 1 не завершена, но отправитель считает что да. Всплывает под нагрузкой, не на демо.
- Идемпотентность получателя обязательна — relay по природе даёт at-least-once, дубликаты неизбежны, обрабатывать их нужно на стороне Б, не надеяться на «ровно один раз».
Для кого и почему
Если вы строите event-driven системы — здесь про класс инцидентов, который возникает не из-за багов реализации, а из-за неверной ментальной модели. Окно eventual consistency и ответственность получателя нужно проектировать явно, а не обнаруживать в проде.
Есть проблемы с рассогласованием в event-driven системе?
Аудит event-driven интеграций: где окно eventual consistency не проговорено с бизнесом, где получатель может отклонить молча, где dual write без outbox — на основе реального опыта с 40+ сервисами на Kafka/RabbitMQ.
Написать на почтуДругие разборы
Серия инженерных разборов: реальная задача → методика → работающий артефакт → честный разбор, где он ломается.
К серии →