Outbox pattern для System Design Интервью

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Я хотела не совсем этого. Точнее - совсем не этого!
Я хотела не совсем этого. Точнее - совсем не этого!

События разворачивались в нескольких реальностях. В первой заказ не пришёл. Во второй - произошло несколько списаний средств. И заказ пришёл. А может даже не один. В третьей система упала посередине процесса, оставив данные в не консистентном состоянии.

А четвертая оказалось самой интересной. Где даже с неполадками сети и падениями частей сервиса был сделан лишь один заказ. И произведена одна оплата. Благодаря чему пришла желаемая теплая пицца. Которую хотел наш дорогой покупатель.

Погрузимся в проблематику оформления заказа и схлопнем все реальности в нужную с помощью Outbox Pattern.

Outbox pattern за 30 секунд

Базовая архитектура

Клик для увеличения.
Клик для увеличения.

Раскроем сервис заказа пиццы:

  1. Orders Service — содержит основную бизнес-логику по обработке заказа.

  2. Orders DB — реляционная СУБД. Имеет таблицы: orders, outbox.

  3. Outbox Relay/worker — компонент, который читает таблицу outbox и пишет в брокер, помечая записи как доставленные.

  4. Message Broker — наш middleware с очередями/топиками для передачи событий.

  5. Consumers — Billing/Inventory и др. с идемпотентной обработкой.

Базовая логика применения паттерна:

Сервис ордеров хранит заказы в своей БД. При создании заказа в той же транзакции кладёт событие в таблицу outbox. Отдельный процесс вычитывает outbox и публикует в брокер сообщений. Другие сервисы (биллинг, склад) подписываются и обрабатывают события.

Проблематика

Зачем всё это нужно?
Зачем всё это нужно?

Проблема в том, что order service взаимодействуя с окружением может пойти лишь по одному из двух путей:

  1. Сначала сохранить ордер в БД и затем оповестить другой сервис/создать событие.

  2. Сначала оповестить другой сервис, затем сохранить ордер в БД

И в каждом из сценариев где-то посередине сервис может упасть не сделав очередное действие.

Всё везде и сразу не получится!
Всё везде и сразу не получится!

Поэтому мы и расшиваем логику в отдельные сущности.

Семантика доставки в распределённых системах

А как оно работает внутри?
А как оно работает внутри?

События

Можно заметить, что мы ввели понятие “события“. При проектирование системы можно выбрать нужную архитектуру в зависимости от требований. К примеру, монолит - когда основная логика выполняется в одном процессе. Или же микросервисная архитектура с синхронным взаимодействием - все друг друга ждут.

Outbox pattern живёт в рамках Event Driven Architecture(EDA) - архитектуры, построенной на основе обмена событий.

Примеры:

  1. Создан заказ

  2. Списаны деньги

  3. Зарезервирован товар

Семантика. Теория

Далее нам важно понимать семантику доставки. Она выражает гарантии, которые мы даём потребителям сообщений. На практике чаще всего обсуждают три варианта:

  • At-most-once — «не более одного раза». Событие может потеряться, но дубликатов не будет.

  • At-least-once — «как минимум один раз». Событие не потеряется, но возможны дубликаты.

  • Exactly-once — «ровно один раз». Достигается комбинацией at-least-once + идемпотентность/дедупликация на стороне обработчика.

Семантика. Практика

Мы не хотим терять сообщения. Конечно же, наши сервисы имеют хорошие показатели доступности. Это известные всем и бесполезные с практической точки зрения на интервью 9ки. И сеть почти никогда не рвется. Но именно доли процента отказов на высоких нагрузках будут портить жизнь нашим дорогим пользователям. Больше нагрузка → больше потерь.

Поэтому как и в других аспектах System Design для обеспечения лучших гарантий мы вводим избыточность. Выбираем at-least-once. Да, могут быть дубли. Но мы с ними справимся благодаря идемпотентной обработке.

Паттерн outbox подробней

Классическое изображение паттерна с microservices.io
Классическое изображение паттерна с microservices.io

1. Клиент вызывает POST /api/v1/orders.
2. В единой транзакции:

а) вставляется запись в orders;
б) вставляется запись в outbox (event_id, event_type, payload, occurred_at, status='NEW').

3. Транзакция коммитится → мы атомарно зафиксировали и состояние, и запись/событие. Cпасибо ACID'у(а конкретно какой букве? :) ). На этом наш order service кланяется и может падать сколько хочет. Главное сделано - зафиксирован ордер и создана запись. Что будет дальше - не его дело.
4. Outbox Relay:

  1. Вычитывает новые записи

  2. Публикует в брокер

  3. Переводит запись в статус “обработано“

Он может упасть после публикации в брокер. Поднимется. И снова вычитает туже вроде бы не обработанную запись. Снова перешлёт. И, наконец, переведёт ей в статус “обработано“. Мы пошли на это сознательно. Допускаем такие дубликаты в брокер.

5. Потребители читают из брокера. Каждый обработчик проверяет, не обрабатывали ли уже событие.

Это даёт at-least-once между outbox и брокером, а вместе с идемпотентностью у потребителей — exactly-once effects на уровне выполнения бизнес логики.

Общая схема

Пора финалить
Пора финалить
Всё вместе
Всё вместе

Основная таблица для сервиса заказа:

CREATE TABLE orders (
 id uuid PRIMARY KEY,
 status text NOT NULL,
 amount bigint NOT NULL,
 created_at timestamptz NOT NULL default now()
);

CREATE TABLE outbox (
 event_id uuid PRIMARY KEY,
 event_type text NOT NULL,
 payload jsonb NOT NULL,
 status text NOT NULL CHECK (status IN ('NEW','SENT','ERROR')),
 occurred_at timestamptz NOT NULL default now(),
 sent_at timestamptz
);

Идемпотентность - что это и зачем бизнесу

Идемпотентность

Это свойство операции давать один и тот же наблюдаемый результат при повторном выполнении.
В контексте событий и очередей это означает: если одно и то же событие обработается два, три и больше раз, бизнес‑состояние не поменяется сверх первого применения.

Зачем бизнесу

Защита от двойного списания и двойных доставок, отсутствие «накрутки» бонусов/скидок, предсказуемость SLA при сбоях и ретраях, меньше тикетов в саппорт и выше доверие клиентов. Благодаря идемпотентности можно использовать семантику доставки at‑least‑once и всё равно получать ровно один бизнес‑эффект.

Идемпотентность потребителей: короткий рецепт

Таблица дедупликации в своей БД (а не в БД ордеров):

CREATE TABLE processed_events (
  event_id     uuid NOT NULL,
  handler      text NOT NULL,
  processed_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (event_id, handler)
);

Что важно:

  • event_id генерирует продюсер при записи в outbox и кладёт в сообщение.

  • handler чаще всего = billing, inventory-metrics, … (имя consumer group при использование кафки).

  • Ack/commit offset — только после коммита БД потребителя.

Выводы

Осчастливили нашу дорогую покупательницу!
Осчастливили нашу дорогую покупательницу!
  • Паттерн outbox позволяет развязать логику сохранения заказа и его обработки.

  • Между outbox и брокером - at-least-once. Поэтому дубликаты неизбежны.

  • Exactly-once достигается идемпотентностью у потребителя.

На System Design Интервью вы дойдёте до использования паттерна после описание основной схемы. В финальных минутах, на которых зайдёт речь про выявление bottlenecks и повышения отказоустойчивости системы.

Теперь вы знаете как правильно спроектировать распределенную систему с использованием outbox pattern!
Теперь вы знаете как правильно спроектировать распределенную систему с использованием outbox pattern!

Приветствую! Меня зовут Невзоров Владимир. Подробней о нашей исследовательнице сервиса заказа на моём канале System Design World, посвященному Архитектуре, System Design, Highload бэкэнду.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Разобрать в следующий раз
0%Websockets для интервью. В сравнение с polling, в применение к System Design задачам0
0%Аутентификацию. От простой к сложной — с токенами, масштабированием0
0%Обожаемый на интервью Consistent Hashing. Кольцо, каскадные сбои0
0%Уровни изоляции транзакций. Advanced level. С примерами, конечно.0
Никто еще не голосовал. Воздержавшихся нет.
Источник: https://habr.com/ru/articles/962286/


Интересные статьи

Интересные статьи

Подземелье знаний открыто не для избранных. Оно ждёт тех, кто готов идти — не быстро, но вдумчиво. Антон пришёл сюда, потому что устал от бессмысленных отчётов и хочет наконец делать что-то настоящее....
Привет! Меня зовут Александра, я системный аналитик в компании, которая помогает бизнесу автоматизировать процессы с помощью Битрикс24.В этом кейсе я расскажу о небольшом проекте по внедрению системы ...
Приветствую! В данной статье мы расскажем об одном из курсов Школы Тестирования КОТ (Как Обучить Тестировщика) Блока качества РСХБ-Интех, посвящённом нагрузочному тестированию. Кур...
Меня зовут Андрей Буйновский, я разработчик в ИТ-компании. Мы с командой решили написать эту статью и поделиться некоторыми проблемами, подводными камнями и их решениями в разработке мобильного прилож...
Кто бы что ни говорил, но я считаю, что изобретение велосипедов — штука полезная. Использование готовых библиотек и фреймворков, конечно, хорошо, но порой стоит их отложить и создать ...