4. Flink 核心概念:DataStream、Source、Transformation、Sink、JobManager、TaskManager

好,咱们进入第四章。这一章我打算把 Flink 的六个核心概念掰开揉碎了讲清楚。你可能会觉得这些名词在官方文档里都见过,但说实话,我当年刚接触 Flink 时,光是搞明白 JobManager 和 TaskManager 到底谁管谁,就花了不少功夫。

这六个概念,说白了就是 Flink 应用的骨架和血肉。你写一个 Flink 作业,本质上就是在跟它们打交道。咱们一个一个来。

4.1 DataStream:流动的数据本身

DataStream 是 Flink 里最核心的数据抽象。你可以把它想象成一条永不停歇的数据河流。每条数据就是一个事件,它们按顺序流过来。

核心要点:DataStream 代表一个无限的数据流。它不关心数据总量,只关心当前这条数据怎么处理。

我在项目中遇到过一个问题:刚上手时总把 DataStream 当成一个集合(Collection)来理解,想着“等数据到齐了再处理”。这其实是个误区。Flink 是流式处理,数据永远到不齐。你只能一条一条地处理,或者按时间窗口攒一批处理。

DataStream 本身不存数据,它只是一个逻辑视图。真正干活的是它背后的算子(Operator)。

4.2 Source:数据从哪来

Source 是 Flink 作业的入口。没有 Source,你的作业就是无源之水。

常见的 Source 有几种:

  • 文件 Source:读取本地或 HDFS 上的文件,支持 Text、CSV、Parquet 等格式。
  • Socket Source:从网络 Socket 读取数据,测试时常用。
  • Kafka Source:从 Kafka Topic 消费数据,这是咱们课程的重点。
  • 自定义 Source:实现 SourceFunction 接口,从任何数据源拉数据。

写一个最简单的 Kafka Source 示例:

DataStream<String> stream = env
    .addSource(new FlinkKafkaConsumer<>(
        "my-topic",
        new SimpleStringSchema(),
        properties
    ));

我的习惯:生产环境中,我一般会给 Source 设置一个 setStartFromLatest() 或 setStartFromEarliest(),避免每次重启都从头消费。具体用哪个,取决于业务能不能容忍丢数据。

4.3 Transformation:数据怎么变

Transformation 是 Flink 作业的“加工车间”。Source 把原材料拉进来,经过一系列 Transformation,变成你想要的输出。

常见的 Transformation 包括:

算子 作用 一对一还是多对多
map 一条进,一条出,类型可以变 一对一
flatMap 一条进,零到多条出 一对一
filter 保留符合条件的记录 一对一
keyBy 按 key 分组,后续算子可以并行处理 多对多(shuffle)
window 按时间或数量攒批 多对多

举个例子,从 Kafka 读到的 JSON 字符串,先解析成 Java 对象,再过滤掉无效数据,最后按用户 ID 分组:

DataStream<UserEvent> parsed = stream
    .map(json -> JsonUtil.parse(json, UserEvent.class));

DataStream<UserEvent> valid = parsed
    .filter(event -> event.getUserId() != null);

DataStream<UserEvent> keyed = valid
    .keyBy(UserEvent::getUserId);

这里要注意,keyBy 之后的数据流类型变成了 KeyedStream,它比普通的 DataStream 多了窗口和状态操作的能力。

我曾经踩过的坑:在 keyBy 之前用了 map 做状态操作,结果状态一直不生效。后来才意识到,状态操作必须在 keyBy 之后才能用。说白了,Flink 的状态是按 key 分片的,没有 key 就没法分片。

4.4 Sink:数据去哪了

Sink 是 Flink 作业的出口。数据经过 Transformation 加工后,最终要写到某个地方去。

常见的 Sink 包括:

  • Kafka Sink:写回 Kafka 的另一个 Topic。
  • JDBC Sink:写入 MySQL、PostgreSQL 等关系型数据库。
  • File Sink:写入 HDFS 或本地文件系统。
  • Elasticsearch Sink:写入 ES 用于搜索和分析。

写一个 Kafka Sink 的示例:

stream
    .addSink(new FlinkKafkaProducer<>(
        "output-topic",
        new SimpleStringSchema(),
        properties
    ));

嗯,这里要提醒一下:Sink 的并行度可以单独设置。如果你的下游系统扛不住高并发,可以调低 Sink 的并行度,甚至设为 1。

4.5 JobManager:作业的大脑

JobManager 是 Flink 集群的“大脑”。它负责三件事:

  1. 接收作业:客户端提交的作业,先到 JobManager 手里。
  2. 生成执行计划:JobManager 把 DataStream 的 DAG(有向无环图)转换成物理执行计划。
  3. 协调调度:把任务分发给 TaskManager,并监控它们的运行状态。

你想想看,JobManager 就像是一个项目经理。它不亲自写代码,但它知道谁该干什么,什么时候干,干不完怎么办。

如果 JobManager 挂了,整个集群就瘫痪了。所以生产环境一般会配置 High Availability(HA),用 ZooKeeper 做 Leader 选举,保证 JobManager 挂了之后能自动切换。

4.6 TaskManager:作业的双手

TaskManager 是真正干活的“工人”。每个 TaskManager 是一个 JVM 进程,里面可以运行多个 Task Slot(任务槽)。

Task Slot 是 Flink 的最小资源单位。一个 Task Slot 可以运行一个线程。你可以把 Task Slot 理解为工位,每个工位只能坐一个人。

举个例子:如果你的作业有 4 个并行度,每个 TaskManager 有 2 个 Slot,那就需要 2 个 TaskManager 来跑。

关键区别:JobManager 管“做什么”,TaskManager 管“怎么做”。JobManager 发号施令,TaskManager 埋头干活。

4.7 它们怎么配合?一张图看懂

下面这张 SVG 图展示了从 Source 到 Sink 的完整数据流,以及 JobManager 和 TaskManager 的分工。

Flink 核心概念协作图 Flink 核心概念协作图 Kafka Source map / filter keyBy / window Kafka Sink JobManager 接收作业 · 生成计划 · 调度 TaskManager 执行任务 · 管理 Slot 说明 JobManager 管“做什么” TaskManager 管“怎么做”

4.8 总结一下

这六个概念,我建议你按这个顺序理解:

  • Source 把数据拉进来,变成 DataStream。
  • Transformation 对 DataStream 做各种加工。
  • Sink 把加工后的数据写出去。
  • 这一切的调度和执行,由 JobManager 和 TaskManager 协作完成。

说白了,Flink 作业就是一个“Source → Transformation → Sink”的管道。JobManager 负责管道的设计图纸,TaskManager 负责按图纸施工。

我个人习惯在写作业前,先在纸上画出这个管道图。哪一步用 map,哪一步用 keyBy,Sink 写到哪里,心里有数了再动手。这样写出来的代码,结构清晰,不容易出 bug。