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 的关系

Task 与 SubTask 关系图 Task: flatMap SubTask[0] SubTask[1] SubTask[2] 并行度 = 3 Task: keyBy SubTask[0] SubTask[1] 并行度 = 2 Task: sum SubTask[0] SubTask[1] 并行度 = 2 每个 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 时的数据流转过程。

WordCount 并行执行流程(并行度=2) Socket Source SubTask[0] SubTask[1] flatMap SubTask[0] SubTask[1] keyBy + sum SubTask[0] SubTask[1] keyBy 根据单词哈希值将数据路由到 对应的 SubTask,相同单词进入同一 SubTask print() 输出 输入: "hello flink" "hello world" 切分后: (hello,1) (flink,1) (hello,1) (world,1) 聚合结果: (hello,2) (flink,1) (world,1)

4.6 核心概念总结

概念 定义 类比
并行度 算子或作业的并行执行实例数 工厂中同一条生产线的工人数量
Task 算子的逻辑执行阶段 生产线上的一个工序
SubTask Task 的并行执行实例,每个 SubTask 是一个独立线程 执行该工序的单个工人
算子链 多个算子合并为一个 Task,减少线程间开销 将多个工序合并由同一工人完成
核心要点:Flink 的并行能力来源于 SubTask 的分布式执行。每个 SubTask 处理数据流的一个分区,多个 SubTask 并行工作,共同完成整个算子的处理逻辑。理解 Task 与 SubTask 的映射关系,是后续学习 Flink 资源调优、反压排查和故障恢复的基础。