Потоковая обработка
Change Data Capture (CDC)
В 2015 году LinkedIn опубликовал статью 'The Log: What every software engineer should know about real-time data's unifying abstraction' - Jay Kreps описал, как Kafka превратил database WAL в universal interchange format. К 2020-му Debezium стал индустриальным стандартом, а CDC превратился из nice-to-have в архитектурное требование: каждый микросервис имеет свою БД, но business cares о consistent state across them. CDC - мост между silo'ами данных, не требующий переделки приложений.
- **Netflix Data Mesh**: CDC через Debezium синхронизирует сотни service databases с центральным data lake - аналитика real-time, без ETL ночью.
- **Stripe CDC**: каждый payment event протекает через Kafka в десятки downstream системы (fraud detection, accounting, customer notifications) - всё стартует с PostgreSQL WAL.
- **Wayfair search index**: каталог из 30M+ items в PostgreSQL обновляется в Elasticsearch через Debezium - latency 100ms от commit до searchable.
Debezium и log-based CDC
Классический ETL: ночью запускается batch-job, читает SELECT * FROM orders WHERE updated_at > yesterday, заливает в DWH. Проблема: задержка 24 часа, и пропадают изменения, которые были потом удалены. Debezium читает WAL/binlog базы данных напрямую - точно ту же запись, которую делает сама БД для replication. Каждая INSERT/UPDATE/DELETE становится событием в Kafka через секунды. PostgreSQL logical replication, MySQL binlog, MongoDB oplog - Debezium поддерживает 10+ источников. К 2024 году Debezium стал индустриальным стандартом: Netflix, Wayfair, JPMorgan используют его для синхронизации сотен БД с озёрами данных и search-индексами. Ключ - точная семантика: каждое изменение фиксируется один раз с causality (LSN/GTID), даже при failover.
Postgres logical replication: создаётся replication slot, который удерживает WAL до прочтения. Если consumer отстаёт - WAL копится, диск переполняется. MySQL binlog: row-based vs statement-based формат, для CDC всегда row-based. MongoDB change streams: API поверх oplog. CDC events: before-state, after-state, operation type (c/u/d), source metadata (LSN, transaction ID, timestamp). Schema evolution через Avro+Schema Registry.
Replication slot - источник риска. Если Debezium падает и не возвращается, WAL продолжает копиться, диск БД переполняется, БД останавливается. Мониторинг pg_replication_slots.confirmed_flush_lsn vs pg_current_wal_lsn критичен - alert при lag > 1 GB.
Debezium читает PostgreSQL WAL. Что произойдёт, если Kafka-кластер недоступен 24 часа?
Outbox Pattern
Сервис заказов сохраняет Order в PostgreSQL и хочет отправить OrderCreated в Kafka. Два варианта плохие: (1) сохранить в БД, потом отправить в Kafka - если Kafka недоступна, событие потеряется. (2) отправить в Kafka, потом в БД - если БД упадёт, в Kafka есть событие о несуществующем заказе. Distributed transaction между БД и Kafka? Технически возможно через XA, но catastrophically медленно и сложно. Outbox pattern решает это: вместо отправки в Kafka, сервис пишет событие в таблицу outbox в той же транзакции с Order. Дальше Debezium читает outbox через CDC и публикует в Kafka. Atomicity гарантирована БД, асинхронная доставка - Debezium.
Outbox table структура: event_id (UUID), aggregate_id (для key Kafka), aggregate_type (для topic), event_type, payload (JSONB), created_at. После публикации события можно удалить из outbox (cleanup job) или хранить для аудита. Inbox pattern - зеркальный для consumer'а: события записываются в inbox-таблицу до обработки, обеспечивая идемпотентность. Микросервисы часто используют пару outbox + inbox для exactly-once семантики между сервисами.
Outbox - не только для Kafka. Тот же паттерн работает для отправки email (через background worker), webhook, обновления search-индекса. Главный принцип: side-effect через таблицу + асинхронный publisher, никогда напрямую в transaction.
Почему outbox pattern предпочтительнее двухфазного commit'а (XA) между БД и Kafka?
Log-based vs query-based CDC
До Debezium стандартом был query-based CDC: scheduled SELECT * FROM table WHERE updated_at > last_sync. Простое, не требует доступа к WAL. Но три фундаментальные слабости: (1) пропускает DELETE - удалённой записи нет в SELECT, нужны soft-delete или audit log. (2) пропускает промежуточные изменения - если row обновился 5 раз за минуту, query увидит только последнее. (3) нагрузка на источник - каждые 30 секунд full table scan по updated_at index. Log-based решает всё: видит каждое изменение в order, не нагружает source (читает уже существующий WAL). Цена - operational complexity: replication slots, schema evolution, source compatibility (не каждая БД даёт WAL access).
Snapshot strategies: initial - читаем всю таблицу при первом запуске, потом log. never - только log с момента старта (потеря исторических данных). when_needed - в случае несовместимости. always - re-snapshot перед каждым запуском (для маленьких таблиц). Incremental snapshot (Debezium 1.6+) позволяет ре-снапшотить таблицу без остановки стрима - watermarks отслеживают, какие row уже обновлены.
В каком сценарии query-based CDC оправдан, а log-based - излишен?
Trigger-based CDC и trade-offs
Третий подход - triggers на source-таблице, которые при каждом INSERT/UPDATE/DELETE записывают в audit/changelog таблицу. Простой polled CDC, потом читает changelog. Это компромисс: видит все изменения (как log-based), не требует WAL access (как query-based). Трагедия - производительность: trigger выполняется в той же транзакции, что и INSERT. Каждая запись теперь дважды дороже: основная таблица + changelog. Lock contention в transaction, lock на changelog. Для transactional систем с 10K writes/sec triggers убивают throughput на 30-50%. Trigger-based живёт там, где нет WAL access (старые Oracle, SQL Server до 2008): legacy миграции, отчётность, аудит.
Сравнение подходов: - Query-based: latency 30s+, no DELETE, simple, low source overhead на больших таблицах огромная. - Trigger-based: latency секунды, видит все, complex schema, 30-50% write penalty. - Log-based: latency мс, видит все, minimal overhead, требует WAL access и operations.
Trigger-based CDC увеличивает write amplification: 1 INSERT в orders = 1 INSERT в orders_changelog + lock на обеих таблицах + transaction log запись для обеих. На бенчмарках pgbench с TPC-C-like workload - throughput падает на 30-50%. Для OLTP это часто неприемлемо.
CDC = бесплатный способ получить event-streaming из любой реляционной БД
CDC даёт streams of changes, но не магически превращает CRUD в event-driven architecture. События CDC - low-level (row changed), а domain events - high-level (OrderShipped). Outbox pattern или CDC + transformation layer нужны для извлечения domain events из table changes
Row-level CDC видит UPDATE orders SET status='shipped' - но domain event 'OrderShipped' содержит контекст (когда, кем, какой carrier), который не в одной строке. Простое publication CDC в Kafka даёт data integration, но не event-driven domain modeling.
Команда выбирает CDC для legacy SQL Server 2005 без logical replication. Какой подход рационален?
Ключевые идеи
- **Log-based CDC** (Debezium) читает WAL/binlog напрямую - low latency, видит все изменения, минимальная нагрузка на источник, но требует управления replication slots.
- **Outbox pattern** решает проблему dual-write: транзакционная запись в БД + Debezium читает outbox table, гарантируя atomicity без XA.
- **Query-based vs trigger-based vs log-based** - спектр trade-offs: simplicity vs completeness vs performance. Выбор зависит от latency requirements, source compatibility и operational maturity.
- **CDC events != domain events**: row-level changes требуют transformation layer (или outbox с domain events) для построения event-driven architecture.
Связанные темы
CDC - инфраструктурный слой, соединяющий БД и стримы:
- Saga Pattern — CDC + outbox - распространённая реализация saga: каждый шаг saga сохраняет event в outbox в своей транзакции, Kafka доставляет в следующий сервис
- Event Sourcing — CDC превращает обычную CRUD-БД в источник событий, открывая путь к event-sourced views без переписывания приложений
Вопросы для размышления
- Replication slot - источник риска для production: если consumer падает, БД переполняется. Какие операционные практики смягчают этот риск?
- CDC выдаёт low-level events ('row changed'), но domain нужны high-level events ('OrderShipped'). Где должна находиться transformation - в Kafka Streams, в outbox, в самом сервисе?
- Schema evolution в CDC: source-таблица меняет тип колонки, downstream consumers падают. Как организовать compatibility и rollout без перезапуска тысяч consumer'ов?
Связанные уроки
- stream-12 — Kafka internals перед CDC
- db-13-transactions — WAL и транзакционные логи - основа CDC
- db-14-mvcc — MVCC определяет что CDC видит в потоке изменений
- stream-14 — CDC открывает Stream-Table Duality концепцию
- ds-05-replication — CDC и репликация данных: CDC - это стриминговая форма репликации
- db-03-acid