Learning Platform
Глоссарий Troubleshooting
Урок 02.01 · 25 мин
Продвинутый
PhilosophyStream vs batchBoundednessUnified APIExecution mode

Flink начинался с противоположной философии большинства big-data систем 2010-х. Hadoop, Spark, Hive — все были batch-engines. Streaming у них появился позже как дополнение к batch: Spark Streaming делал micro-batches, чтобы переиспользовать batch-runtime.

Spark Structured Streaming: micro-batch и его последствия Это позволяло быстро поддержать streaming use case, но накладывало ограничения: latency была привязана к size батча, exactly-once требовал отдельных усилий, event-time обрабатывался через workaround.

Flink выбрал обратный путь. Stream — это фундаментальная абстракция. Batch — это streaming-граф над ограниченным потоком данных. Эта философия определяет всё: runtime-архитектуру, API дизайн, как работают checkpoints, что значит “exactly-once”. Без понимания этой философии многие архитектурные решения Flink выглядят странно или избыточно.

В этом уроке разберём, что значит “stream first” на практике, и как Flink в 2026 году реализует unified DataStream API для streaming и batch режимов.

Историческое расхождение Hadoop vs streaming

Чтобы понять Flink, нужно вспомнить контекст. В 2003-2004 годах Google опубликовал две статьи: MapReduce (2004, Dean & Ghemawat) и GFS (2003). Идея MapReduce: дайте задачу, мы её разобьём на map и reduce фазы, raspараллелим по тысячам машин, обработаем огромный датасет, выдадим результат. Эта модель оптимизирована под batch: входной датасет известен, конечен, статичен. Время обработки — часы (или дни) — приемлемо.

Hadoop (2006+) реализовал эту модель в opensource, и она стала доминирующей парадигмой big-data 2007-2014. Spark (2010+) улучшил MapReduce — добавил in-memory DAG-планирование вместо disk-shuffles между MR jobs — но философски остался batch-engine. Spark Streaming (2013) был добавлен поверх как micro-batch wrapper: разрезали поток на маленькие batches (1 секунда, 100ms), исполняли каждый как обычный Spark job.

Параллельно в индустрии вырастал другой класс задач: реальные streaming-приложения. Twitter Storm (2011), LinkedIn Kafka (2011) + Samza (2013), Google MillWheel (2013) — всё это streaming-first системы, которые обрабатывают данные по мере поступления, не дожидаясь батча.

Flink (2014, форк Stratosphere) сделал ставку на streaming-first архитектуру. Стартовое предположение: поток данных бесконечен, и runtime должен быть оптимизирован под это. А батч — это просто частный случай: поток с известным концом.

Что значит “stream first” в runtime

На практике “stream first” означает несколько вещей.

Pipeline исполняется как continuous

В Flink job — это continuous pipeline, а не batch-jobs. Когда вы запускаете job, все операторы стартуют одновременно, и данные текут через них post-completion только для bounded sources (когда source закрылся). В streaming-режиме операторы не закрываются никогда.

Это даёт несколько преимуществ:

  • Низкая latency. Элемент проходит через pipeline без блокировок: source -> map -> keyBy -> window -> sink. Каждый шаг — несколько микросекунд.
  • Backpressure через flow control. Если downstream-оператор медленный, upstream блокируется через credit-based flow control (модуль 03). Нет очередей сообщений между операторами — данные просто медленнее текут.
  • Native event-time. Watermarks (модуль 08) — это специальные сигналы в потоке, которые движутся вместе с данными. Это естественно для streaming, но в batch-runtime требует workaround.

Operator state живёт долго

Операторы в Flink stateful. State (модуль 05) хранится в state backend и обновляется в runtime. Это значит:

  • Window-операторы накапливают элементы окна в state (например, в ListState).
  • Aggregation-операторы хранят промежуточное состояние в ValueState.
  • CEP-операторы (модуль 15) хранят NFA state на каждый pattern instance.

State переживает rescaling, restart, savepoint/restore. В batch-runtime state живёт только в течение одного job-а; в streaming — годами.

Checkpointing — не дополнительный механизм, а core

В Spark exactly-once делается через idempotent writes и atomic commits на уровне batch-job. В Flink — через continuous checkpointing (модуль 06): каждые N секунд (типично 30s-5min) делается snapshot всего state всех операторов, согласованный по алгоритму Чанди-Лэмпорта. Чекпоинт — это атомарный moment-in-time снимок всего pipeline.

Это сложнее, чем batch exactly-once, но даёт гарантии end-to-end exactly-once с continuous low latency. Без checkpointing streaming exactly-once невозможно.

Boundedness.BOUNDED: как batch выражается в streaming API

В Flink 2.x DataStream API — это единственный публичный API для job-ов (Table/SQL — отдельный поверхностный слой, который компилируется в DataStream). DataSet API удалён в 2.0. Это значит: и streaming, и batch пишутся одним API.

Различие между streaming и batch — в типе источника и execution mode.

Boundedness в Flink Source V2
Source.getBoundedness()Метод интерфейса Source`<T, SplitT, EnumChkT>` в Flink Source V2 (FLIP-27). Возвращает Boundedness.BOUNDED или Boundedness.CONTINUOUS_UNBOUNDED.
Boundedness.BOUNDEDИсточник имеет известный конец. Примеры: FileSource в режиме static directory scan, KafkaSource с указанным end offset, JdbcSource. Job завершится, когда все данные обработаны.
Boundedness.CONTINUOUS_UNBOUNDEDИсточник продолжает выдавать данные неограниченно. Примеры: KafkaSource без end offset (по умолчанию), FileSource в monitoring mode, custom Source с external event stream.

Когда вы строите DataStream:

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

KafkaSource<String> source = KafkaSource.<String>builder()
    .setBootstrapServers("localhost:9092")
    .setTopics("events")
    .setStartingOffsets(OffsetsInitializer.earliest())
    .setBounded(OffsetsInitializer.latest())
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build();

DataStream<String> stream = env.fromSource(source,
    WatermarkStrategy.noWatermarks(), "kafka-source");

.setBounded(OffsetsInitializer.latest()) — превращает Kafka source в bounded source: он читает от earliest до latest (на момент старта job-а) и завершается. Этот же job в streaming-режиме (без .setBounded) был бы unbounded и работал бы вечно.

Execution mode: STREAMING vs BATCH

Когда все sources в job-е bounded, Flink может выбрать один из двух execution modes:

env.setRuntimeMode(RuntimeExecutionMode.STREAMING);  // default
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
env.setRuntimeMode(RuntimeExecutionMode.AUTOMATIC);  // выберет BATCH если все sources bounded

Различие в runtime — фундаментальное.

STREAMING vs BATCH execution mode
STREAMING modeВсе операторы стартуют одновременно. Pipeline исполняется как continuous flow. Checkpoints. State backend (RocksDB / Heap).
BATCH modeStage-based execution. Операторы исполняются последовательно по stages, разделённым shuffle boundaries. Нет checkpoints — recovery через re-execution. Spilling state на диск.
Operator chaining: aggressiveВ STREAMING max chaining: соседние операторы соединяются в один Task, чтобы избежать сериализации. Network shuffle только при keyBy.
Shuffle: per-stageВ BATCH каждая stage завершается полностью перед следующей. Между stages — disk shuffle (blocking) или pipelined (для simple cases).
Checkpoint: каждые N секSTREAMING: периодические async snapshots всего state. Failure recovery — restart с последнего checkpoint.
Checkpoint: НЕТBATCH: нет checkpoints. Failure recovery — re-execute upstream stage от точки сохранённых intermediate results.
State: RocksDB / HeapSTREAMING state — на диске (RocksDB) или в heap. Доступ через ColumnFamily / HashMap.
State: sorted spillingBATCH state — сортируется по key и spilling на диск. После обработки stage — освобождается.
Watermarks: реальныеSTREAMING — watermarks движутся с данными, влияют на window firing.
Watermarks: +inf на endBATCH — watermarks как бы equal MAX_WATERMARK на конце stage; windows всё видят сразу после input completion.

Когда BATCH mode полезен:

  • Backfill of streaming pipeline. У вас есть streaming-job, который обрабатывает Kafka realtime. Историческая обработка — год архива данных. Запустить тот же job в BATCH mode на bounded Kafka source — оптимально по ресурсам.
  • ETL-style jobs. Periodic batch jobs, которые читают из S3/HDFS и пишут результаты. BATCH-runtime эффективнее, чем гонять streaming-pipeline на bounded input.
  • Reprocessing с новой логикой. Поменяли логику оператора — запустили в BATCH-mode на исторических данных, перезагрузили state, переключились на streaming.

Главный класс: ExecutionConfig + StreamGraph

В runtime эта философия выражается через org.apache.flink.streaming.api.environment.StreamExecutionEnvironment#getStreamGraph, который строит StreamGraph с учётом execution mode и boundedness sources.

Ключевые места кода:

// StreamExecutionEnvironment, упрощённо:
public JobClient executeAsync(StreamGraph streamGraph) throws Exception {
    // ...
    // RuntimeExecutionMode определяет StreamGraph properties:
    //  - в STREAMING: ScheduleMode.EAGER, ResultPartitionType.PIPELINED
    //  - в BATCH:     ScheduleMode.LAZY,  ResultPartitionType.BLOCKING
    // ...
}

Для STREAMING режима ResultPartitionType.PIPELINED: оператор пишет байты в network buffer и сразу отдаёт consumer-у. Если consumer медленный — backpressure через credit.

Для BATCH режима ResultPartitionType.BLOCKING: оператор пишет байты в network buffer полностью, и только когда буфер закроется — consumer начинает читать. Это позволяет fault tolerance через re-execution: если consumer упал, мы можем перезапустить его и переиграть buffered результат, без перезапуска upstream stage.

Конкретный класс, отвечающий за выбор — org.apache.flink.streaming.api.graph.StreamGraphGenerator, и далее org.apache.flink.streaming.api.graph.StreamingJobGraphGenerator превращает StreamGraph в JobGraph.

Unified Table API: SQL поверх streaming

Table API и SQL — отдельный слой поверх DataStream. Но философия “stream first” сохраняется: каждая Table — это changelog stream под капотом.

CREATE TABLE Orders (
  order_id BIGINT,
  user_id BIGINT,
  amount DECIMAL(10, 2),
  ts TIMESTAMP(3),
  WATERMARK FOR ts AS ts - INTERVAL '5' SECONDS
) WITH (
  'connector' = 'kafka',
  'topic' = 'orders',
  ...
);

SELECT user_id, SUM(amount) AS total
FROM Orders
GROUP BY user_id;

Этот SELECT в Flink SQL — streaming aggregation. Результат: changelog stream с операциями +I (insert), -U (update before), +U (update after). Каждый раз, когда приходит новый order для user_id=42, появляется -U (старый total) + +U (новый total).

Это не batch SELECT. Это continuous query над unbounded stream. Та же query на bounded источнике (с 'scan.bounded.mode' = 'latest-offset') даёт обычный batch SELECT — финальный snapshot после обработки всех данных.

Унифицированность: одна SELECT, два режима выполнения, в зависимости от boundedness источника.

NOTE

В Apache Beam (Dataflow model) это же различие выражается через триггеры: WatermarkBasedTrigger vs RepeatedlyForever. Flink не использует Beam, но философию заимствует — стрим — фундамент, батч — частный случай с terminal watermark.

Почему “batch first” не работает для streaming

Альтернативная философия — batch first — была у Spark Streaming. Идея: возьмём batch-runtime, разрежем поток на маленькие batches (micro-batches), исполним каждый.

Проблемы такого подхода:

Latency привязана к size батча

Если micro-batch = 1 секунда, минимальная latency = 1 секунда (а в среднем — 0.5 с). Чтобы получить миллисекундную latency, нужно micro-batch = 10 мс, но overhead на запуск batch-job такой, что throughput катастрофически падает.

Flink, не имея micro-batch концепции, имеет per-element latency в диапазоне миллисекунд — потому что элемент идёт через operator chain без блокировок.

Spark Structured Streaming: watermarks и опоздавшие данные

Window-семантика странная

В Spark Structured Streaming окна определяются на уровне query (window(col("ts"), "5 minutes")). Но runtime — micro-batch. Если ваше окно 5 минут, а micro-batch 1 секунда, runtime должен буферизировать данные нескольких micro-batches до триггера окна. Это работает, но complexity ложится на user и не natively выражено.

В Flink окно — это first-class concept оператора. WindowOperator буферизирует элементы окна в state, и при срабатывании trigger-а (по watermark или processing time) — испускает результат. Просто и natively.

Event-time через workaround

В batch-first моделях event-time обрабатывается через groupBy(window(timestamp)). Это работает, но требует sortBy timestamp на каждом batch boundary, что дорого. Late data — отдельная проблема: micro-batches уже отправили результат, как доехавшийся late event повлияет?

В Flink event-time — через watermarks (модуль 08). Watermark — это signal “событий с timestamp ≤ T больше не будет”. WindowOperator закрывает окно по watermark и испускает result. Late events после watermark обрабатываются через allowedLateness. Чистая модель.

Exactly-once требует idempotency

Spark exactly-once: каждый micro-batch имеет уникальный ID, и output sink должен быть idempotent (write-or-skip по batch ID). Это работает с многими sinks (S3, HDFS), но не работает с Kafka transactional producer без extra coordination.

Flink exactly-once: через 2PC protocol (модуль 12). Каждый checkpoint — атомарный moment-in-time снимок всего pipeline + state + offsets sources + transactions sinks. Это нативно работает с Kafka transactional producer, JDBC XA, FileSystem (через rename), и так далее.

Три уровня доставки: at-most, at-least, exactly-once Управление offset в Kafka-консьюмерах

Где stream-first проигрывает

Будем честны. Не во всех случаях stream-first архитектура лучше.

Скорость batch ETL

Если у вас разовый ETL job на 10 TB данных, batch-runtime (Spark) обычно быстрее, чем streaming-runtime. Причины:

  • Batch может делать sort-merge join, который оптимально использует диск (vs streaming join, который держит state в памяти).
  • Batch может агрессивно использовать column pruning, predicate pushdown, vectorized processing.
  • Batch не платит за continuous overhead (checkpointing, watermark propagation).

Flink в BATCH mode пытается закрыть этот gap, но всё ещё проигрывает Spark на чистом batch для типичных DWH-задач.

Простота ad-hoc анализа

spark.sql("SELECT * FROM ... WHERE ... GROUP BY ...") в Jupyter — простой workflow для data scientist. Flink здесь сложнее: streaming SELECT возвращает unbounded result, который сложно положить в DataFrame для дальнейшего анализа.

Для ad-hoc data exploration data scientists обычно используют Spark / Trino / DuckDB, а не Flink.

Operational complexity

Streaming-pipeline сложнее эксплуатировать, чем batch:

  • Нужно мониторить checkpoint durations, backpressure, watermark lag.
  • Нужно правильно настраивать state TTL.
  • Нужно понимать savepoint upgrade workflow.

Batch — запустил, дождался завершения, проверил output. Простота.

Flink выбирает быть good at streaming, decent at batch. Если у вас pure batch workloads — Spark или Trino. Если у вас streaming или mixed — Flink.

Практическое следствие

Когда вы пишете Flink-программу, постоянно держите в голове: это будет работать как continuous pipeline. Это меняет интуицию:

  • Не делайте долгих синхронных операций в map(). В batch это норм (несколько секунд на элемент ок), в streaming — гарантированный backpressure.
  • Берегите state. Каждый byte состояния — это byte, который пойдёт в каждый checkpoint. Если state растёт unbounded, рано или поздно job упадёт. Используйте TTL.
  • Понимайте, что watermarks — это сигналы в потоке. Они влияют на window firing. Если watermark stuck — окна не закрываются — output не появляется.
  • Понимайте, что rescaling — это не runtime операция. Чтобы изменить parallelism, нужно сделать savepoint, остановить job, запустить с новым parallelism из savepoint. (С Adaptive Scheduler — частично можно без savepoint, но это отдельная тема, модуль 11.)
WARNING

Если приходите в Flink из Spark, отучите себя от batch-интуиций. Думать “ну я запущу stream-job и через час он закончится” — это путь к багам. Stream-job не “заканчивается” — он работает, пока кто-то его не остановит. Если ваш use case — финальный (исторический backfill), используйте BATCH mode явно.

Что дальше

Следующий урок — про state и time как first-class citizens. Откуда эта идея пришла исторически (Storm, MillWheel, Dataflow), и как именно Flink реализует первоклассный state в runtime.

После — урок про эволюцию Flink от 1.x к 2.2: что было удалено, что добавлено, как мигрировать.

Проверка знанийKnowledge check
У вас Flink-pipeline для real-time fraud detection: Kafka source (unbounded) -> KeyedProcessFunction с rich state (ValueState + MapState, 100 GiB total) -> Kafka sink с exactly-once. Вам нужно сделать backfill за последние 30 дней истории (~3 TB) из Kafka. Параметры stream-job-а оптимизированы под realtime: checkpoint interval 30s, RocksDB block cache 2 GiB на TM, RocksDBPredefinedOptions.SPINNING_DISK_OPTIMIZED. Какие проблемы вы предвидите если просто запустить тот же job на bounded Kafka source за 30 дней истории, и какие архитектурные изменения вы бы сделали?
ОтветAnswer
Проблемы: (1) Checkpoint interval 30s в backfill бессмыслен — данные текут на максимальной throughput, каждый checkpoint — overhead, лучше делать реже (5-10 минут) или вообще выключить (FsStateBackend без чекпоинтов, recovery через re-execution). (2) RocksDB block cache 2 GiB рассчитан на realtime read-heavy workload; в backfill rate write больше, эффективнее увеличить write buffer и MemTable. (3) SPINNING_DISK_OPTIMIZED — для HDD; для backfill хочется FLASH_SSD_OPTIMIZED с агрессивной compaction. (4) Главное: stream-mode с continuous checkpointing — это overhead на 3 TB throughput. Лучшее решение — переключить job в RuntimeExecutionMode.BATCH: shuffle становится blocking, нет checkpoints, state сортируется и spilling на диск (sorted shuffle), recovery через re-execution stage — оптимально для bounded. Но тут есть catch: BATCH mode не поддерживает all DataStream APIs, особенно те, что зависят от processing time или асимметричной обработки timer-ов; нужно проверить, что используемый KeyedProcessFunction совместим с BATCH mode. Альтернатива — оставить STREAMING с настройками для high-throughput: checkpoint interval 5min, увеличенный state.backend.rocksdb.write-buffer-size, exactly-once -> at-least-once для backfill (Kafka idempotent producer).

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что значит 'stream first, batch is bounded stream' в контексте Flink runtime?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 3