Транспорт бэкенда

Transactional Outbox и CDC

Заказ создан, деньги списаны. Код отправляет событие в Kafka - и в этот момент деплой убивает процесс. Inventory Service никогда не узнает о заказе, товар не зарезервирован. Через 20 минут клиент получит уведомление "ваш заказ не может быть выполнен". Transactional Outbox - паттерн, который делает этот сценарий невозможным.

  • **Debezium** используется в тысячах production систем включая Zalando, Convoy, WePay. Читает PostgreSQL WAL и доставляет события с миллисекундной задержкой.
  • **Zalando** опубликовал Nakadi - event bus поверх Kafka с outbox паттерном в основе. Используется для всех e-commerce событий платформы.
  • **Netflix** использует вариант outbox для Conductor (workflow engine): задачи сохраняются атомарно с событиями, CDC доставляет их в execution engine.

Dual-Write Problem

Dual-write - когда код записывает в два места атомарно: сначала в БД, потом публикует событие в Kafka/RabbitMQ. Проблема: между двумя операциями нет транзакции. Процесс может упасть после записи в БД, но до публикации события. Или наоборот.

Dual-write - одна из самых частых причин data inconsistency в микросервисах. Кажется безобидным в dev (процессы редко падают), но в prod под нагрузкой и с деплоями - происходит регулярно.

Почему нельзя обернуть запись в БД и публикацию в Kafka в одну транзакцию?

Transactional Outbox Pattern

Outbox Pattern решает dual-write: событие сохраняется в таблицу `outbox` в той же транзакции с бизнес-данными. Отдельный процесс (relay) читает outbox и публикует события в брокер, помечая как обработанные. Две операции теперь атомарны через одну DB-транзакцию.

Relay публикует at-least-once: при сбое между публикацией и пометкой как published - событие будет опубликовано дважды. Поэтому потребители должны быть идемпотентными (обрабатывать дубликаты без последствий).

Что гарантирует Transactional Outbox Pattern в случае сбоя процесса?

Change Data Capture и Debezium

Change Data Capture (CDC) - захват изменений прямо из WAL (Write-Ahead Log) базы данных. Debezium - популярная open-source платформа CDC, которая читает PostgreSQL WAL, MySQL binlog, MongoDB oplog и публикует изменения в Kafka. Нет polling, нет задержки, no-code outbox relay.

Debezium требует настройки PostgreSQL: `wal_level = logical`, `max_replication_slots >= 1`. Debezium хранит offset (LSN) в Kafka, что позволяет переиграть события с любой точки в истории WAL.

Чем CDC через Debezium лучше polling outbox таблицы?

Идемпотентные потребители

At-least-once доставка (Kafka, RabbitMQ, outbox relay) означает: одно и то же сообщение может прийти дважды. Идемпотентный потребитель обрабатывает дубликаты без побочных эффектов - повторная обработка даёт тот же результат, что и первичная.

Stripe требует idempotency keys для всех API запросов: клиент генерирует уникальный ключ, повторный запрос с тем же ключом возвращает кешированный результат без повторного выполнения. Это идемпотентность на уровне API.

Лучший способ обеспечить идемпотентность обработки Kafka-событий:

Гарантии порядка событий

Kafka гарантирует порядок внутри partition - не между partitions. Если события одного заказа попадают в разные partitions, они могут быть обработаны не в порядке создания. Решение: partition key = aggregate ID (orderId, userId).

Ordering между разными агрегатами (orderId=1 и orderId=2) в Kafka не гарантирован - и это нормально. Бизнес-логика обычно требует порядок только внутри одного агрегата.

Outbox Pattern - это костыль, правильное решение - использовать Kafka Transactions

Outbox - стандартный production паттерн, используемый Netflix, Zalando, Uber. Kafka Transactions решают другую задачу - атомарность внутри Kafka.

Kafka Transactions (Kafka -> Kafka) не помогают когда нужно атомарно записать в PostgreSQL И опубликовать в Kafka. Outbox использует транзакцию самой БД - это более простое и надёжное решение чем XA.

Как гарантировать порядок обработки событий одного заказа в Kafka?

Ключевые идеи

  • **Dual-write невозможно сделать атомарным** между БД и Kafka - нет XA транзакций. Outbox решает это через таблицу в той же БД.
  • **CDC через Debezium** читает WAL напрямую - нет polling, минимальная задержка, no-code relay. Вместе с outbox таблицей - production-ready решение.
  • **Идемпотентность обязательна** при at-least-once доставке. Deduplication по event ID или natural idempotency через upsert.

Связанные темы

Outbox Pattern обеспечивает надёжную основу для Saga и Event-Driven архитектур:

  • Saga Pattern — Каждый шаг саги должен атомарно обновить БД и опубликовать событие - именно здесь нужен Outbox
  • Dead Letter Queue и Retry — Outbox relay должен обрабатывать ошибки публикации - те же паттерны retry и DLQ применяются к relay процессу

Вопросы для размышления

  • Как обеспечить очистку outbox таблицы от обработанных записей, не теряя возможность replay событий?
  • При каком объёме событий polling outbox становится узким местом и стоит переходить на CDC?
  • Debezium читает WAL - что произойдёт если WAL ротируется быстрее, чем Debezium успевает его читать?

Связанные уроки

  • dist-07-transactions
Transactional Outbox и CDC

0

1

Войти