open atlas
↑ К треку
Основы System Design SD · 06 · 03

Событийная архитектура

Событийные системы координируются фактами, а не вызовами — хореография против оркестрации. Несущий паттерн — outbox: делает «записать в БД и опубликовать событие» атомарным, побеждая двойную запись ценой согласованности в конечном счёте, которую надо показать в UX.

SD Middle ◷ 20 min
Уровень
ОсновыJuniorMiddleSenior

Сервис заказов на checkout делал две вещи: записывал заказ в свою БД, затем публиковал событие «заказ оформлен», чтобы склад и доставка могли отреагировать. В тестах всё работало каждый раз. В проде, раз в несколько тысяч заказов, коммит в БД проходил успешно, а затем процесс умирал до публикации события. Заказ существовал; мир о нём так и не услышал. Склад не уменьшил остаток, не создалась накладная, и заказ платящего клиента тихо растворился в состоянии, на которое ни один сервис не знал, как реагировать. Баг был не в одном из шагов. Он был в зазоре между ними — две системы обновлены без транзакции, охватывающей обе. Этот зазор — проблема двойной записи, и её решение — сердце этого урока.

Координация фактами вместо вызовов

В request-driven системе сервис A заставляет сервис B что-то сделать, вызывая его: A держит поток управления, ждёт и зависит от того, жив ли B. В событийной архитектуре (EDA) A вместо этого испускает факт — «заказ оформлен» — в топик и идёт дальше; кто заинтересован, реагирует по своему расписанию. Поток управления инвертируется: producer больше не командует downstream’ами, он объявляет, что произошло, и поведение системы складывается из независимых реакций на эти факты.

Выигрыш — слабая связность из урока про pub/sub, поднятая на уровень архитектуры: сервисы не знают друг о друге, возможности добавляются добавлением подписчиков, и один медленный сервис не блокирует остальных. Цена — ты отказываешься от простого, трассируемого потока управления «A вызвал B вызвал C». Поведение теперь распределено по реакциям, отладка охватывает сервисы, и — тема всего этого раздела — ты наследуешь последствия асинхронности: дубли, порядок и согласованность в конечном счёте.

Хореография против оркестрации

Бизнес-процессу обычно нужно несколько шагов (оформить заказ → списать → зарезервировать товар → отгрузить). Координировать их можно двумя способами, и неверный выбор плохо масштабируется.

Хореография — нет центрального мозга. Каждый сервис слушает события и испускает свои: сервис заказов испускает OrderPlaced; платежи слышат, списывают, испускают PaymentTaken; склад слышит это, резервирует товар, испускает StockReserved; доставка реагирует. Логика — это сумма реакций, размазанная по сервисам. Это максимально развязано и отлично для простых потоков — но для сложного процесса сквозная логика не существует нигде: ни одно место не говорит «вот что делает оформление заказа», и поток трудно увидеть, менять и отлаживать.

Оркестрация — центральный координатор («оркестратор» или сага) явно ведёт шаги: он велит платежам списать, ждёт результата, велит складу зарезервировать, а при сбое запускает компенсирующие действия (возврат, освобождение товара), чтобы откатить. Поток живёт в одном читаемом месте, что куда лучше для сложных, многошаговых, не-должных-завершиться-наполовину процессов — ценой возврата компонента, знающего про всех (хотя командует он через события/команды, а не синхронными цепочками).

Правило большого пальца: хореография для простых, малошаговых реакций, где развязка важнее всего; оркестрация, когда процесс сложен, имеет компенсации или кому-то нужно ответить «где сейчас заказ #8842 в потоке?».

Почему это работает

Почему не продолжать хореографить по мере роста процесса? Потому что хореография распределяет логику, но не даёт места для состояния всего процесса. Когда пять сервисов реагируют каждый на событие предыдущего, ни один компонент не знает, завершил ли заказ #8842 шаг 3 или застрял, а сбой в середине оставляет осиротевшее частичное состояние, за уборку которого никто не отвечает. Ты кончаешь тем, что восстанавливаешь поток из логов пяти сервисов, чтобы ответить на один вопрос. Оркестратор намеренно централизует это состояние процесса и логику сбоев/компенсаций — ты меняешь немного связности на возможность видеть, возобновлять и откатывать долгую транзакцию. Ошибка — трактовать «хореография = всегда развязаннее = всегда лучше»; за несколькими шагами отсутствующее состояние процесса становится доминирующей ценой.

Event sourcing и CQRS вкратце

Проектируя событийную систему, ты быстро столкнёшься с двумя паттернами, которые звучат фундаментально, но нередко применяются раньше времени — стоит знать их, чтобы понимать, когда они оправданы. Два паттерна сопутствуют EDA, и их стоит узнавать. Event sourcing: вместо хранения текущего состояния с перезаписью ты хранишь последовательность событий как источник истины (AccountOpened, Deposited 100, Withdrew 30) и выводишь текущее состояние, переигрывая их. Получаешь идеальный аудит-лог и возможность перестроить любую проекцию — и ровно поэтому удержанный, реплеябельный лог (из урока про pub/sub) — естественная подложка. CQRS (разделение ответственности команд и запросов): раздели write-модель (команды, нормализованные ради корректности) от read-модели (запросы, денормализованные ради скорости), держа read-сторону обновлённой потреблением событий write-стороны. Оба мощны и оба покупают операционную сложность и согласованность в конечном счёте — тянись к ним, когда аудит-трейл или независимое масштабирование чтения/записи реально это оправдывают, а не по умолчанию.

Паттерн outbox: победа над проблемой двойной записи

Теперь сердце. Баг из хука — это проблема двойной записи (dual-write): обработчик должен обновить свою БД и опубликовать событие, но это две отдельные системы без общей транзакции. В каком бы порядке ты их ни ставил, падение в зазоре портит состояние: коммит-затем-публикация теряет событие (хук); публикация-затем-коммит может испустить событие для записи, которая затем откатывается (фантомное событие, на которое реагируют downstream’ы). Сделать коммит в БД и публикацию в брокер атомарными напрямую нельзя.

Транзакционный outbox превращает две записи в одну. В той же локальной транзакции БД, что пишет бизнес-состояние, ты также вставляешь событие в таблицу outbox. Поскольку обе строки в одной транзакции, они коммитятся или откатываются вместе — атомарность, которой нельзя было получить между системами, возвращена удержанием обеих записей в системе, у которой есть транзакции. Затем отдельный процесс-ретранслятор (relay) читает неопубликованные строки из outbox и публикует их в брокер, помечая каждую сделанной после подтверждения.

ОДНА локальная транзакция:
  INSERT order (id=8842, ...)            ← бизнес-состояние
  INSERT outbox (event="OrderPlaced")    ← событие к публикации
  COMMIT                                  ← обе, или ни одной

Relay (отдельный, асинхронный):
  SELECT * FROM outbox WHERE published = false
  опубликовать каждое в брокер → при ack пометить published = true

Relay публикует at-least-once (он может упасть после публикации, но до пометки строки сделанной, переопубликовав при рестарте) — что замыкает нас обратно на урок 1: consumer’ы обязаны быть идемпотентны, потому что outbox гарантирует, что событие никогда не потеряется, а не что оно доставлено ровно один раз. Outbox меняет «потерянные события» на «дубли событий», а дубли ты уже умеешь обрабатывать.

Согласованность в конечном счёте — это проблема UX, а не только бэкенда

Асинхронность означает, что остальная система узнаёт о факте после того, как он случился — есть окно, когда заказ существует, а склад ещё не отреагировал. Эта согласованность в конечном счёте (eventual consistency) — неизбежное следствие обмена синхронных вызовов на события, и senior-ошибка — трактовать её как чисто бэкенд-заботу. Она протекает прямо в UX: пользователь, оформивший заказ и тут же обновивший страницу, может его не увидеть; проекция «ваш баланс», питаемая CQRS read-моделью, может отставать от записи на удар. Надо проектировать интерфейс под задержку — оптимистичный UI, что сразу показывает ожидаемый результат, состояние «обрабатывается…», read-your-own-writes для действующего пользователя или явное сообщение «может занять момент». Притворяться, что система синхронна, когда это не так, — вот как ты выпускаешь сбивающий с толку опыт «сработал ли мой клик?».

Частая ошибка

Тонкий сбой outbox: делать вставку в outbox, но публиковать событие прямо из обработчика «ради экономии хопа», вместо publish из relay. Теперь ты снова в двойной записи — обработчик может упасть после коммита outbox-строки, но ты также попробовал inline-публикацию, которая могла случиться, а могла нет, и ты не получил ничего. Дисциплина строга: единственная задача обработчика — локальная транзакция (бизнес-строка + outbox-строка); отдельный relay владеет всей публикацией. Держи их раздельно, или паттерн тихо деградирует обратно в баг, который должен был чинить.

Викторина

Обработчик делает `db.commit(order)`, затем `broker.publish(event)`. Изредка заказ сохранён, но событие так и не срабатывает. Что это и как outbox это чинит?

Викторина

6-шаговый процесс выполнения заказа должен поддерживать возвраты/освобождение товара при сбое, и эксплуатация всё спрашивает «где этот заказ в потоке?». Хореография или оркестрация?

Закончи аналогию

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

Это высота композиции — выбор стилей координации и outbox для дизайна. Отдельный трек queues уходит глубже в transactional/idempotent producer’ы на стороне брокера и exactly-once stream-обработку; тянись туда, когда реализуешь relay против конкретного брокера, и оставайся здесь, чтобы решить хореография против оркестрации и нужен ли тебе outbox вообще.

Вспомните перед уходом
  1. 01
    Что инвертируется в event-driven против request-driven и чего это стоит?
  2. 02
    Хореография против оркестрации — определи каждую и правило большого пальца.
  3. 03
    Что такое проблема двойной записи и как outbox её решает?
Итог

Событийная архитектура координируется, испуская факты, а не делая вызовы: producer объявляет «что произошло», и поведение системы складывается из независимых реакций, покупая слабую связность ценой трассируемого потока управления и наследуя последствия асинхронности. Многошаговые процессы координируются хореографией (сервисы реагируют на события друг друга — развязано, лучше для простых потоков, но логика и состояние процесса не живут нигде) или оркестрацией (центральная сага ведёт шаги и запускает компенсирующие действия при сбое — лучше, когда поток сложен или не должен завершиться наполовину). Event sourcing (события как источник истины, переигрываемые для вывода состояния) и CQRS (разделение write- и read-моделей) сопутствуют на удержанном логе и оправдывают сложность лишь когда аудит или независимое масштабирование того требуют. Несущий паттерн — транзакционный outbox, побеждающий проблему двойной записи: пиши бизнес-строку и строку события в одной локальной транзакции, затем пусть отдельный relay публикует асинхронно — возвращая атомарность внутри той единственной системы, у которой есть транзакции. Поскольку relay публикует at-least-once, гарантия это «никогда не потеряно, возможно дублировано», поэтому consumer’ы обязаны быть идемпотентны; а итоговая согласованность в конечном счёте — проблема UX, под которую ты проектируешь (оптимистичный UI, состояния обработки, read-your-own-writes), а не деталь бэкенда, которую можно спрятать. Отдельный трек queues покрывает transactional producer’ы на стороне брокера; оставайся здесь, чтобы выбрать стиль координации и решить, нужен ли outbox. Теперь, когда встретишь заказ, что есть в базе, но так и не запустил доставку, — ищи зазор: была ли там двойная запись без outbox или хореография без того, кто отвечает за пропущенный шаг?

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.