Event Sourcing & CQRS Vocabulary: Патерни для розподілених систем
Сховище подій, проекції, команди проти запитів, можлива послідовність і словник CQRS/ES для розробників розподілених систем.
Розподілені системи мають свій власний діалект. Коли ваша команда почне обговорювати архітектуру, керовану подією, ви швидко зіткнетеся з щільним кластером взаємопов’ язаних термінів — пошуку подій, CQRS, проекцій, саги — які можуть здатися вам приголомшливими, якщо ви раніше стикалися лише з традиційними системами, заснованими на CRUD. У цьому довіднику розглянуто основні слова, що використовуються у пошуку джерел подій і CQRS, пояснено кожен з цих термінів простою англійською мовою, а також показано, у яких розмовах ці слова використовуються у справжніх інженерних командах.
Основні терміни: події і команди
Вся парадигма базується на фундаментальній відмінності між двома типами повідомлень.
** Команда ** — повідомлення, що виражає * намір *: запит на те, щоб система щось зробила. Команди написані в імперативному настрої і можуть бути відхилені.
«Ми моделюємо дії користувача як команди —
PlaceOrder,CancelSubscription,UpdateShippingAddress. Система перевіряє їх перед тим, як щось робити»
“Якщо перевірка зазнає невдачі, команда відкидається і нічого не записується. Це і є краса в цьому — побічні ефекти трапляються тільки тоді, коли задовольняються бізнес-правила»
** Подія ** — повідомлення, яке записує * факт *: щось, що вже сталося. Події незмінні і завжди записуються у минулому часі.
“Якщо команда прийнята, ми випускаємо
OrderPlaced. Ця подія є джерелом правди — ми ніколи не вилучаємо або змінюємо її»
UserDeactivatedне означає «будь ласка, деактивуйте цього користувача» — це означає «цей користувач був деактивований в цей час, кінець історії»
** Визначення джерела подій ** — архітектурний шаблон, за якого стан програми повністю визначається послідовністю подій, а не змінним записом у базі даних. Замість збереження поточного стану, ви зберігаєте кожну зміну стану як подію.
“Ми перейшли на пошук подій, тому що ми продовжували втрачати історію аудиту. Тепер у нас є повний журнал всього, що коли-небудь траплялося з замовленням»
«Складна частина пошуку подій полягає в тому, що ви не можете просто запустити
SELECT— ви повинні відтворити події, щоб відновити поточний стан»
Зберігання і відновлення
** Склад подій ** — спеціалізована база даних (або спеціально створена таблиця у реляційній базі даних), оптимізована для запису подій лише за допомогою додавання. Популярними варіантами є EventStoreDB, Apache Kafka, і нетипові реалізації на PostgreSQL.
“Наш магазин подій доступний тільки для додатків. Ніхто не оновлює рядки — ми тільки вставляємо нові події»
«Ми обрали EventStoreDB, тому що він дає нам вбудовані підписки і прогнози з коробки»
** Потік подій ** — упорядкована послідовність подій, пов’ язаних з одним агрегатом (наприклад, всі події для Order-42 ). Кожен потік має унікальний ідентифікатор і монотонно зростаюче число версії.
“У кожного порядку є свій поток подій. Коли ви завантажуєте замовлення, ви отримуєте його поток і відтворюєте його знову»
«У нас була помилка, коли два процеси записували в один і той же потік одночасно. Оптимістична перевірка одночасності негайно виявила конфлікт»
** Снімок ** — серіалізоване представлення стану агрегату у певній точці часу, зберігається разом з потоком подій, щоб уникнути повторення тисяч подій при кожному завантаженні.
«Після 500 подій на одному агрегаті, повторення з нуля займало 200 мілісекунд. Ми ввели знімок кожних 100 подій і знизили це до менш ніж 5 мс»
«Знімок — це оптимізація, а не вимога. Почати без них і додати їх, коли тестування на навантаження показує, що вони вам потрібні»
** Повторення ** — процес повторної обробки послідовності історичних подій, або для відновлення стану агрегату, або для відновлення прочитаної моделі з нуля.
«Коли ми розгорнули нову службу звітів, ми відтворили шість місяців подій, щоб заповнити її базу даних»
“Повторення є однією з наддержав пошуку подій. Ви можете отримати будь-який вигляд ваших даних в будь-який момент в історії»
CQRS і читання моделей
** CQRS (Command Query Responsibility Segregation) ** — шаблон, який відокремлює сторону запису системи (команди, які змінюють стан) від сторони читання (запити, які повертають дані). Дві сторони можуть використовувати абсолютно різні моделі даних, бази даних і стратегії оптимізації.
«Ми прийняли CQRS, тому що наша модель запису була сильно нормалізованою, але наші запитуванні на панелі управління потребували денормалізованих агрегацій. Тепер вони повністю відокремлені.»
«CQRS не вимагає пошуку подій, але два шаблони доповнюють один одного дуже добре»
** Модель запису ** — частина системи, відповідальна за обробку команд, застосування бізнес- правил і тривалих подій. Його оптимізовано для послідовності і коректності, а не для швидкодії запиту.
“Модель запису дбає тільки про те, чи є команда чинною. Вона не знає і не турбується про те, як дані виглядають для кінцевого користувача»
** Read model ** — денормалізований, оптимізований для запиту перегляд даних, створений за допомогою подій, що відбуваються на стороні запису. Також називається проекція (див. нижче). Можуть існувати декілька моделей читання, що обслуговують різних споживачів.
«Ми маємо три моделі читання для замовлень: одну для стану, що звертається до клієнта, одну для складського підбору і одну для фінансової звітності»
“Якщо модель читання пошкоджується, ми просто відкидаємо її і граємо знову. Це ключова перевага — це одноразове використання»
** Проекція ** — процес (або його вивід) перетворення потоку подій у модель для читання. Проекція підписується на події і відповідно оновлює сховище даних, яке можна запитати.
Проекція слухає події
OrderShippedі оновлює кеш Redis, який читає мобільне застосування
“У нас була відстань у проекції на три години під час піку споживання Кафки. Користувачі бачили застарілі дані, поки вони не наздоганяють»
Постійність і надійність моделей
** Послідовність можливих результатів ** — властивість розподілених систем, за якої після запису всі моделі читання * з часом * відображатимуть новий стан — але не обов’ язково відразу. Це компроміс, який ви приймаєте з проекціями CQRS.
«Ми мусили навчити команду продукту, що читання врешті-решт є послідовними. Якщо ви зробите замовлення і негайно оновите, ви можете не бачити його протягом декількох сотень мілісекунд»
“Відповідність є в порядку для 95% випадків використання. Для 5%, які потребують сильної послідовності, ми зберігаємо їх на синхронних шляхах»
** Ідемотентний обробник подій ** — обробник подій, розроблений так, щоб обробка однієї події декілька разів давала той самий результат, що і обробка її один раз. Необхідний у розподілених системах, де повідомлення надсилається принаймні один раз.
“Всі наші проекції є ідемпотентними. Якщо повідомлення знову доставляється, ми перевіряємо, чи ми вже обробляли цей ідентифікатор події і пропускаємо його»
“Зробити обробники ідемпотентними не підлягає обговоренню, коли ви використовуєте Kafka. Ви отримаєте подвійні повідомлення»
** Saga / менеджер процесів ** — координатор потоку робіт з довгим часом роботи, який реагує на події і видає команди для організації багатокрокового бізнес- процесу у багатьох агрегатах або службах. Іноді викликається менеджер процесів, коли він підтримує явний стан.
«Сага виконання замовлення слухає
PaymentConfirmed, потім відсилаєReserveInventory, потім чекаєInventoryReservedперед відправкою команди доставки»
“Сага замінює розподілені транзакції. Замість двофазного затвердження, ви розробляєте компенсаційні команди для шляхів невдач
** Шаблон вихідних повідомлень ** — метод надійності, за якого команди або події спочатку записуються до таблиці * вихідних повідомлень * у тій самій локальній базі даних, що і транзакція зміни домену, а потім окремий процес публікує їх у брокері повідомлень. Це гарантує принаймні одну доставку без розподілених транзакцій.
«Без шаблону outbox, у нас була гонка умов, де запис бази даних був успішним, але публікація Kafka зазнала невдачі. Ми втратили події»
“Візуалізація вихідних повідомлень вирішила нашу проблему подвійного запису. Тепер рядок бази даних і подія або обидва затверджені, або жоден з них не є»
Як використовувати їх у розмові
Зрозуміти ці терміни академічно - це одне, а використовувати їх природно в звичайних ситуаціях, перегляді коду і обговоренні архітектури - це інше. Ось деякі типові сценарії.
Сценарий 1 — обновление в режиме ожидания:
“Прогноз для панелі аналітики відстав за ніч. Я визначив, що це була помилка в обробнику, яка приводить до пропуску подій. Я розгортаю виправлення і переграю з вчорашнього зніму»
** Сценарій 2 — Перегляд архітектури: **
“Я б запропонував використовувати шаблон outbox тут, а не публікувати безпосередньо в Kafka. Таким чином команда і подія записуються атомарно і ми отримуємо принаймні один раз гарантії доставки без жодних проблем»
** Сценарій 3 — Коментар перегляду коду: **
«Ця обробка не є ідемпотентною — якщо одна і та ж подія доставляється двічі, ми створимо дублікат запису. Ми повинні перевірити на ідентифікатор події в моделі читання перед застосуванням оновлення.”
** Сценарій 4 — Введення нового члена команди: **
“У цій кодовій базі, модель запису живе в пакунку
domain, а моделі читання знаходяться вprojections. Склад подій — EventStoreDB. Коли ви завантажуєте агрегат, ви відтворюєте його поток подій — якщо тільки не доступний знімок, в якому випадку ви завантажуєте його і відтворюєте тільки події після нього
Краткий справочник
| Term | Type | One-line definition |
|---|---|---|
| Command | Message | Expresses intent; may be rejected |
| Event | Message | Records a past fact; immutable |
| Event sourcing | Pattern | State derived from an append-only event log |
| Event store | Infrastructure | Append-only storage optimised for events |
| Projection | Process / output | Transforms events into a queryable read model |
| Snapshot | Optimisation | Cached aggregate state to avoid full replay |
| CQRS | Pattern | Separates write model from read model |
| Eventual consistency | Property | Reads converge to correct state, but not instantly |
| Idempotent handler | Design property | Safe to process same event multiple times |
| Outbox pattern | Reliability pattern | Atomic write + guaranteed message publish via outbox table |