2.2 Flink 程序与数据流图 (Dataflow Graph)


在我们深入了解数据流图之前,先来看看它的源头------一个基础的 Flink 程序。无论多复杂的 Flink 作业,其代码结构通常都遵循一个经典的四步模式,就像写一个烹饪配方:

  1. 准备"厨具" (Execution Environment):获取一个执行环境,这是所有 Flink 操作的基础。
  2. 选取"食材" (Source):定义数据从哪里来。它可以是 Kafka 消息队列、一个文件,甚至是一个内存中的集合。
  3. 设计"烹饪步骤" (Transformation):对数据进行一系列的转换操作,比如过滤、映射、分组、聚合等。这是数据处理的核心逻辑。
  4. 上菜 (Sink):定义处理完的数据要发送到哪里,比如写回 Kafka、存入数据库或直接打印到控制台。

让我们来看一个最经典的"WordCount"配方,它的目标是统计一段文本中每个单词出现的次数。

代码示例 (Java):

java 复制代码
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 WordCount {

    public static void main(String[] args) throws Exception {
        // 1. 准备"厨具":获取流处理执行环境
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        // 2. 选取"食材":从内存中创建一个数据流
        DataStream<String> text = env.fromElements(
                "Hello Flink Stream Word Count",
                "Hello World Flink Java API"
        );

        // 3. 设计"烹饪步骤":
        DataStream<Tuple2<String, Integer>> counts = text
                // a. 将每行文本打散成单词 (FlatMap)
                .flatMap(new Tokenizer())
                // b. 按单词进行分组 (keyBy)
                .keyBy(value -> value.f0)
                // c. 对每个单词的数量进行累加 (sum)
                .sum(1);

        // 4. "上菜":将结果打印到控制台
        counts.print();

        // 执行任务!
        // 关键一步:直到调用 execute(),上面所有的定义才会被真正构建并提交
        env.execute("Streaming WordCount");
    }

    /**
     * 自定义分词函数
     * 将一行字符串拆分为一个个的 (单词, 1)
     */
    public static final class Tokenizer implements FlatMapFunction<String, Tuple2<String, Integer>> {
        @Override
        public void flatMap(String value, Collector<Tuple2<String, Integer>> out) {
            // 标准化并按空格切分
            String[] tokens = value.toLowerCase().split("\\W+");
            for (String token : tokens) {
                if (token.length() > 0) {
                    out.collect(new Tuple2<>(token, 1));
                }
            }
        }
    }
}

这段代码非常清晰地定义了一个完整的数据处理流程。但是,当你点击运行时,Flink 并不是逐行解释执行这些 Java 代码。相反,它会做一次"魔术转换"。

二、数据流图 (Dataflow Graph):从"配方"到"流程图"

上面我们编写的代码,其实是一种"声明式"的 API。我们只是声明了我们想做什么,但并没有指定具体怎么做。当我们调用 env.execute() 时,Client 端(比如你的 IDE 或者命令行工具)的 Flink 优化器就会介入。

它会解析你写的全部代码,把你的"配方"翻译成一个标准的、逻辑化的流程图 ,这就是数据流图 (Dataflow Graph)

数据流图的特点:

  • 有向无环图 (DAG):这是一个关键特征。数据流在一个方向上从源头流向终点,中间不会形成循环。这保证了数据处理的可终止性和可预测性。
  • 节点 (Nodes) :图中的每个节点代表一个算子 (Operator)。这包括数据源 (Source)、各种转换 (Transformation) 和数据汇 (Sink)。
  • 边 (Edges) :图中的有向边代表了数据流 (DataStream)。它表示数据如何从一个算子的输出流向另一个算子的输入。

对于我们上面的 WordCount 代码,Flink 生成的数据流图在逻辑上看起来是这样的:

三、为什么数据流图如此重要?

你可能会问,为什么要多此一举,非要生成这么一个图呢?直接执行代码不行吗?

这个中间步骤是 Flink 分布式计算的精髓所在,它带来了巨大的好处:

  1. 优化 (Optimization) :JobManager 在拿到这份"蓝图"后,并不是直接执行。它可以根据图的结构进行优化。例如,它可以发现某些连续的操作(比如一个 map 后面紧跟着一个 filter)可以合并成一个任务执行,从而减少数据的网络传输和序列化开销。这个过程叫做算子链(Operator Chaining)
  2. 并行化 (Parallelization):这份清晰的逻辑图让 Flink 能够一目了然地知道哪些计算步骤可以并行执行。JobManager 会根据配置的并行度,将图中的每一个算子拆分成多个并行的子任务,分发到不同的 TaskManager 上去执行,从而实现大规模的分布式计算。
  3. 任务分发 (Distribution):数据流图是 JobManager 向 TaskManager 分配任务的依据。它将这个逻辑图转化为一个物理执行图(ExecutionGraph),精确地定义了每个 TaskManager 需要运行哪些任务,以及任务之间的数据如何交换。
四、如何查看数据流图?

最直观的方式就是通过 Flink 的 Web UI。当你提交一个作业后,可以打开 Flink 的 Web 控制台(默认是 localhost:8081),在作业详情页中,你会看到一个与上面类似的、由 Flink 自动为你生成的图形化界面,它真实地展示了你的代码所对应的逻辑视图。

此外,你也可以在代码中生成它。

java 复制代码
// 在调用 env.execute() 之前
System.out.println(env.getExecutionPlan());

这行代码会以 JSON 格式打印出作业的执行计划,这其实就是数据流图的文本表示。

最后

让我们回顾一下:

  • 我们用 Flink API 编写的是一份数据处理的"配方"。
  • 在作业提交时,Flink Client 会将这份"配方"翻译成一张标准的、逻辑化的"流程图",即数据流图 (Dataflow Graph)
  • 这张图是 Flink 进行优化、并行化和任务分发的基础,是连接我们业务逻辑和 Flink 分布式运行时的核心桥梁。