Потоковая обработка
Event-Driven Architecture
Jay Kreps, LinkedIn Kafka и рождение современной EDA
2010 год: Greg Young формализует CQRS и Event Sourcing в серии статей и докладов на конференциях. Паттерн существовал в финансах, но впервые получил строгое определение для разработки ПО. 2011 год: LinkedIn открывает Kafka - Jay Kreps, Neha Narkhede, Jun Rao. Публикация описывает log как универсальную абстракцию для всех данных в компании. 2013 год: Jay Kreps пишет 'The Log: What every software engineer should know about real-time data's unifying abstraction' - эссе, изменившее то, как индустрия думает о потоках событий. Kafka стала инфраструктурой для event sourcing в масштабе: LinkedIn, Uber, Netflix, Airbnb.
Банковский счёт показывает баланс 1500 рублей, но клиент утверждает - вчера было 1800. В традиционной БД UPDATE перезаписал старое значение без следа. Что если бы хранилась не сумма, а каждая транзакция? Тогда можно 'перемотать' до любой точки - и баг воспроизвести, и аудит пройти, и compliance доказать.
- **LinkedIn (2011)** - Kafka как event bus для всей компании: 700 млрд событий в день сегодня
- **Axon Framework** - CQRS + Event Sourcing фреймворк для Java: используется в ABN AMRO, ING, Rabobank
- **EventStoreDB** - специализированная СУБД для event sourcing: финтех, страховые компании, healthcare
- **Confluent Platform** - коммерческий Kafka: CQRS projections через Kafka Streams и ksqlDB
- **Git** - event sourcing для кода: каждый commit - событие, текущий файл = replay всех коммитов
- **Stripe** - event-driven API: каждое действие генерирует webhook-событие для интеграций
Предварительные знания
Events - факты произошедшего
LinkedIn в 2011 году не мог справиться с нагрузкой. Сервисы вызывали друг друга по цепочке - ProfileService -> RecommendationService -> NotificationService. Один сервис тормозит - вся цепочка встаёт. Джей Крепс, Неха Нарход и Джун Рао придумали Kafka: сервисы публикуют **события** в лог, другие сервисы читают из лога независимо. Никакой прямой связанности. Так родился modern event-driven подход.
**Event** - неизменяемый (immutable) факт того, что произошло. OrderCreated, PaymentProcessed, ItemShipped. Event описывается в прошедшем времени - он уже случился, его нельзя 'отклонить'. Компоненты подписываются на события и реагируют автономно, не зная друг о друге.
**Слабая связанность** - главное преимущество. Publisher не знает о subscribers. Новый сервис аналитики подключается к Kafka topic без единого изменения в OrderService. Падение NotificationService не влияет на InventoryService. Масштабирование каждого consumer независимо. Kafka в Netflix обрабатывает 700 млрд событий в день - именно благодаря этой развязке.
| Свойство Events | Описание |
|---|---|
| Immutable | Факт нельзя изменить - OrderCreated уже произошёл |
| Past tense | Именование в прошедшем времени: Created, Updated, Deleted |
| Self-contained | Содержит всю необходимую информацию для обработки |
| Ordered | События одного агрегата строго упорядочены по времени |
| Durable | Хранятся в брокере для replay и fault tolerance |
Почему events описываются в прошедшем времени (OrderCreated, а не CreateOrder)?
Commands - запросы на действие
**Command** - запрос на выполнение действия. CreateOrder, CancelSubscription, UpdateProfile. В отличие от event, command **может быть отклонён**: недостаточно средств, товара нет на складе, нет прав. Command адресован конкретному handler, а не broadcast всем. Это принципиальная семантика: command - намерение, event - результат.
**Event vs Command:** Event - то, что произошло (факт, immutable, broadcast). Command - то, что хотим сделать (запрос, может быть отклонён, адресован конкретному handler). Поток: Command -> обработка и валидация -> Event (если успешно).
| Свойство | Command | Event |
|---|---|---|
| Именование | Императив: CreateOrder | Прошедшее время: OrderCreated |
| Адресат | Один конкретный handler | Все подписчики (broadcast) |
| Может быть отклонён? | Да - валидация и бизнес-правила | Нет - это уже произошедший факт |
| Содержит | Намерение + параметры | Факт + полные данные события |
| Idempotent? | Должен быть | По природе (факт однократен) |
В хорошо спроектированной системе command handlers - тонкий слой валидации и координации. Вся реактивная логика - уведомления, аналитика, синхронизация - живёт в event handlers. Новый подписчик не требует изменения command handler. Именно это даёт EDA эластичность: Uber добавляет новый сервис аналитики поездок - и нулевых изменений в TripService.
Пользователь отправляет CancelOrder. Заказ уже отправлен и не может быть отменён. Что произойдёт?
Event Sourcing
Традиционная БД хранит **текущее состояние**: баланс = 1500. **Event Sourcing** хранит **все события**: Deposited(1000), Deposited(800), Withdrawn(300). Текущее состояние = replay всех событий. Audit log бесплатно - это и есть хранилище. Greg Young представил этот паттерн в 2010 году, вдохновившись... бухгалтерскими книгами. Двойная запись в бухгалтерии - тот же принцип с XIV века.
**Преимущества Event Sourcing:** 1. Полная история - восстановить состояние на любой момент. 2. Audit trail бесплатно. 3. Легко добавлять новые projections без изменения данных. 4. Debugging: воспроизвести баг, replay-ив события. 5. Temporal queries: 'какой был баланс вчера в 14:23?'
Replay миллиардов событий может быть медленным. Решение - **snapshots**: периодически сохранять текущее состояние. При восстановлении: загрузить последний snapshot + replay событий после него. EventStoreDB делает это автоматически каждые N событий.
| Свойство | Traditional DB | Event Sourcing |
|---|---|---|
| Хранит | Текущее состояние | Все события с начала времён |
| История | Потеряна (UPDATE перезаписывает) | Полная, immutable |
| Восстановление на дату | Невозможно без WAL/backup | replay до нужного момента |
| Audit trail | Отдельная реализация | Встроен - events и есть audit |
| Сложность | Простой CRUD | Выше: event store, projections, versioning |
| Объём данных | Только актуальное | Растёт постоянно (retention policy) |
Какую проблему НЕ решает event sourcing?
CQRS
**CQRS** (Command Query Responsibility Segregation) - Грег Янг и Удо Удо Фарке, 2010. Разделение моделей чтения и записи. Команды (записи) идут в event store. Запросы (чтения) идут в оптимизированные read-модели (projections). Одни и те же события строят разные projections - каждая в формате, удобном для конкретных запросов. Netflix строит сотни projections из одного потока Kafka событий.
**Eventual consistency в CQRS:** Read-модели обновляются асинхронно после публикации события. Между записью события и отражением в projection есть задержка - обычно миллисекунды, иногда секунды при высокой нагрузке. Для большинства use cases это приемлемо. Для критичных операций (баланс счёта, инвентарь) - нужен read-your-writes паттерн или версионирование.
| Свойство | Без CQRS | С CQRS |
|---|---|---|
| Модель данных | Одна для всего | Отдельные для write и read |
| Масштабирование | Одинаковое для read/write | Независимое: 10 read replicas, 1 write |
| Оптимизация запросов | Компромиссная для всех | Read-модель оптимальна под конкретный запрос |
| Consistency | Strong | Eventual (задержка в миллисекундах) |
| Сложность | Низкая | Высокая: projections, синхронизация, versioning |
CQRS и Event Sourcing часто используются вместе, но это **разные паттерны**. CQRS применяется без ES: разделить read/write модели в обычной PostgreSQL. ES используется без CQRS: одна unified модель. Вместе они дают максимальную гибкость - и максимальную сложность. Axon Framework, EventStoreDB, Confluent Platform строят экосистемы именно вокруг этой комбинации.
Event sourcing подходит для любой системы
Event sourcing добавляет значительную сложность: event versioning (формат события изменился - старые данные?), eventual consistency, GDPR compliance, растущий объём данных. Для простого CRUD - overkill.
Event sourcing оправдан когда: история критична (финансы, медицина, аудит), нужны temporal queries, много разных view на одни данные, нужен replay для debugging или миграции. Для блога или TODO-листа - обычная PostgreSQL надёжнее и проще.
Пользователь создал заказ и сразу открывает список заказов. Заказа в списке нет. Почему?
Ключевые идеи
- **Events** - immutable факты (OrderCreated): broadcast всем подписчикам, не могут быть отклонены
- **Commands** - запросы на действие (CreateOrder): адресованы конкретному handler, могут быть отклонены
- **Event Sourcing** - хранение всех событий вместо текущего состояния: полная история, audit trail, temporal queries, snapshots для производительности
- **CQRS** - раздельные модели для write (event store) и read (projections): независимое масштабирование, eventual consistency
- Комбинация ES + CQRS - проверенный паттерн для финансов и систем с аудитом; избыточна для простого CRUD
- Главное противоречие ES: immutable events vs GDPR right to erasure - решается crypto-shredding
Связанные темы
EDA - архитектурный паттерн, реализуемый через конкретные технологии:
- Message Brokers: Kafka vs RabbitMQ vs NATS — Брокеры - транспортный уровень для events и commands
- Batch vs Stream Processing — Events - фундамент stream processing; batch обрабатывает события пакетами
- Event-Driven паттерны — Saga, Outbox, DLQ - production паттерны поверх EDA
Вопросы для размышления
- Git хранит историю как цепочку коммитов. Какие паттерны из этого урока есть в Git?
- Как решить проблему GDPR (right to erasure) в системе с event sourcing?
- В каких сценариях eventual consistency CQRS неприемлема? Как это компенсировать?
Связанные уроки
- stream-01 — Batch vs stream - фундамент для понимания EDA
- stream-03 — Kafka, RabbitMQ, NATS - транспорт для events и commands
- bt-17-event-driven — Паттерны EDA: Saga, Outbox, DLQ на практике
- bt-18-saga — Saga pattern для распределённых транзакций через events
- ds-02-cap-theorem — Eventual consistency в CQRS - следствие CAP-теоремы
- isd-10-message-queues
- dist-07-transactions