Real-Time Backend

Event Store

В 2012 году у Knight Capital Group за 45 минут сломался торговый алгоритм и «сжёг» $440 млн. Если бы состояние системы хранилось как последовательность событий, а не как текущий баланс - любой момент можно было бы воспроизвести и найти первую аномалию за секунды.

  • **Axon Framework + EventStoreDB в ABN AMRO:** голландский банк хранит все транзакции как события с 2015 года - регуляторный аудит по требованию DNB занимает часы, а не недели, потому что полная история неизменна и не требует реконструкции
  • **Apache Kafka в Uber:** 1.5 триллиона сообщений в день в 2022 году - каждый GPS-ping, каждый старт/финиш поездки, каждое изменение цены хранится как событие; surge pricing пересчитывается 10+ проекциями параллельно из одного потока
  • **EventStoreDB в Nasdaq:** все биржевые заявки и сделки - append-only, 8 лет retention по требованию SEC; в случае расследования любой день торгов воспроизводится детерминированно без потери ни одного тика
  • **Axon + Kafka в Zalando:** 50M+ заказов в год; команда платежей и команда логистики читают один поток `OrderPlaced` независимо - каждая строит свою проекцию, деплоит независимо, не блокирует друг друга

Event Store

Банк хранит не «баланс = 4 200 руб.», а список событий: `MoneyDeposited(5000)`, `MoneyWithdrawn(800)`. Текущее состояние - это результат воспроизведения всей цепочки с самого начала. Event Store - база данных, заточенная именно под такую модель: каждое изменение фиксируется как неизменяемое событие с монотонно растущим порядковым номером.

EventStoreDB (бывший Event Store) обрабатывает свыше **1 млн событий/сек** на одном узле и используется в Wolseley UK, SKY, Nasdaq. Kafka тоже служит event store, но без встроенной семантики агрегатов - её используют Uber, LinkedIn, Netflix.

Поле `expectedRevision` обеспечивает optimistic locking без SELECT FOR UPDATE: запись падает с `WrongExpectedVersionError`, если другой процесс успел вставить событие раньше. Это единственный механизм конкурентного контроля в append-only системах.

Зачем EventStoreDB хранит `eventNumber` как монотонно растущий bigint вместо UUID?

Append Log

Append-only log - фундаментальная структура данных: новые записи добавляются только в конец, существующие никогда не модифицируются и не удаляются. Именно эту структуру использует Kafka (commit log), PostgreSQL WAL (Write-Ahead Log) и Git (объектная база). Операционная система читает диск последовательно в 10-100 раз быстрее, чем вразброс - append log извлекает максимум из этого факта.

  • **Идемпотентность воспроизведения:** один и тот же лог всегда даёт одно и то же состояние - детерминированная функция `fold(events) -> state`
  • **Аудит из коробки:** история не переписывается, регуляторы (PCI DSS, SOX) получают полный trail без доп. таблиц
  • **Time travel:** можно восстановить состояние на любой момент прошлого, просто остановив воспроизведение на нужном eventNumber
  • **Fan-out:** несколько потребителей читают тот же лог независимо, каждый со своей позицией - Kafka retention 7 дней позволяет добавить нового потребителя постфактум

LinkedIn сохраняет **7 триллионов** сообщений в день через Kafka. При такой нагрузке append-only критичен: random write SSD выдаёт ~100K IOPS, sequential write - до 500K IOPS. Отсюда 5x разница в пропускной способности только за счёт паттерна записи.

Потребитель Kafka упал и пропустил 2 часа событий. Как он восстановит пропущенные данные?

Snapshots

Банковский счёт с историей за 20 лет содержит миллионы событий. Воспроизводить весь лог при каждом запросе баланса - неприемлемо. Снапшот - это периодически сохраняемое «замороженное» состояние агрегата на конкретном eventNumber. При следующем запросе загружается снапшот + только события после него.

Greg Young - автор CQRS/Event Sourcing - рекомендует создавать снапшот каждые **200-1000 событий**. Microsoft Azure's EventSourcing guidance использует порог в 500 событий. При меньших агрегатах снапшоты не нужны вообще: пересчёт 50 событий занимает микросекунды.

Снапшот хранится отдельно от основного лога - в blob storage (S3/GCS) или в отдельной таблице. Это важно: основной лог остаётся immutable, снапшоты - вспомогательный кеш, который можно удалить и пересоздать в любой момент.

Снапшот агрегата сохранён на версии 1000. Потом обнаружили баг в логике `apply()` для событий 500-700. Что нужно сделать?

Projections

Проекция - это materialized view поверх event store: подписчик читает поток событий и строит денормализованную read-модель, оптимизированную для конкретного запроса. Один и тот же поток событий может питать десятки параллельных проекций - таблицу пользователей, лидерборд, аналитику по регионам, ML-feature store.

  • **Eventual consistency:** write-сторона подтверждает событие мгновенно, read-модель обновляется через миллисекунды - допустимо для большинства UI, но не для финансовых расчётов
  • **Rebuild:** сломанную проекцию можно удалить и пересоздать с нуля из лога за минуты - у Netflix занимает около 20 минут для rebuild recommendation-модели на 200M пользователей
  • **Polyglot persistence:** разные проекции пишут в разные хранилища - одна в PostgreSQL для отчётов, другая в Elasticsearch для поиска, третья в Redis для кеша счётчиков
  • **Versioning:** при изменении схемы проекции создаётся `orders_v2_read` параллельно с rebuild, затем происходит атомарный switch - blue/green для read-моделей

Zalando перестроила весь e-commerce backend на event sourcing + projections в 2018-2020. Команды деплоят независимо, каждая владеет своим потоком событий и своими проекциями. При аварии достаточно перезапустить проекцию с последнего checkpoint - данные не теряются.

Event Sourcing - это просто audit log, добавленный поверх обычной CRUD-базы

Event Store - единственный source of truth. Текущее состояние производно от событий, а не наоборот. CRUD-таблица становится одной из проекций, а не основным хранилищем

При «audit log поверх CRUD» у системы два source of truth: основная таблица и лог. Они неизбежно расходятся при ошибках. В настоящем event sourcing база событий неизменна и первична - всё остальное пересоздаётся из неё детерминированно

Read-модель проекции показывает устаревшие данные сразу после записи нового события. Это баг или особенность архитектуры?

Итоги

  • **Event Store = append-only log событий:** состояние никогда не перезаписывается, новые факты только добавляются; текущее состояние вычисляется как `fold(events, initialState)`
  • **Snapshots решают проблему длинных логов:** агрегат загружается из снапшота + delta-событий; снапшоты - пересоздаваемый кеш, лог - неизменяемый source of truth
  • **Projections = CQRS read-side:** один поток событий питает множество специализированных read-моделей в разных хранилищах; при изменении схемы rebuild выполняется без downtime

Связанные темы

Event Store - фундамент для нескольких ключевых архитектурных паттернов:

  • CQRS — Event Store реализует write-сторону CQRS; projections реализуют read-сторону
  • Kafka как distributed log — Kafka - distributed реализация append log с репликацией, партиционированием и retention policy
  • Saga / Distributed Transactions — Event Store хранит шаги саги как события; откат реализуется компенсирующими событиями

Вопросы для размышления

  • Система хранит данные профиля пользователя в CRUD-таблице. При каких условиях стоит мигрировать на event sourcing, а при каких - нет?
  • Проекция обрабатывает события медленнее, чем они поступают (backpressure). Какие три стратегии помогут выровнять нагрузку без потери событий?
  • Команда хочет добавить новую аналитическую проекцию поверх 2-летней истории событий. Как организовать процесс rebuild так, чтобы не затронуть production read-модели?

Связанные уроки

  • db-03-acid
Event Store

0

1

Войти