Все проекты
Событийная архитектура / Учебный проект

EventForge

Событие пришло дважды. Что должно произойти дальше?

В работеGo / PostgreSQL / Kafka / Docker

Интерактивная архитектура

eventforge / architecture labИллюстративная модель

Одно событие. Все последствия.

Иллюстративная архитектура · в работе · без живого backend

активен / записанповторсбой / отменаНе живая телеметрия
Шаг 1 / 10Пауза

Поступает запрос

Клиент создаёт заказ. Это иллюстрация, а не трафик живого backend.

Заказ
не создан
Outbox
не записан
Бизнес-эффект
0 изменений
Kafka offset
не фиксирован
Узел / API заказов

Принимает запрос клиента. В модели order-42 и event-42 — иллюстративные идентификаторы.

Прочитать все шаги

Зафиксировать заказ, доставить событие, применить изменение и затем зафиксировать offset.

  1. Клиент создаёт заказ. Это иллюстрация, а не трафик живого backend.

  2. API записывает заказ и событие outbox в одной открытой транзакции. Пока ни одна запись не зафиксирована.

  3. Commit надёжно сохраняет обе строки. При rollback relay не увидит ни одну.

  4. Relay читает зафиксированное событие. В реальном relay нужна также безопасная стратегия конкурентного захвата.

  5. Relay публикует event-42 с ключом order-42. В этой модели ключ выбирает партицию p1.

  6. После ответа брокера relay помечает запись отправленной. Эта отметка не атомарна с публикацией.

  7. Worker A владеет p1. Другие участники могут владеть p0 или p2, но не обрабатывают p1 параллельно в этой группе.

  8. Обработчик вставляет ID обработанного события. Новый ID разрешает бизнес-изменение в той же транзакции.

  9. ID и статус заказа фиксируются вместе. Бизнес-эффект один; Kafka offset ещё не фиксирован.

  10. Только после успеха БД consumer фиксирует offset. Это at-least-once с идемпотентным эффектом, не распределённая exactly-once транзакция.

Обзор

Учебное исследование обработки заказов: transactional outbox, брокер сообщений и идемпотентные обработчики. Схема ниже — иллюстративная модель, а не развёрнутая система.

Задача

Запись заказа в базу и публикация события — независимые операции. Сбой между ними может оставить downstream-сервисы без информации о сохранённом заказе.

Мой вклад

Здесь определены учебные цели и архитектура. Реализация, развёртывание и завершённые этапы пока не подтверждены; проект представлен как работа в процессе.

Решения и компромиссы

  • Записывать заказ и outbox в одной транзакции PostgreSQL. Relay публикует зафиксированные записи в Kafka.
  • Использовать ID заказа как ключ партиции для сохранения порядка внутри неё. Consumer groups распределяют партиции между обработчиками.
  • Атомарно сохранять ID обработанного события и бизнес-изменение. Повторять временные ошибки, а исчерпавшие попытки события отправлять в DLQ.
Компромисс

Доставка at-least-once упрощает восстановление, но допускает дубликаты. Идемпотентность требует надёжного хранилища, outbox — очистки и сопровождения relay. Порядок Kafka действует внутри партиции, не глобально.

Две записи. Окно для сбоя.

В этой схеме PostgreSQL и Kafka не разделяют прикладную транзакцию. Между записью заказа и публикацией остаётся окно: процесс может умереть после commit БД, но до отправки брокеру. Обратный порядок лишь меняет направление несогласованности.

Учебные ограничения явны: надёжное состояние заказа, восстанавливаемое намерение публикации, порядок внутри партиции и безопасная повторная доставка. End-to-end exactly-once не заявлено. Полнота реализации и production-нагрузка не подтверждены.

Outbox закрывает конкретное окно.

Заказ и outbox записываются одной локальной транзакцией. Relay читает зафиксированные записи, отправляет исходный ID и ключ заказа, затем отмечает подтверждение. Если он падает после отправки, запись публикуется снова. Дубликат — осознанная цена восстановления.

Реальному relay нужны конкурентный захват, backpressure, очистка и наблюдение за возрастом неотправленных строк. Polling с блокировками и CDC — альтернативы с разной эксплуатационной ценой. Здесь ни одна не выдаётся за реализованную функцию.

Иллюстративный SQL
-- Illustrative SQL, not extracted production code
BEGIN;
INSERT INTO orders (id, status) VALUES ($1, 'created');
INSERT INTO outbox (event_id, order_id, payload)
VALUES ($2, $1, $3);
COMMIT;
-- Publish only committed rows. Mark sent after broker ack.

Одна фиксация БД защищает два связанных факта. Публикация в Kafka происходит позже.

Сохранённое изменение может прийти снова.

Самый показательный сбой — после бизнес-транзакции, но до фиксации offset Kafka. Новый worker получает запись повторно. Уникальное ограничение на (consumer, event_id) защищает от второго эффекта; in-memory set теряет защиту при рестарте.

Метка обработанного события и бизнес-изменение должны находиться в одной транзакции. При конфликте уникальности изменение не выполняется. Offset фиксируется только после надёжного решения. Offset не является атомарным продолжением commit PostgreSQL.

Иллюстративный SQL
-- Illustrative handler transaction
BEGIN;
-- Requires UNIQUE (consumer, event_id).
WITH accepted AS (
  INSERT INTO processed_events (consumer, event_id)
  VALUES ($1, $2) ON CONFLICT DO NOTHING
  RETURNING event_id
)
UPDATE orders SET status = 'processed'
WHERE id = $3 AND EXISTS (SELECT 1 FROM accepted);
COMMIT;
-- Then commit Kafka offset; never before DB success.

Результат INSERT разрешает изменение. Если ожидаемый заказ не найден, транзакцию нужно откатить, не сохраняя метку неприменённого эффекта.

У порядка есть граница.

Стабильный ключ заказа направляет связанные события в одну партицию Kafka. В каждый момент ей владеет один участник consumer group. Это не глобальный порядок; число consumer сверх числа партиций не добавляет параллелизма топику.

Inline-повторы приостанавливают партицию и сохраняют порядок, но медленная запись блокирует следующие. Retry-топики уменьшают блокировку, меняя семантику порядка. Для DLQ необходимо подтверждение до продвижения исходного offset; поздний replay может нарушить порядок относительно последующих событий.

Проверять окна отказа.

Предлагаемая матрица backend-тестов: остановить relay после ответа брокера до отметки sent; остановить consumer после БД до offset; доставить дубликаты конкурентно; исчерпать повторы. Проверять надёжные строки и offsets, не только HTTP-ответы.

Это стратегия проверки backend, не результаты тестов предоставленного Go-репозитория. Браузерные тесты проверяют объясняющую модель: паузу, replay, узлы, все сценарии, очистку при навигации и reduced motion.

Наблюдать очередь, а не красивый счётчик.

Полезные проектные сигналы: возраст старейшей outbox-записи, ошибки публикации, consumer lag, конфликты идемпотентности, повторы и возраст DLQ. Связывать order ID и event ID в трассах; не помещать высокую кардинальность в labels метрик. Это не живая телеметрия.

Процедура восстановления должна определять владельца разбора DLQ, сохранение ID при replay и допустимые повторные бизнес-состояния. Без этого DLQ — хранилище, а не механизм восстановления.

Что говорят данные

Production-результаты и бенчмарки не заявлены. Интерактивная модель объясняет ожидаемое поведение, но не подтверждает работу реализации на Go/Kafka.

Главный вывод

Гарантия доставки — контракт с операционными последствиями. Обработка дубликатов, повторы, наблюдаемость и восстановление должны входить в проектирование с самого начала.

Продолжить чтениеFuncWarm