Big Data
Apache Flink: потоковая обработка
Alibaba, Singles' Day 2015
Ноябрь 2015 года, Singles' Day. Alibaba обрабатывает логи 1 миллиарда событий в секунду - клики, корзины, платежи. Spark Streaming с micro-batching: задержка 500 мс. Этого достаточно для логирования, но не для real-time fraud detection - мошеннические транзакции нужно блокировать немедленно. Alibaba перешёл на Flink: задержка упала до 10 мс. Секрет - true streaming, где каждое событие обрабатывается сразу, а не батчами. Сегодня Alibaba - крупнейший контрибьютор Apache Flink, обрабатывающий через него exabytes данных ежегодно. Именно давление Singles' Day превратило академический проект берлинского Технического университета (TU Berlin, 2011) в production-ready систему.
Микро-батч - это ложь. Spark Streaming не потоковый: он пакетный с маленькими пакетами. Каждые 100 мс собирается батч, создаётся RDD, запускается Spark job. Задержка определяется размером батча, не скоростью обработки. Flink первым сказал правду: настоящий streaming - это когда оператор получает событие и сразу его обрабатывает. Без ожидания конца батча.
- **Alibaba**: Flink обрабатывает real-time логи 1 млрд событий/сек во время Singles' Day - fraud detection с задержкой 10 мс
- **Netflix**: Flink-пайплайн для real-time personalization - обновление рекомендаций в момент просмотра, не через 5 минут после
- **Lyft**: статeful sessionization поездок на Flink - сложные паттерны поведения водителей без внешней базы данных
Event Time vs Processing Time
2015 год, Singles' Day на Alibaba. 1 миллиард событий в секунду. Spark Streaming с micro-batching даёт задержку 500 мс - каждые полсекунды формируется мини-батч, обрабатывается, результат записывается. Flink с true streaming выдаёт 10 мс. Разница не в железе, не в скорости сети. Разница в архитектуре: Spark ждёт, Flink обрабатывает каждое событие в момент прихода.
Но у любой потоковой системы есть фундаментальная проблема: **время события** и **время обработки** - разные вещи. Пользователь нажал кнопку в мобильном приложении в 14:00:00.000. Событие пришло на сервер в 14:00:00.350 (350 мс сетевая задержка). Flink обработал его в 14:00:00.360. Три разных метки времени для одного факта.
**Processing time** - системное время оператора Flink в момент обработки. Быстро, нет ожидания, но результаты недетерминированы: запустить один и тот же поток дважды - разные окна, разные результаты. **Event time** - метка из самого события (поле в JSON, Kafka timestamp). Результаты детерминированы, но требуют механизм работы с опозданиями.
Выбор между event time и processing time - это trade-off между корректностью и задержкой. Real-time дашборды с задержкой до 1 мс выбирают processing time - ждать опаздывающие события некогда. Биллинг, аналитика транзакций, расчёт метрик SLA - event time обязателен: события из разных часовых поясов должны попасть в правильные временные окна.
Команда строит систему биллинга: считать сумму транзакций пользователя за каждый час для выставления счёта. Какой режим времени необходим?
Watermarks: управление временем в потоке
Event time создаёт проблему: Flink получает события не в хронологическом порядке. Событие с меткой 14:00:00 может прийти после события 14:00:05. Сколько ждать перед тем как закрыть временное окно? Ждать вечно - результата нет никогда. Не ждать - потерять поздние события. Watermark - это ответ на вопрос: "после получения этого события, все предыдущие с меткой времени <= T уже пришли".
Watermark W(t) - специальный маркер в потоке данных, который движется вместе с событиями. Когда оператор видит watermark W(t), он знает: можно закрывать окна с end_time <= t. Опоздавшие события (late events, event time < текущего watermark) обрабатываются по отдельной стратегии.
**Стратегия опозданий**: `allowedLateness(Duration)` - окно живёт дольше, обновляется при каждом позднем событии. `sideOutputLateData(OutputTag)` - поздние события уходят в боковой поток для отдельной обработки. Комбинация watermark + allowedLateness позволяет балансировать задержку (latency) и полноту результатов (completeness).
Параллельные потоки добавляют сложность: Flink берёт минимальный watermark из всех входящих потоков. Одна медленная партиция или idle источник блокирует продвижение watermark для всего оператора. Именно поэтому `withIdleness()` критичен в production - иначе один пустой раздел Kafka останавливает весь пайплайн.
Flink job читает из Kafka топика с 8 партициями. Одна из партиций (partition 3) перестала получать события на 5 минут. Что произойдёт с watermark при отсутствии `withIdleness()`?
Stateful Operators: состояние как первый класс
Большинство потоковых систем притворяются, что каждое событие обрабатывается независимо. Flink честен: состояние - это первоклассная конструкция. Оператор может хранить произвольный state, который переживает перезапуски и сохраняется в checkpoint. Это открывает класс задач недоступных для stateless систем: sessionization, exact-once агрегации, ML-инференс с накапливаемым контекстом.
Keyed state - самый частый паттерн: поток разбивается по ключу (`keyBy(UserEvent::getUserId)`), каждый ключ получает изолированный state. Гонок данных нет по дизайну - Flink гарантирует, что все события одного ключа обрабатываются строго последовательно в одном task. Это позволяет реализовать fraud detection, sessionization и аномалии за десятки строк кода без внешних баз.
**Типы state**: `ValueState<T>` - одно значение; `ListState<T>` - список; `MapState<K,V>` - словарь; `ReducingState<T>` - инкрементальная агрегация; `AggregatingState<IN,OUT>` - кастомная агрегация. **State backend**: MemoryStateBackend (только dev), FsStateBackend (HDFS/S3), RocksDBStateBackend (production, терабайты state).
RocksDB state backend: state хранится на локальном диске, не в JVM heap - GC паузы не зависят от размера state. Единственный разумный выбор при state > нескольких GB. Downside: операции чтения/записи медленнее heap (~10 мкс vs ~1 мкс). Для hot-path с мелкими объектами HashMapStateBackend (in-heap) может быть быстрее.
Flink job считает количество покупок на пользователя для fraud detection. State backend - MemoryStateBackend. Job обрабатывает 100K уникальных пользователей, state растёт. Какую проблему это создаёт?
Checkpointing: алгоритм Чэнди-Лэмпорта в продакшне
1985 год. Калифорнийский технологический институт. Мани Чэнди (Mani Chandy) и Лесли Лэмпорт публикуют алгоритм распределённого снимка (distributed snapshot). Задача: зафиксировать согласованное глобальное состояние системы без остановки вычислений. Решение элегантно: специальные маркеры (barriers) движутся по каналам передачи данных, операторы снимают своё состояние при получении барьера. Через 30 лет этот алгоритм стал основой Flink checkpointing.
Flink Checkpoint работает так: JobManager периодически посылает barrier в каждый source. Barrier - это специальный маркер "checkpoint N начинается здесь". Источник фиксирует свой offset (позицию в Kafka), посылает barrier дальше. Когда оператор получает barrier от всех входящих каналов (barrier alignment), он сохраняет свой state в distributed storage (HDFS, S3) и отправляет barrier в downstream операторы. Checkpoint считается завершённым, когда все sinks подтвердили получение.
Exactly-once семантика требует barrier alignment: оператор буферизует данные от быстрых входных каналов, пока не получит барьер от медленных. Это добавляет latency. Альтернатива: at-least-once режим без alignment - барьеры не ждут друг друга, события могут попасть в state до снятия snapshot. При восстановлении часть событий обработается повторно.
**Unaligned checkpoints** (Flink 1.11+) - гибрид: барьеры не ждут выравнивания, но данные в буферах операторов включаются в checkpoint. Latency как at-least-once, семантика exactly-once. Решает проблему backpressure-induced checkpoint timeout: при backpressure барьеры застревают в буферах надолго, aligned checkpoint не завершается в срок.
Savepoint - это checkpoint, инициированный вручную, с гарантиями совместимости между версиями job. Checkpoint - автоматический, может быть несовместим при изменении топологии. При upgrade Flink pipeline: остановить job с savepoint (`flink cancel --withSavepoint`), задеплоить новую версию, запустить с этого savepoint. Zero data loss, минимальный downtime.
Exactly-once в Flink означает, что каждое событие обрабатывается ровно один раз
Exactly-once означает, что каждое событие влияет на state и output ровно один раз - даже если при recovery оно обрабатывается повторно
При восстановлении из checkpoint события после последнего checkpoint читаются из Kafka заново. Они обрабатываются второй раз, но state уже содержит snapshot до этих событий - итоговый эффект как будто они обработались один раз. Это end-to-end exactly-once, а не event-level exactly-once.
Flink job с EXACTLY_ONCE semantics испытывает backpressure: один оператор не успевает обрабатывать данные. Как это влияет на checkpoint?
Связанные темы
Flink строится поверх нескольких фундаментальных концепций:
- Apache Spark: основы — Flink приходит туда, где Spark micro-batching слишком медленный
- Kafka: архитектура и паттерны — Kafka - стандартный source для Flink в production
- Stream Processing Patterns — Windowing, sessionization, joins реализуются через Flink API
- CAP-теорема — Distributed snapshot Чэнди-Лэмпорта - прикладная теория из мира DS
Ключевые идеи
- **True streaming vs micro-batching**: Flink обрабатывает каждое событие немедленно - задержка 10 мс вместо 500 мс у Spark Streaming
- **Event time + Watermarks**: детерминированные результаты для опаздывающих событий - watermark W(t) означает "все события до t уже пришли"
- **Keyed state**: состояние изолировано по ключу, переживает перезапуски, масштабируется до терабайт с RocksDB backend
- **Chandy-Lamport checkpointing**: barriers в потоке данных обеспечивают exactly-once без остановки обработки; unaligned checkpoints решают проблему backpressure
Вопросы для размышления
- Почему при параллельном чтении из Kafka watermark определяется минимумом из всех партиций, и как это влияет на idle партиции?
- В чём разница между checkpoint и savepoint с точки зрения upgrade pipeline без потери данных?
- Когда exactly-once семантика в Flink недостаточна и требуется idempotent sink - какие системы хранения данных поддерживают это нативно?
Связанные уроки
- bd-04 — Spark RDD и lazy DAG - фундамент для понимания отличий от true streaming
- bd-05 — Spark SQL micro-batching - точка отсчёта для сравнения архитектур
- bd-10 — Kafka как источник событий для Flink - стандартная продакшн-связка
- bd-11 — Stream processing паттерны реализуются именно через Flink windowing и state
- ds-02-cap-theorem — Chandy-Lamport checkpoint - это distributed snapshot, прямое применение CAP
- ds-01-intro