4. Flink DataStream 入门:第一个流处理程序(WordCount)
本章将带领你编写并运行第一个 Flink DataStream 程序——流式 WordCount。通过这个经典案例,你将直观理解 Flink 流处理的核心概念:并行度(Parallelism)、Task 与 SubTask。这些概念是理解 Flink 分布式执行引擎的基石。
4.1 流式 WordCount 程序实战
与批处理不同,流式 WordCount 处理的是无界数据流,即数据源源不断地产生,程序持续运行并输出实时统计结果。下面是一个完整的 Flink DataStream WordCount 程序。
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;
public class StreamWordCount {
public static void main(String[] args) throws Exception {
// 1. 创建流处理执行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 2. 设置全局并行度为 2(可选,默认等于 CPU 核数)
env.setParallelism(2);
// 3. 从 socket 读取数据流(无界流)
DataStream<String> textStream = env.socketTextStream("localhost", 9999);
// 4. 数据转换:切分、映射、分组、聚合
DataStream<Tuple2<String, Integer>> wordCountStream = textStream
.flatMap(new Tokenizer()) // 切分单词
.keyBy(value -> value.f0) // 按单词分组
.sum(1); // 对第二个字段求和
// 5. 打印结果(并行输出到标准输出)
wordCountStream.print();
// 6. 启动执行
env.execute("Stream WordCount");
}
// 自定义 FlatMap 函数:将每行文本切分为单词
public static class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {
@Override
public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
String[] words = value.toLowerCase().split("\\W+");
for (String word : words) {
if (word.length() > 0) {
out.collect(new Tuple2<>(word, 1));
}
}
}
}
}
运行提示:在启动程序前,需要先在终端执行
nc -lk 9999 开启一个 socket 数据源。然后在 IDE 中运行 StreamWordCount 主类,在终端输入单词即可看到实时统计结果。
4.2 并行度(Parallelism)
并行度是 Flink 中算子或作业被并行执行的子任务数量。它决定了每个算子同时运行的实例数,直接影响处理吞吐量和资源利用率。
4.2.1 并行度的层级设置
| 层级 | 设置方式 | 生效范围 | 优先级 |
|---|---|---|---|
| 算子级别 | dataStream.map(...).setParallelism(4) |
单个算子 | 最高 |
| 执行环境级别 | env.setParallelism(2) |
当前作业所有算子 | 中 |
| 提交命令级别 | ./bin/flink run -p 4 |
整个作业 | 低 |
| 配置文件级别 | parallelism.default: 1(flink-conf.yaml) |
集群所有作业 | 最低 |
关键理解:并行度不是越大越好。过高的并行度会导致任务调度开销增加、网络 shuffle 加剧、资源碎片化。通常建议并行度与可用 CPU 核数或 Kafka 分区数保持一致。
4.3 Task 与 SubTask
在 Flink 的运行时架构中,Task 和 SubTask 是描述执行单元的两个核心概念。
- Task(任务):对应一个算子(Operator)的逻辑阶段。例如
flatMap、keyBy、sum各是一个 Task。 - SubTask(子任务):Task 的并行执行实例。每个 SubTask 由一个独立的线程执行,处理数据流的一个分区。
一个 Task 的并行度等于其 SubTask 的数量。例如,flatMap 算子并行度设为 3,则它对应 3 个 SubTask,每个 SubTask 处理一部分输入数据。
4.3.1 Task 与 SubTask 的关系
4.4 算子链(Operator Chains)与 Task 合并
Flink 会将满足条件的多个算子链接(chaining)成一个 Task,以减少线程间数据传输和序列化开销。被链接的算子运行在同一个线程中,SubTask 共享同一个 Slot。
4.4.1 算子链的合并条件
| 条件 | 说明 |
|---|---|
| 并行度相同 | 两个算子的并行度必须一致 |
| one-to-one 分区 | 上游 SubTask 的数据直接发往下游对应 SubTask,无 shuffle |
| 无 keyBy/rebalance 等重分区操作 | 重分区会打断算子链 |
注意:在 WordCount 程序中,
flatMap 和 keyBy 之间由于存在 keyBy 重分区,算子链会被打断。keyBy 和 sum 之间同样无法链式合并,因为 sum 需要基于 key 的分组状态。
4.5 从 WordCount 理解并行执行流程
下面通过一个完整的执行流程图,展示 WordCount 程序在并行度为 2 时的数据流转过程。
4.6 核心概念总结
| 概念 | 定义 | 类比 |
|---|---|---|
| 并行度 | 算子或作业的并行执行实例数 | 工厂中同一条生产线的工人数量 |
| Task | 算子的逻辑执行阶段 | 生产线上的一个工序 |
| SubTask | Task 的并行执行实例,每个 SubTask 是一个独立线程 | 执行该工序的单个工人 |
| 算子链 | 多个算子合并为一个 Task,减少线程间开销 | 将多个工序合并由同一工人完成 |
核心要点:Flink 的并行能力来源于 SubTask 的分布式执行。每个 SubTask 处理数据流的一个分区,多个 SubTask 并行工作,共同完成整个算子的处理逻辑。理解 Task 与 SubTask 的映射关系,是后续学习 Flink 资源调优、反压排查和故障恢复的基础。