awesome-everything EN
↑ Обратно к восхождению

Data engineering

Event sourcing: append-only лог как источник истины

Суть Храни неизменяемый поток событий, меняющих состояние, а не само состояние — текущее состояние это свёртка лога слева. Покупаешь аудит, путешествие во времени и реплей; платишь версионированием, GDPR, снапшотами и eventual consistency.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 17 min

Клиент оспаривает списание: «Я никогда не ставил годовой план». Поддержка открывает строку — plan: annual — и разводит руками. Но таблица accounts хранит только текущее значение; UPDATE, который его выставил, затёр то, что было до этого, а логи приложения ротировались две недели назад. Никто не может сказать, какой план был 14-го, кто его менял и не сделал ли это глючный webhook. В event-sourced системе это запрос на тридцать секунд: проиграть поток до этого момента и прочитать ответ. Разница не в том, что лог лучше — а в том, что лог и есть база данных.

Состояние — это свёртка событий слева

Обычная CRUD-таблица хранит последний снимок и уничтожает историю на каждом UPDATE. Event sourcing переворачивает это: append-only хранилище событий держит упорядоченную неизменяемую последовательность фактов — AccountOpened, PlanChanged, CardDeclined — а текущее состояние это просто то, что получается свёрткой редьюсера по ним с самого начала. state = events.reduce(apply, initial). События — источник истины; «текущее состояние» — производный кэш, который можно выбросить и пересчитать в любой момент.

Из этой одной инверсии берётся вся польза и вся цена. Ты получаешь полный аудит-трейл бесплатно (лог и есть аудит). Получаешь темпоральные запросы — состояние на любой прошлый момент, свёрткой только событий до этого timestamp. Получаешь отладку реплеем: копируешь продакшен-поток в staging, прогоняешь его через новый код и смотришь, как баг детерминированно воспроизводится. Ничто из этого не прикручено сбоку; всё выпадает из того, что данные никогда не выбрасываются.

Ограничение append-only несущее. Ты никогда не делаешь UPDATE или DELETE события. Ошибка исправляется добавлением компенсирующего события (PlanCorrected), так же как бухгалтер никогда не стирает запись в книге — он проводит сторнирующую. Поэтому хранилища событий оптимизированы под одно: быстрые append и быстрое последовательное чтение потока.

Event sourcing — это не «Kafka» и не change log

Это различие сеньоры путают чаще всего. Лог change-data-capture или обычный топик Kafka записывает события, но само по себе это не event sourcing. Определяющее свойство — лог это авторитетный источник истины, и состояние восстанавливается из него, а не побочный канал изменений, эмитированный после того, как база данных уже зафиксировала истину где-то ещё.

Kafka может служить хранилищем событий, но с острыми оговорками. Компакция лога — флагманская фича Kafka — оставляет только последнее значение на ключ, что прямо уничтожает историю, на которой держится event sourcing; нужно использовать топики на retention с log.retention.ms = -1 (бесконечный), а не компактируемые. Оптимистичная конкурентность на агрегат («append только если я на версии N») не имеет чистого примитива в Kafka, тогда как специализированное хранилище вроде EventStoreDB делает expectedVersion first-class условием append. Частый продакшен-паттерн: держать сырой топик событий вечно как источник, а производное текущее состояние публиковать в отдельный компактируемый топик для read-моделей — компактируемый топик это проекция, никогда не истина.

СвойствоCRUD-таблицаChange log / CDCEvent sourcing
Источник истиныТекущая строкаБаза, за которой он следитСам лог событий
ИсторияТеряется на UPDATEЧасто ограничена по времениПолная, навсегда
Состояние на прошлый моментНевозможноСложно / частичноСвёртка до timestamp
Построить новый viewСкрипты бэкфиллаОграничено retentionРеплей всего лога

CQRS: проекции — это одноразовые read-модели

Нельзя обслуживать запрос вроде «покажи дашборд», сворачивая весь лог на каждый запрос — это было бы разорительно медленно. Поэтому event sourcing почти всегда идёт в паре с CQRS: сторона записи добавляет события; сторона чтения гоняет проекции, которые потребляют поток и материализуют под задачу read-модели (SQL-таблицу, индекс Elasticsearch, денормализованный кэш). Каждая проекция — крошечная программа: на каждое событие обнови свою таблицу. Поскольку проекции производные, они одноразовые — дропни таблицу, проиграй лог, получи её назад. Нужен совершенно новый view через полгода? Напиши проекцию и прогони через неё историю; данные были там всегда.

Два свойства делают это безопасным в продакшене. Первое: проекции должны быть идемпотентными: одно и то же событие может быть доставлено больше одного раза (ретраи, доставка at-least-once), поэтому применить его дважды должно равняться применить один раз. Стандартный механизм — отслеживать последнюю обработанную версию события на поток и игнорировать любое событие, чья версия <= последней виденной. Второе: сторона чтения eventually consistent: между добавлением события и тем, как проекция догонит, есть реальный лаг. EventStoreDB и похожие хранилища документируют этот лаг read-модели явно — read-модель «сходится к корректному состоянию со временем», она не гарантированно актуальна в любой момент.

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

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

Сложные части: версионирование, GDPR и цена реплея

Вот где event sourcing зарабатывает свою репутацию. Версионирование схемы неизбежно и постоянно: ты никогда не сможешь удалить старую форму события, потому что старые события, записанные в этой форме, живут в логе вечно и обязаны оставаться воспроизводимыми. Когда OrderPlaced обзаводится полем, у каждого исторического OrderPlaced его нет. Стандартный ответ — upcasting: цепочка функций-трансформаций, которые поднимают старую форму события к текущей при десериализации, так что остальной код видит только последнюю версию. Апкастеры накапливаются; это код, который ты несёшь бесконечно.

GDPR-«право быть забытым» сталкивается лоб в лоб с неизменяемым логом. Ты юридически обязан стереть персональные данные пользователя, но лог append-only, и ты не можешь переписать историю, не сломав каждый последующий реплей и гарантию аудита. Доминирующая техника — crypto-shredding: шифровать PII каждого пользователя ключом на пользователя, хранимым вне лога; чтобы «забыть» его, выбрось ключ, сделав шифротекст невосстановимым навсегда, при этом структура события остаётся целой. Заметь острую оговорку, которую сеньор обязан подсветить: регуляторы всё ещё могут считать неудаляемый зашифрованный PII персональными данными, так что crypto-shredding — прагматичная мера, а не гарантированный юридический выигрыш; подключай юристов.

Наконец, цена реплея неограничена, если ничего не делать. Свёртка миллионов событий, чтобы загрузить один агрегат, становится медленной; финансовая система, проигрывающая терабайты тиков цен, может тратить минуты на пересборку. Фикс — snapshotting: периодически сохранять свёрнутое состояние на версии N, затем при загрузке читать снапшот и проигрывать только события после N. Снапшоты — чистая оптимизация производительности и грабли: если снапшот разошёлся с событиями (поменялась логика, потерялась запись посреди потока), он молча отдаёт неверное состояние и маскирует баг, потому что реплей его никогда не пересчитывает. Защищающиеся команды считают чексумму снапшота и пересобирают при несовпадении.

Выбери лучший вариант

Пользователь требует GDPR-стирания. Его PII вшит в сотни неизменяемых событий в append-only хранилище. Выбери подход, который сеньор реально отгружает.

Викторина

Загрузка одного агрегата теперь сворачивает 4 миллиона событий и длится слишком долго. Какой фикс сеньора?

Викторина

Почему проекция, строящая read-модель, обязана быть идемпотентной?

Расставь шаги по порядку

Расставь жизненный цикл записи в event-sourced + CQRS системе:

  1. 1 Приходит команда; загрузи агрегат свёрткой его событий (или снапшот + хвост)
  2. 2 Проверь команду относительно текущего свёрнутого состояния
  3. 3 Добавь получившееся событие(я) в поток с проверкой ожидаемой версии
  4. 4 Проекции потребляют новое событие асинхронно и обновляют read-модели
  5. 5 Запросы читают (eventually consistent) read-модель, а не сырой лог
Вспомните перед уходом
  1. 01
    Коллега говорит: «мы уже публикуем события в Kafka, значит мы event-sourced». Объясни, почему это может быть неправдой и что реально сделало бы это event sourcing.
  2. 02
    Пройди по тому, почему версионирование схемы в event sourcing постоянно, и как upcasting справляется с этим без переписывания истории.
Итог

Event sourcing хранит неизменяемый append-only поток событий, меняющих состояние, как источник истины и выводит текущее состояние свёрткой этого лога слева; «текущее состояние» — одноразовый кэш, который можно пересчитать когда угодно. Эта одна инверсия покупает полный аудит-трейл, темпоральные запросы (состояние на любой прошлый момент) и детерминированную отладку реплеем — ничего не прикручено сбоку, всё выпадает из того, что данные никогда не уничтожаются. Это отлично от change log на CDC или обычного топика Kafka, где истину держит база где-то ещё; в настоящем event sourcing авторитетен лог, а Kafka годится, только если избегать компакции и держать бесконечный retention. CQRS встаёт в пару естественно: проекции потребляют поток в построенные под задачу одноразовые read-модели, которые обязаны быть идемпотентными и лишь eventually consistent, поэтому чтения отстают от стороны записи. Сложные части постоянны: ты никогда не сможешь удалить старую форму события, поэтому изменения схемы решаются upcasting; GDPR-стирание против неизменяемого лога решается crypto-shredding (с реальной юридической оговоркой); а неограниченная цена реплея ограничивается snapshotting, который сам становится граблями, если снапшот хоть раз разойдётся с событиями, которые он претендует обобщать.

Продолжить восхождение ↑Event sourcing: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.