Потоковая обработка

Stream-Table Duality

В 2016 году LinkedIn обрабатывал 4.5 триллиона сообщений в Kafka в день. Им нужно было показывать live-счётчики ("123 пользователя смотрят эту вакансию прямо сейчас") без отдельной БД. Решение - построить TABLE прямо поверх потока. Так родился Kafka Streams и идея stream-table duality.

  • **LinkedIn (2016)**: 4.5 трлн сообщений Kafka в день, live-счётчики через KTable
  • **Confluent ksqlDB**: SQL-надстройка над Kafka Streams, используется в Lyft, Walmart, Bosch
  • **Pinterest**: Apache Flink для materialized views поверх Kafka, 100+ ТБ state
  • **Uber Athenadb**: TABLE через materialized views для real-time pricing
  • **Yelp Joinery**: stream-stream joins для match orders и payments в real-time

Materialized views: таблица из потока

Stream-table duality - центральная идея Kafka Streams и подобных систем. Любая таблица может быть представлена как поток её изменений, и любой поток ключ-значение может быть свёрнут в таблицу через агрегацию по последнему значению на ключ. Materialized view - это и есть таблица, материализованная из потока.

**Формально:** таблица T = aggregate(stream S, by_key, last_value). Поток S = changelog(table T). Если поток упорядочен по ключу, эти операции инвертны: table -> stream -> table даёт исходную таблицу.

Что такое materialized view в Kafka Streams?

Changelog streams

Changelog stream - это поток обновлений таблицы: каждое сообщение содержит ключ и новое значение (или null для удаления). Compaction Kafka хранит только последнее сообщение на ключ, поэтому changelog топик становится "физически" таблицей: размер ограничен количеством уникальных ключей, а не временем.

**Свойства:** идемпотентность (повтор сообщения = повтор последнего значения = no-op), сериализуемость (можно восстановить таблицу с нуля проиграв changelog), tombstone (key, null) обозначает удаление и удаляется compaction после retention.

Почему changelog топик использует cleanup.policy=compact, а не delete?

ksqlDB: SQL над потоками

ksqlDB - надстройка над Kafka Streams, дающая SQL-синтаксис. Позволяет создавать STREAMs и TABLEs, делать join-ы, агрегации, окна без написания Java кода. Внутри генерирует Kafka Streams топологию.

**Pull vs Push query:** pull читает текущий snapshot из state store (как обычный SELECT). Push возвращает поток изменений в реальном времени. Оба используют одну и ту же KTable внутри.

В ksqlDB STREAM и TABLE - это одно и то же физически?

Streaming joins: stream-stream и stream-table

Stream-table join обогащает каждое событие потока данными из таблицы по ключу. Это lookup: для события приходит текущее значение из TABLE. Идеально для обогащения транзакций профилем пользователя.

Stream-stream join сложнее: нужно сопоставить два события из разных потоков, но они не приходят одновременно. Решение - временное окно: "если событие B пришло в течение N минут после события A с тем же ключом, выдай пару". Без окна join был бы бесконечно растущей таблицей.

**Подводный камень:** join требует co-partitioning. Оба потока должны быть партиционированы по одному ключу и иметь одинаковое число партиций. Иначе ksqlDB сделает auto-repartition (дорогой shuffle через промежуточный топик).

TABLE в ksqlDB - это таблица как в обычной БД, отдельная сущность от Kafka.

TABLE - это представление поверх changelog топика. Физически данные хранятся в Kafka (для durability) и в RocksDB (для query latency). При падении worker-а state восстанавливается из changelog топика.

Нет отдельной БД, нет двойного хранения - это и есть stream-table duality в действии. Если думать о TABLE как о PostgreSQL-таблице, инженер начнёт искать ETL job-ы между Kafka и БД, которых не существует. Понимание единого хранилища меняет дизайн системы целиком.

Зачем stream-stream join требует временное окно?

Ключевые идеи

  • Stream-table duality: таблица = aggregate(stream, by key, last value). Stream = changelog(table). Преобразования взаимно обратны
  • Materialized view = таблица из потока + локальный state store (RocksDB) + changelog топик для recovery
  • ksqlDB даёт SQL-синтаксис над Kafka Streams. STREAM и TABLE - разные семантики над одним механизмом топик плюс state store
  • Stream-table join = lookup по ключу. Stream-stream join требует временного окна, иначе state растёт бесконечно. Оба требуют co-partitioning

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

Stream-table duality - центральная ментальная модель для всех streaming-систем; она связывает CDC, event sourcing, CQRS и Kafka Streams в одну архитектурную картину:

  • Change Data Capture (CDC) — обратная сторона duality: table -> changelog
  • Kafka Streams — первичная реализация stream-table duality
  • Event Sourcing — stream как source of truth, table как projection
  • CQRS — stream для commands, materialized view для queries

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

  • Как бы построили counter "online users" через KTable? Какой changelog топик нужен?
  • У вас два потока: clicks и impressions. Как соединить их в pairs для расчёта CTR?
  • Почему stream-stream join без окна был бы неправильным дизайном?
  • В каких случаях ksqlDB не подходит и нужно писать Kafka Streams на Java напрямую?

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

  • stream-13 — CDC - ключевой паттерн для понимания stream-table duality
  • db-02-relational-model — Таблица как частный случай стрима с единственным состоянием
  • prob-17 — Марковские цепи: поток - это марковские переходы, таблица - стационарное состояние
  • stream-15 — Понимание duality открывает путь к оконным функциям
  • aie-08-streaming — LLM streaming - приложение stream-table duality для AI систем
Stream-Table Duality

0

1

Войти