Big Data
Stream Processing Patterns
Twitter в момент финала Суперкубка 2013 обрабатывал 24 миллиона твитов в час. Реальный вопрос был не 'сколько твитов', а 'сколько в секунду прямо сейчас, и растёт или падает'. Для этого нужны скользящие окна по стриму - batch-обработка за ночь даёт правильное число, но на 8 часов позже чем нужно.
- **Uber** считает surge pricing каждые 5 минут через tumbling windows: количество запросов / количество свободных машин в геогексагоне - если ratio > threshold, цена растёт
- **LinkedIn** использует windowed join стримов: просмотры профиля джойнятся с актуальными данными профиля для real-time feed analytics с корректными snapshot-данными
- **Confluent** (создатели Kafka) оценивают, что до 30% производственных инцидентов в stream processing связаны с неправильной обработкой опоздавших данных - watermarks чаще всего настроены слишком агрессивно
Windowing
Бесконечный поток данных нельзя агрегировать без ограничения - нужна 'рамка', внутри которой считается среднее, сумма или max. Window (окно) - это именно такая рамка: подмножество стрима, над которым выполняется агрегация. Парадокс windowing: Uber считает surge pricing каждые 5 минут по всем поездкам в радиусе 2 км - это tumbling window по времени и spatial window по координатам одновременно, 100 000 параллельных окон в реальном времени.
Три типа временных окон: **Tumbling** - фиксированные непересекающиеся отрезки (0:00-1:00, 1:00-2:00). Каждое событие попадает ровно в одно окно. **Sliding** - окна с шагом меньше размера (окно 10 мин, шаг 1 мин): событие попадает в несколько окон, дорого вычислительно. **Session** - окна определяются активностью: закрываются после N секунд тишины. Идеально для user sessions. Четвёртый тип: **Global window** - всё в одном окне с custom trigger (по количеству событий или внешнему сигналу).
Нужно считать количество ошибок за каждые 5 минут. Каждая ошибка должна попасть ровно в один 5-минутный bucket. Какой тип окна?
Stream Joins
JOIN двух бесконечных стримов - это операция, которую невозможно выполнить без ограничений: нельзя ждать 'всех' записей из второго стрима, их бесконечно много. Фреймворки решают это через windowed joins: объединять записи из двух стримов, которые попали в одно временное окно. LinkedIn использует stream join в реальном времени: стрим просмотров профилей (левый) джойнится с обновлёнными данными профиля (правый) чтобы в аналитику попадала актуальная версия профиля на момент просмотра.
Типы stream joins: **Windowed join** - обе стороны в одном временном окне; обе ограничены. **Stream-table join** (enrichment) - стрим событий + медленно меняющаяся таблица (lookup); таблица кешируется в state. **Temporal join** - запись из стрима джойнится с версией таблицы актуальной на момент события (point-in-time join). Kafka Streams поддерживает все три. Стоимость растёт слева направо: windowed дёшево, temporal дорого.
Стрим транзакций нужно обогатить данными клиента (имя, email) из БД. База меняется редко, задержка обогащения некритична. Какой join?
Deduplication
At-least-once delivery - стандарт для большинства стриминговых систем - гарантирует, что сообщение придёт, но не гарантирует что придёт ровно один раз. Для финансовых транзакций или счётчиков рекламных показов дублирование недопустимо. Dedup на стриме сложнее, чем в batch: нужно хранить 'видели ли мы это событие', но нельзя хранить бесконечную историю - только за разумное окно времени.
Стратегии дедупликации в стриме: **Windowed dedup** - хранить set ID за последние N минут, отбрасывать повторы (работает если дубликаты приходят близко по времени). **Bloom filter** - вероятностная структура: 0 false negatives, low false positives; экономит память за счёт редких 'мимо' (принять дубликат). **RocksDB state store** - персистентный K/V store в Flink/Kafka Streams для хранения seen IDs с TTL. **Idempotent write** - не dedup в стриме, а idempotent операция на стороне sink (upsert вместо insert).
Система считает рекламные показы. Нужно исключить дубликаты, но допустима ошибка менее 0.1% (редкий дубликат пройдёт). Какая структура данных оптимальна?
Late Data и Watermarks
Событие в стриме имеет два времени: event time (когда произошло) и processing time (когда брокер получил). Мобильный пользователь зашёл в метро без связи - его клики копились в буфере приложения 20 минут. Когда он вышел, 50 событий пришли одновременно с event time 20-минутной давности. Система с processing time посчитает их в 'неправильное' окно. Watermark - это заявление системы: 'все события с event time до T уже получены, окна до T можно закрывать'.
**Watermark** продвигается по мере прихода событий: `watermark = max_event_time_seen - max_lateness_tolerance`. Чем больше tolerance - тем позже закрываются окна, тем больше опоздавших данных принимается, тем выше latency результатов. **Allowed lateness** (Flink) - дополнительная задержка после watermark перед финальным закрытием окна: принимать ещё N секунд после 'закрытия'. **Side output** - опоздавшие события после allowed lateness идут в отдельный стрим для обработки или алертинга.
Больший watermark tolerance всегда лучше - принимает больше опоздавших данных
Watermark tolerance - это прямой трейдоф между completeness (больше данных) и latency (позже закрываются окна и выдаются результаты)
Tolerance 5 минут означает: результаты окна появятся на 5 минут позже. В real-time системах это часто неприемлемо. Правильный tolerance = максимальная задержка, которую принимает бизнес.
Watermark установлен с tolerance 30 секунд. Событие пришло с event time на 45 секунд раньше текущего watermark. Что с ним произойдёт?
Ключевые идеи
- **Windowing** превращает бесконечный стрим в конечные агрегаты: tumbling для точных периодов, sliding для скользящих метрик, session для пользовательской активности - выбор типа определяет бизнес-требование
- **Stream joins** ограничены временными окнами или кешированием таблицы в state; stream-table join - золотой стандарт для обогащения событий reference данными без временных ограничений
- **Watermarks** - механизм прогресса event time в стриме: tolerance watermark - прямой трейдоф latency vs completeness; опоздавшие данные без side output теряются навсегда
Связанные темы
Stream processing patterns строятся на инфраструктуре брокеров и аналитических хранилищ:
- Real-time Analytics — Результаты windowing и join попадают в OLAP-движки (Druid, ClickHouse) для интерактивных запросов
- Брокеры сообщений — Kafka - типичный источник и sink для stream processing; at-least-once семантика Kafka требует dedup на уровне обработки
Вопросы для размышления
- Session window закрывается после N минут тишины. Но что если пользователь делает паузу 35 минут внутри одного реального визита? Как отличить конец сессии от временного отсутствия активности?
- Deduplication через Bloom filter даёт 0.1% false positives. В рекламных показах 0.1% это миллионы записей в день - как взвесить стоимость памяти HashSet против стоимости пропущенных дубликатов для конкретного бизнеса?
- Watermark tolerance 30 секунд означает результаты на 30 секунд позже. При каких условиях стоит использовать два параллельных пайплайна - быстрый с агрессивным watermark и медленный с большим tolerance?