Event-Driven ядро
Что такое Event-Driven ядро
Event-Driven ядро (EDC) — это «позвоночник» архитектуры, в котором бизнес-факты фиксируются и распространяются как неизменяемые события, а остальная функциональность (чтение, интеграции, аналитика, кэши, нотификации) строится поверх потока этих событий. Ядро задает контракт событий, правила доставки и инварианты порядка/идемпотентности, обеспечивая слабую связность и масштабируемость.
Ключевая идея: сначала записать факт (ядро), а затем независимо обогащать и проецировать его в нужные модели. Это уменьшает связность и повышает устойчивость к частичным сбоям.
Цели и свойства EDC
Истинность фактов: каждое событие — неизменяемая запись «что произошло».
Слабая связность: продюсеры не знают потребителей; расширение системы — добавлением подписчиков.
Масштабирование: горизонтальный рост по партициям/топикам, независимые потребители.
Наблюдаемость и аудит: сквозные идентификаторы, воспроизводимость, ретенции и репроигрывание.
Управляемая эволюция: версии схем, совместимость, deprecation.
Архитектурные компоненты
1. Шина/брокер событий: Kafka/NATS/Pulsar/SNS+SQS — каналы, партиции, ретенции.
2. Реестр схем: JSON Schema/Avro/Protobuf для совместимости и эволюции.
3. Outbox/CDC-контур: атомарная фиксация факта + публикация без «двойной записи».
4. Проекции/чтения (CQRS): материализованные представления для быстрых запросов.
5. Саги/оркестрация: координация долгоживущих процессов через события/команды.
6. Обогащение: отдельные топики `.enriched`/`.derived` без влияния на критический путь.
7. Обсервабилити: трассировка, логирование, метрики по событиям и лагам.
Модель событий
Типы событий
Domain Events: бизнес-факты (`payment.authorized`, `kyc.approved`).
Integration Events: ориентированы на внешние системы (стабильные, медленно меняются).
Change Data Capture (CDC): технические изменения записи (используйте для миграций/интеграций).
Audit/Telemetry: действия акторов, безопасность, SLA.
Обязательные атрибуты (ядро)
json
{
"event_id": "uuid",
"event_type": "payment. authorized. v1",
"occurred_at": "2025-10-31T11:34:52Z",
"producer": "payments-service",
"subject": { "type": "payment", "id": "pay_123" },
"payload": { "amount": 1000, "currency": "EUR", "method": "card" },
"schema_version": 1,
"trace_id": "abc123",
"partition_key": "pay_123"
}
Рекомендации: `event_id` глобально уникален, `partition_key` задает порядок для сущности, `trace_id` обеспечивает корреляцию.
Семантика доставки и идемпотентность
At-least-once (по умолчанию у многих брокеров): потребители обязаны быть идемпотентными.
At-most-once: приемлемо лишь для второстепенных телеметрий.
Exactly-once: достигается на уровне потока и хранилища через транзакции/идемпотентные ключи/лейки (дороже, нужна веская причина).
Шаблон идемпотентности потребителя
Дедуп-таблица по `event_id`/`(event_id, consumer_id)` с TTL ≥ ретенции топика.
Upsert вместо insert; версионирование проекций по `sequence`/`occurred_at`.
Операции в рамках транзакции: отметка «видел» + изменение состояния.
Порядок и партиционирование
Гарантированный порядок в пределах партиции.
Выбирайте `partition_key` так, чтобы все события одной агрегат-сущности попадали в одну партицию (`user_id`, `payment_id`).
Избегайте «горячих ключей»: хеш с солью/под-ключи, если требуется распределить нагрузку.
Схемы и эволюция
Additive-first: новые опциональные поля, запрет на смену типов/семантики без major-версии.
Совместимость: BACKWARD/FORWARD в реестре схем; CI блокирует несовместимые изменения.
Именование: `domain.action.v{major}` (`payment.authorized.v1`).
Миграции: публикуйте пары `v1` и `v2` параллельно, обеспечьте двойное излучение (dual-write через outbox), снимайте `v1` после перехода.
Outbox и CDC
Outbox (рекомендуется для транзакционных сервисов)
1. В одной БД-транзакции: сохраняем доменную запись и событие в outbox.
2. Фоновый паблишер читает outbox, публикует в брокер, помечает «отправлено».
3. Гарантии: нет «потери» факта при падениях, нет рассинхронизации.
CDC (Change Data Capture)
Подходит для существующих систем/миграций; источник — лог репликации БД.
Требует фильтрации/перекодирования в доменные события (не транслируйте «сырые» таблицы вовне).
CQRS и проекции
Команды изменяют состояние (часто синхронно), события — формируют проекции (асинхронно).
Проекции рассчитаны на запросы (поиск, списки, отчеты), обновляются подписчиками.
Временная рассинхронизация — норма: показывайте устойчивый UX («данные обновятся через несколько секунд»).
Саги: координация процессов
Оркестрация: один координатор посылает команды и ждет событий.
Хореография: участники реагируют на события друг друга (проще, но требует дисциплины в контрактах).
Правила: четкие компенсации и тайм-ауты, повторяемые шаги, идемпотентные обработчики.
Наблюдаемость
Trace/Span: прокидывайте `trace_id`/`span_id` через заголовки при порождении событий.
Метрики: лаг потребителей, скорость публикации/потребления, dead-letter rate, доля дедупликаций.
DLQ/parking lot: неуспешные сообщения — в отдельный топик с алертом; обеспечьте переобработку.
Безопасность и соответствие
Классификация данных: ядро содержит только необходимый минимум PII/финданных (модель обратной пирамиды), детали — в обогащениях.
Подпись/хэш критичных атрибутов, контроль целостности.
Шифрование in-flight и at-rest, секционирование прав по темам/консюмерам (IAM/ACL).
Политики ретенции и права на забвение: четко определены для каждого топика.
Производительность и устойчивость
Backpressure: у потребителей — ограничение конкурентности, у брокера — квоты/лимиты.
Batch-обработка и компрессия: группируйте записи для снижения накладных расходов.
Ретраи с джиттером и DLQ вместо бесконечных попыток.
Rebalance-стойкость: храните оффсеты транзакционно/внешне, ускоряйте холодный старт снапшотами.
Типовые шаблоны событий
Ядро платежей
`payment.initiated.v1` → `payment.authorized.v1` → `payment.captured.v1` → `payment.settled.v1`
Отказы: `payment.declined.v1`, `payment.refunded.v1`
Партиционирование: `payment_id`
SLA: лаг ядра ≤ 2с p95; идемпотентность потребителей обязательна.
KYC/верификации
`kyc.started.v1` → `kyc.document.received.v1` → `kyc.approved.v1`/`kyc.rejected.v1`
PII — минимально; детали документа — в `kyc.enriched.v1` с ограниченным доступом.
Аудит/безопасность
`audit.recorded.v1` с атрибутами `actor`, `subject`, `action`, `occurred_at`, `trace_id`.
Непрерывная ретенция/архивирование; повышенная целостность (WORM-хранилища).
Антипаттерны
Fat Event: перегруженные payload’ы без нужды, утечки PII.
Hidden RPC через события: ожидание синхронных ответов «здесь и сейчас».
Сырые CDC наружу: тесная связность со схемой БД.
Нет идемпотентности у потребителей: дубли приводят к двойным побочным эффектам.
Один общий топик «на все»: конфликт интересов, проблемный порядок, сложная эволюция.
Пошаговое внедрение EDC
1. Картирование домена: выделите ключевые агрегаты и жизненные циклы.
2. Каталог событий: названия, смыслы, инварианты, обязательные поля.
3. Схемы и реестр: выберите формат, включите правила совместимости.
4. Outbox/CDC: для каждого продюсера определите механизм публикации фактов.
5. Партиционирование: выберите ключи и оцените горячие ключи/переразбиение.
6. Идемпотентность: шаблон дедупа + транзакционность потребителей.
7. Проекции: определите материализованные модели и SLA обновления.
8. Обсервабилити: трассировка, лаги, DLQ, алерты.
9. Security/PII: классификация данных, шифрование, ACL.
10. Гайд по эволюции: политика версий, депрекейт, dual-write для миграций.
Чек-лист продакшена
- У каждого события есть `event_id`, `trace_id`, `occurred_at`, `partition_key`.
- Схемы в реестре, включены проверки совместимости.
- Идемпотентность потребителей реализована и протестирована.
- Настроены DLQ/parking lot и алерты на ошибки публикации/потребления.
- Проекции пересоздаются из лога (replay) с приемлемым временем.
- Ограничен доступ к PII; минимальные payload’ы в ядре.
- SLA по лагам/доставке замеряются и видны на дашбордах.
- Есть план миграции версий событий и окон депрекейта.
FAQ
Чем EDC отличается от «просто шины»?
Ядро — это не только брокер, но и контракт событий, правила порядка/идемпотентности, процессы эволюции и наблюдаемость.
Можно ли строить только на CDC?
CDC подходит для интеграций/миграций, но доменные события яснее выражают смысл и стабильнее переживают изменения БД.
Как быть с согласованностью?
Принимаем eventual consistency и проектируем UX/процессы под нее (индикаторы обновления, ретраи, компенсации).
Когда нужен exactly-once?
Редко: когда удвоение строго недопустимо и невозможно компенсировать. Чаще достаточно at-least-once + идемпотентность.
Итог
Event-Driven ядро превращает поток бизнес-фактов в надежный фундамент системы. Четкие контракты событий, дисциплина доставки и наблюдаемость дают масштабируемость, устойчивость и скорость эволюции — без хрупких синхронных связей и «шторма» регрессий при изменениях.