【4】Flink 编程风格与底层数据结构设计

290 阅读5分钟

本文及后续只说 flink 的流式处理模式,即 DataStream 类型的数据处理,DataSet 不在考虑范围内。

Flink 编程风格

一个 flink 的 job 一般包括输入、处理和输出三个部分。

我们来看一段经典的 flink 流程:

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.flink.api.common.functions.FlatMapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.util.Collector;

@Slf4j
public class StreamWordCountMain {
  public static void main(String[] args) throws Exception {
    // 获取当前执行环境
    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    // 添加数据源,WordDataSource 中不断产生数据
    DataStreamSource<String> dataSource = env.addSource(new WordDataSource());
    // 对 dataSource 中的文本进行分词处理,并对每个词语计数为 1
    SingleOutputStreamOperator<Tuple2<String, Long>> flatMap = dataSource.flatMap(
        new FlatMapFunction<String, Tuple2<String, Long>>() {
          @Override
          public void flatMap(String line, Collector<Tuple2<String, Long>> collector) throws Exception {
            String[] strs = line.split("\\s+");
            for (String s : strs) {
              if (StringUtils.isNotBlank(s)) {
                collector.collect(Tuple2.of(s, 1L));
              }
            }
          }
        });

    flatMap.keyBy(k -> k.f0) // 按照 key 分区
        .sum(1) // 按照第一个字段进行求和
        .print(); // 打印及输出

    // 开始执行
    env.execute();
  }
}

如上述程序所示:一开始获取执行环境,然后定义一系列的处理流程,这些处理流程更像是定义了一系列的 pipeline,在最后通过 env.execute() 来运行 job,此时 pipeline 才真正开始运行。

这是 flink 的编程风格,更像是一种函数式编程,先定义过程,后开始执行。

数据结构设计

如上述程序所示:

  • 添加数据源,WordDataSource 继承自 SourceFunction,并间接继承自 Function 接口,添加数据源后,返回一个 DataStreamSource 类型,该类型间接继承自 DataStream 类;
  • flatMap 中定义具体的处理逻辑,flatMap 中传的参数也是继承自 Function 接口,并返回 DataStream 的子类;
  • keyBy、sum 等与 flatMap 类似;
  • print 中实际使用了 SinkFunction 接口,也是间接继承自 Function 接口。

基于以上,我们可以看到,在数据处理环节,无论是输入数据、数据处理、还是输出数据,都是使用 Function 的子类来完成,而在数据传输环节,都是使用 DataStream 的子类来完成,即:

// 类继承调用关系
<? extends DataStream> stream1 = stream0.process(<? extends Function> func);
<? extends DataStream> stream2 = stream1.process(<? extends Function> func);

DataStream 继承关系及运行机制

org.apache.flink.streaming.api.datastream.DataStream 是用于数据传输的 API,其类继承关系如下:

DataStream  // 原始类型
  +--- SingleOutputStreamOperator  // 可指定输出类型,预定义了比如设置并行度等方法
         +--- DataStreamSource  // 提供更丰富的构造方法,一般可以直接使用
         +--- IterativeStream  // 用于对流式数据重复迭代处理使用
  +--- KeyedStream // 按照 Key 分区后的数据流

而 DataStream 所关联派生出来的类还包括:

JoinedStreams:两个流之间等 key 连接,类似与 MySQL 中的 join 操作,通过一个 key 将值连接起来;
ConnectedStreams:使用两个不同输入类型的 DataStream 合并在一起形成的新的 Stream,可以连接类型不一致的数据流(union 只能连接多个一致数据流),允许双流之间共享状态,提供对两个流的数据应用不同处理方法的能力;
CoGroupedStreams:与 JoinedStreams 区别在于,JoinedStreams 专注对一对数据进行处理,而 CoGroupedStreams 专注对一组相同 key 的数据进行处理;
WindowedStream:允许数据按照窗口聚合,基于 KeyedStream,窗口聚合方法在后续会介绍到;
AllWindowedStream:直接基于 DataStream 做窗口聚合,不区分数据的 Key;
... 其他更多的参考:org.apache.flink.streaming.api.datastream 下面的类定义。

DataStream 只是一个逻辑概念,实际在运行的时候是通过 DataStream 中的 Transformation 来完成数据转换的。即 DataStream 的 pipeline 最终都会转换为 Transformation 的 pipeline 来执行。StreamGraph 的构建是基于 Transformation 来构建的。在运行的时候,数据在 Transformation 之间流动。

各种类型的 DataStream 及 API 转换关系如下:

image.png

关于各个 API 的功能请自行查阅即可。

Function 运行机制

在上述代码中,我们看到 dataSource.flatMap(xxx) 的代码,flatMap 中传的是一个 org.apache.flink.api.common.functions.Function 的对象。Function 接口本身只是一个标记类,不带有方法。

Function 接口继承自 java.io.Serializable,表示其需要被序列化和反序列化,这主要是 TaskManager 在实际运行的时候,需要将 Function 对象复制到各个 TaskManager 运行。所以延伸了一个问题,如果在 TaskManager 中使用了 Spring 框架,只是复制 Function 其实不会初始化 Spring 容器,而需要在 Function 中重新初始化。

所有的处理过程都会继承自 Function 接口,如 flatMap 使用的是 FlatMapFunction 接口,用户可以自定义 Function 接口即:User Define Function(UDF),使用自定义 Function 可以完成更复杂的事情。

常见的 Function 包括比如 sum、flatMap 中使用的以外,一般常用的几个 Function 主要包括:

  • RichFunction 系列:定义了一个完整的生命周期,有 open / close / getRuntimeContext / setRuntimeContext / getIterationRuntimeContext 等过程;
  • AggregateFunction 系列:用于聚合使用;
  • KeySelector 系列:用于 keyBy 操作指定计算 key,包含 getKey 方法;
  • CheckpointedFunction 系列:用于响应和操作 checkpoint,包含 initializeState / snapshotState 方法,分别用于初始化和生成 checkpoint 时的动作;
  • SourceFunction 系列:用于定义数据源;

Function 中的逻辑在 TaskManager 中实际运行。

其他数据结构

其他的数据结构比如 StreamOperator 系列等,他们之间都有一定关系。

  • DataStream 是 Flink 中用于表示数据流的 API,它是用户进行数据流处理的入口。【侧重数据流定义】
  • Function 是对数据流中元素进行操作的函数,是数据流转换的基本单元。【侧重数据处理逻辑实现】
  • Transformation 是对数据流进行操作的过程,它通过调用相应的 Function 来实现 DataStream 的转换。【侧重数据流转换和流转】
  • StreamOperator 是 Flink 内部用于实现 Transformation 逻辑的组件,它封装了数据流处理的底层细节。【运行上述流程使用】

这四个概念在Flink中共同构成了数据流处理的核心框架。用户通过 DataStream API 定义数据流处理逻辑,这些逻辑通过 Transformation 过程实现,而 Transformation 过程则通过调用相应的 Function 来完成,最终由StreamOperator在Flink内部执行。