【Flink】DataStream API—执行环境、源算子、转换算子、输出算子

目录

前言

一、执行环境

1、创建执行环境

2、执行模式(Execution Mode)

3、触发执行

二、源算子(Source)

1、读取数据的算子就是源算子。

 2、源算子种类

3、Flink 支持的数据类型

三、转换算子(Transformation)

1、基本转换算子

2、聚合算子(Aggregation)

3、匿名函数(Lambda) 

4、富函数类(Rich Function Classes)

5、物理分区

四、输出算子(Sink)

1、连接到外部系统

2、输出到文件

3、输出到 Kafka

4、输出到 Redis

5、输出到 Elasticsearch

6、输出到 MySQL(JDBC)

7、自定义 Sink 输出


前言

Flink 有非常灵活的分层 API 设计,其中的核心层就是 DataStream/DataSet API 。由于新版
本已经实现了流批一体, DataSet API 将被弃用,官方推荐统一使用 DataStream API 处理流数
据和批数据。
DataStream (数据流)本身是 Flink 中一个用来表示数据集合的类( Class)。一个 Flink 程序,其实就是对 DataStream 的各种转换。 一个Flink程序的构成:
                        
  • 获取执行环境(execution environment
  • 读取数据源(source
  • 定义基于数据的转换操作(transformations
  • 定义计算结果的输出位置(sink
  • 触发程序执行(execute 

一、执行环境

1、创建执行环境

(1)getExecutionEnvironment
最简单的方式,就是直接调用 getExecutionEnvironment 方法。它会根据当前运行的上下文
直接得到正确的结果:如果程序是独立运行的,就返回一个本地执行环境;如果是创建了 jar
包,然后从命令行调用它并提交到集群执行,那么就返回集群的执行环境。
StreamExecutionEnvironment env = 
StreamExecutionEnvironment.getExecutionEnvironment();
(2)createLocalEnvironment
这个方法返回一个本地执行环境。可以在调用时传入一个参数,指定默认的并行度;如果
不传入,则默认并行度就是本地的 CPU 核心数。
StreamExecutionEnvironment localEnv = 
StreamExecutionEnvironment.createLocalEnvironment();
(3)createRemoteEnvironment
这个方法返回集群执行环境。需要在调用时指定 JobManager 的主机名和端口号,并指定
要在集群中运行的 Jar 包。
 
StreamExecutionEnvironment remoteEnv = StreamExecutionEnvironment
 .createRemoteEnvironment(
     "host", // JobManager 主机名
     1234, // JobManager 进程端口号
     "path/to/jarFile.jar" // 提交给 JobManager 的 JAR 包
);

2、执行模式(Execution Mode)

        从 1.12.0 版本起, Flink 实现了 API 上的流批统一。 DataStream API 新增了一个重要特
性:可以支持不同的“执行模式”( execution mode ),通过简单的设置就可以让一段 Flink 程序
在流处理和批处理之间切换。这样一来, DataSet API 也就没有存在的必要了。
  • 流执行模式STREAMING
  • 批执行模式BATCH
  • 自动模式AUTOMATIC

(1)设置方式

  • 通过命令行配置
bin/flink run -Dexecution.runtime-mode=BATCH ...
  • 通过代码配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
建议 : 不要在代码中配置,而是使用命令行。这同设置并行度是类似的:在提交作业时指
定参数可以更加灵活,同一段应用程序写好之后,既可以用于批处理也可以用于流处理。而在 代码中硬编码(hard code )的方式可扩展性比较差,一般都不推荐。

3、触发执行

Flink 是由事件驱动的,只有等到数据到来了才会触发真正的计算, 这也被称为“延迟执行”或“懒执行”(lazy execution )。 所以我们需要显式地调用执行环境的 execute() 方法来触发程序执行。 execute() 方法将一直等待作业完成,然后返回一个执行结果(JobExecutionResult )。

二、源算子(Source

1、读取数据的算子就是源算子。

Flink 代码中通用的添加 source 的方式,是调用执行环境的 addSource() 方法:
DataStream<String> stream = env.addSource(...);
该方法传入一个对象参数,需要实现 SourceFunction 接口;返回 DataStreamSource 。这里的
DataStreamSource 类继承自 SingleOutputStreamOperator 类,又进一步继承自 DataStream 。所以 很明显,读取数据的 source 操作是一个算子,得到的是一个数据流( DataStream

POJO:一个简单的Java类,这个类没有实现/继承任何特殊的java接口或者类,不遵循任何主要java模型,约定或者框架的java对象。在理想情况下,POJO不应该有注解。方便数据的解析和序列化。

 2、源算子种类

(1)从集合中读取数据

// 构建集合
ArrayList<Event> clicks = new ArrayList<>();
clicks.add(new Event("Mary","./home",1000L));
clicks.add(new Event("Bob","./cart",2000L));
DataStream<Event> stream = env.fromCollection(clicks);
// 不构建集合,直接列出元素
DataStreamSource<Event> stream2 = env.fromElements(
 new Event("Mary", "./home", 1000L),
 new Event("Bob", "./cart", 2000L)
);
(2)从文件读取数据
一个比较常见的方式就是读取日志文件。这也是批处理中最常见的读取方式:
DataStream<String> stream = env.readTextFile("clicks.csv");

 (3)Socket 读取数据

DataStream<String> stream = env.socketTextStream("localhost", 7777);

(4)Kafka 读取数据

Kafka Flink 天生一对,是当前处理流式数据的双子星。在如今的实时流处理应用中,由 Kafka 进行数据的收集和传输, Flink 进行分析计算,这样的架构已经成为众多企业的首选。
  • 引入 Kafka 连接器的依赖
<dependency>
 <groupId>org.apache.flink</groupId>
 <artifactId>flink-connector-kafka_2.12</artifactId>
 <version>1.13.0</version>
</dependency>
  • 然后调用 env.addSource() ,传入 FlinkKafkaConsumer 的对象实例
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
 env.setParallelism(1);
 Properties properties = new Properties();
 properties.setProperty("bootstrap.servers", "hadoop102:9092");
 properties.setProperty("group.id", "consumer-group");
 properties.setProperty("key.deserializer", 
"org.apache.kafka.common.serialization.StringDeserializer");
 properties.setProperty("value.deserializer", 
"org.apache.kafka.common.serialization.StringDeserializer");
 properties.setProperty("auto.offset.reset", "latest");
 DataStreamSource<String> stream = env.addSource(new FlinkKafkaConsumer<String>(
     "clicks",
     new SimpleStringSchema(),
     properties
 ));

(5)自定义 Source

创建一个自定义的数据源,实现 SourceFunction 接口。
主要重写两个关键方法: run()和 cancel()
  • run()方法:使用运行时上下文对象(SourceContext)向下游发送数据;
  • cancel()方法:通过标识位控制退出循环,来达到中断数据源的效果。

3、Flink 支持的数据类型

(1)类型系统

TypeInformation 类是 Flink 中所有类型描述符的基类。 它涵盖了类型的一些基本属性,并为每个数据类型生成特定的序列化器、反序列化器和比较器。

(2)基本类型

所有 Java 基本类型及其包装类,再加上 Void String Date BigDecimal BigInteger
(3)数组类型
包括基本类型数组( PRIMITIVE_ARRAY )和对象数组 (OBJECT_ARRAY)
(4)复合数据类型
  • Java 元组类型(TUPLE):这是 Flink 内置的元组类型,是 Java API 的一部分。最多
25 个字段,也就是从 Tuple0~Tuple25 ,不支持空字段
  • Scala 样例类及 Scala 元组:不支持空字段
  • 行类型(ROW):可以认为是具有任意个字段的元组,并支持空字段
  • POJOFlink 自定义的类似于 Java bean 模式的类

 (5)辅助类型

Option、EitherListMap

(6)泛型类型(GENERIC
(7)类型提示Type Hints
        Flink 还具有一个类型提取系统,可以分析函数的输入和返回类型,自动获取类型信息,
从而获得对应的序列化器和反序列化器。但是,由于 Java 中泛型擦除的存在,在某些特殊情
况下(比如 Lambda 表达式中),自动提取的信息是不够精细的。
        在 word count 流处理程序,我们在将 String 类型的每个词转换成(word ,count)二元组后,就明确地用 returns 指定了返回的类型。因为对于 map 里传入的 Lambda 表达式,系统只能推断出返回的是 Tuple2 类型,而无法得到 Tuple2<String, Long> 。只有显式地告诉系统当前的返回类型,才能正确地解析出完整数据。
.map(word -> Tuple2.of(word, 1L))
.returns(Types.TUPLE(Types.STRING, Types.LONG));

三、转换算子(Transformation

1、基本转换算子

(1)映射(map) :就是一个“一一映射”,消费一个元素就产出一个元素

// 传入匿名类,实现 MapFunction
 stream.map(new MapFunction<Event, String>() {
     @Override
     public String map(Event e) throws Exception {
         return e.user;
     }
 });

// 传入 MapFunction 的实现类
 stream.map(new UserExtractor()).print();

(2)过滤:filter 转换操作,顾名思义是对数据流执行一个过滤,通过一个布尔条件表达式设置过滤条件,对于每一个流内元素进行判断,若为 true 则元素正常输出,若为 false 则元素被过滤掉。

// 传入匿名类实现 FilterFunction
 stream.filter(new FilterFunction<Event>() {
     @Override
     public boolean filter(Event e) throws Exception {
         return e.user.equals("Mary");
     }
 });

(3)扁平映射

flatMap 操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个一个的个体使用。消费一个元素,可以产生 0 到多个元素。flatMap 可以认为是“扁平化”(flatten) 和“映射”(map)两步操作的结合,也就是先按照某种规则对数据进行打散拆分,再对拆分后的元素做转换处理。 flatMap 并没有直接定义返回值类型,而是通过一个(Collector)来 指定输出。

同 map 一样,flatMap 也可以使用 Lambda 表达式或者 FlatMapFunction 接口实现类的方式来进行传参,返回值类型取决于所传参数的具体逻辑,可以与原数据流相同,也可以不同。

2、聚合算子(Aggregation

(1)按键分区(keyBy):在 Flink 中,需要先进行分区,再做聚合; 这个操作就是通过 keyBy 来完成的。keyBy 是聚合前必须要用到的一个算子。keyBy 通过指定键(key),可以将一条流从逻辑上划分成不同的分区(partitions)。这里所说的分区,其实就是并行处理的子任务,也就对应 着任务槽(task slot)。

// 使用 Lambda 表达式
 KeyedStream<Event, String> keyedStream = stream.keyBy(e -> e.user);

// 使用匿名类实现 KeySelector
 KeyedStream<Event, String> keyedStream1 = stream.keyBy(new KeySelector<Event, String>() {
     @Override
     public String getKey(Event e) throws Exception {
         return e.user;
     }
 });

(2)简单聚合

 stream.keyBy(r -> r.f0).sum(1).print();
 stream.keyBy(r -> r.f0).sum("f1").print();

 stream.keyBy(r -> r.f0).max(1).print();
 stream.keyBy(r -> r.f0).max("f1").print();

 stream.keyBy(r -> r.f0).min(1).print();
 stream.keyBy(r -> r.f0).min("f1").print();

 stream.keyBy(r -> r.f0).maxBy(1).print();
 stream.keyBy(r -> r.f0).maxBy("f1").print();

 stream.keyBy(r -> r.f0).minBy(1).print();
 stream.keyBy(r -> r.f0).minBy("f1").print();

// 如果数据流的类型是 POJO 类,那么就只能通过字段名称来指定,不能通过位置来指定了。

 (3)归约聚合(reduce

调用 KeyedStream reduce 方法时,需要传入一个参数,实现 ReduceFunction 接口。
与简单聚合类似, reduce 操作也会将 KeyedStream 转换为 DataStream 。它不会改变流的元
素数据类型,所以输出类型和输入类型是一样的。
对于一组数据,我们可以先取两个进行合并,然后再将合并的结果看作一个数据、再跟后面的数据合并,最终会将它“简化”成唯一的一个数据
public interface ReduceFunction<T> extends Function, Serializable {
    T reduce(T value1, T value2) throws Exception;
}

3、匿名函数(Lambda 

Flink 的所有算子都可以使用 Lambda 表达式的方式来进行编码,但是,当 Lambda
达式使用 Java 的泛型时,我们需要显式的声明类型信息。
//map 函数使用 Lambda 表达式,返回简单类型,不需要进行类型声明
 DataStream<String> stream1 = clicks.map(event -> event.url);

// flatMap 使用 Lambda 表达式,必须通过 returns 明确声明返回类型
DataStream<String> stream2 = clicks.flatMap((Event event, Collector<String> 
out) -> {
    out.collect(event.url);
}).returns(Types.STRING);

// 使用显式的 ".returns(...)"
 DataStream<Tuple2<String, Long>> stream3 = clicks
 .map( event -> Tuple2.of(event.user, 1L) )
 .returns(Types.TUPLE(Types.STRING, Types.LONG));
 stream3.print();

4、富函数类(Rich Function Classes

        与常规函数类的不 同主要在于,富函数类可以获取运行环境的上下文,并拥有一些生命周期方法,所以可以实现更复杂的功能。
Rich Function 有生命周期的概念。典型的生命周期方法有:
  • open()方法,是 Rich Function 的初始化方法,也就是会开启一个算子的生命周期。当一个算子的实际工作方法例如 map()或者 filter()方法被调用之前,open()会首先被调用。所以像文件 IO 的创建,数据库连接的创建,配置文件的读取等等这样一次性的工作,都适合在 open()方法中完成。
  • close()方法,是生命周期中的最后一个调用的方法,类似于解构方法。一般用来做一些清理工作。需要注意的是,这里的生命周期方法,对于一个并行子任务来说只会调用一次;而对应的,实际工作方法,例如 RichMapFunction 中的 map(),在每条数据到来后都会触发一次调用。
另外,富函数类提供了 getRuntimeContext() 方法可以获取到运行时上下文的一些信息,例如程序执行的并行度,任务名称,以及状态(state )。这使得我们可以大大扩展程序的功能,特别是对于状态的操作,使得 Flink 中的算子具备了处理复杂业务的能力。
一个常见的应用场景就是,如果我们希望连接到一个外部数据库进行读写操作,那么将连
接操作放在 map() 中显然不是个好选择——因为每来一条数据就会重新连接一次数据库;所以 我们可以在 open() 中建立连接,在 map()中读写数据,而在 close() 中关闭连接
public class MyFlatMap extends RichFlatMapFunction<IN, OUT>> {
 @Override
 public void open(Configuration configuration) {
        // 做一些初始化工作
         // 例如建立一个和 MySQL 的连接
 }

 @Override
 public void flatMap(IN in, Collector<OUT out) {
         // 对数据库进行读写
 }

 @Override
 public void close() {
     // 清理工作,关闭和 MySQL 数据库的连接。
 } 

}

5、物理分区

keyBy 是一种逻辑分区 logical partitioning)操作。物理分区与 keyBy 另一大区别在于,keyBy 之后得到的是一个 KeyedStream,而物理分区之后结果仍是 DataStream,且流中元素数据类型保持不变。常见的物理分区策略有随机分配(Random)、轮询分配(Round-Robin)、重缩放(Rescale)和广播(Broadcast)。
(1) 随机分区( shuffle
最简单的重分区方式就是直接“洗牌”。通过调用 DataStream .shuffle()方法,将数据随机地分配到下游算子的并行任务中去。因为是完全随机的,所以对于同样的输入数据, 每次执行得到的结果也不会相同。

// 经洗牌后打印输出,并行度为 4
 stream.shuffle().print("shuffle").setParallelism(4);
(2) 轮询分区( Round-Robin
轮询也是一种常见的重分区方式。简单来说就是“发牌”,按照先后顺序将数据做依次分发。通过调用 DataStream .rebalance() 方法,就可以实现轮询重分区。 rebalance 使用的是 Round-Robin 负载均衡算法,可以将输入流数据平均分配到下游的并行任务中去。

// 经轮询重分区后打印输出,并行度为 4
 stream.rebalance().print("rebalance").setParallelism(4);
(3) 重缩放分区( rescale
重缩放分区和轮询分区非常相似。当调用 rescale() 方法时,其实底层也是使用 Round-Robin 算法进行轮询,但是只会将数据轮询发送到下游并行任务的一部分中,如图 5-11 所示。也就是说,“发牌人”如果有多个,那么 rebalance 的方式是每个发牌人都面向所有人发牌;而 rescale 的做法是分成小团体,发牌人只给自己团体内的所有人轮流发牌

从底层实现上看, rebalance rescale 的根本区别在于任务之间的连接机制不同。 rebalance
将会针对所有上游任务(发送数据方)和所有下游任务(接收数据方)之间建立通信通道,这
是一个笛卡尔积的关系;而 rescale 仅仅针对每一个任务和下游对应的部分任务之间建立通信
通道,节省了很多资源。
(4)广播( broadcast
这种方式其实不应该叫做“重分区”,因为经过广播之后,数据会在不同的分区都保留一份,可能进行重复处理。可以通过调用 DataStream broadcast() 方法,将输入数据复制并发送到下游算子的所有并行任务中去
(5)全局分区( global
全局分区也是一种特殊的分区方式。这种做法非常极端,通过调用 .global()方法,会将所有的输入流数据都发送到下游算子的第一个并行子任务中去。这就相当于强行让下游任务并行度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力。
(6) 自定义分区( Custom
在调用时,方法需要传入两个参数,第一个是自定义分区器( Partitioner )对象,第二个
应用分区器的字段,它的指定方式与 keyBy 指定 key 基本一样:可以通过字段名称指定,
也可以通过字段位置索引来指定,还可以实现一个 KeySelector
// 将自然数按照奇偶分区
 env.fromElements(1, 2, 3, 4, 5, 6, 7, 8)
 .partitionCustom(new Partitioner<Integer>() {
     @Override
     public int partition(Integer key, int numPartitions) {
         return key % 2;
     }
     }, new KeySelector<Integer, Integer>() {
     @Override
     public Integer getKey(Integer value) throws Exception {
         return value;
     }
     })
 .print().setParallelism(2);

四、输出算子(Sink

Flink 程序中所有对外的输出操作,一般都是利用 Sink 算子完成的。Sink 在 Flink 中代表了将结果数据收集起来、输出到外部的意思, print 方法其实就是一种 Sink,它表示将数据流写入标准控制台打印输出。
stream.addSink(new SinkFunction(…));
addSink 方法同样需要传入一个参数,实现的是 SinkFunction 接口。在这个接口中只需要重写一个方法 invoke(), 用来将指定的值写入到外部系统中。这个方法在每条数据记录到来时都会调用:

1、连接到外部系统

        像 Kafka 之类流式系统, Flink 提供了完美对接, source/sink 两端都能连接,可读可写;而对于 Elasticsearch 、文件系统( FileSystem )、 JDBC 等数据存储系统,则只提供了输出写入的 sink 连接器。

2、输出到文件

Flink 为此专门提供了一个流式文件系统的连接器: StreamingFileSink ,它继承自抽象类 RichSinkFunction,而且集成了 Flink 的检查点( checkpoint )机制,用来保证精确一次( exactly
once)的一致性语义。StreamingFileSink 为批处理和流处理提供了一个统一的 Sink,它可以将分区文件写入 Flink支持的文件系统。它可以保证精确一次的状态一致性,大大改进了之前流式文件 Sink 的方式。
它的主要操作是将数据写入桶( buckets ,每个桶中的数据都可以分割成一个个大小有限的分
区文件,这样一来就实现真正意义上的分布式文件存储。我们可以通过各种配置来控制“分桶”
的操作;默认的分桶方式是基于时间的,我们每小时写入一个新的桶。换句话说,每个桶内保
存的文件,记录的都是 1 小时的输出数据。

3、输出到 Kafka

 Flink 与 Kafka 的连接器提供了端到端的精确一次(exactly once)语义保证,这在实际项目中是最高级别的一致性保证。具体步骤如下:

(1)添加 Kafka 连接器依赖

(2)启动 Kafka 集群

(3)编写输出到 Kafka 的示例代码
public static void main(String[] args) throws Exception {
     StreamExecutionEnvironment env = 
     StreamExecutionEnvironment.getExecutionEnvironment();
     env.setParallelism(1);

     Properties properties = new Properties();
     properties.put("bootstrap.servers", "hadoop102:9092");
     DataStreamSource<String> stream = env.readTextFile("input/clicks.csv");
     stream
         .addSink(new FlinkKafkaProducer<String>(
             "clicks",
             new SimpleStringSchema(),
             properties
     ));

     env.execute();
 }
FlinkKafkaProducer 继承了抽象类 TwoPhaseCommitSinkFunction,这是一个实现了 两阶段提交 RichSinkFunction 。两阶段提交提供了 Flink Kafka 写入数据的事务性保证,能够真正做到精确一次( exactly once )的状态一致性。
数据管道:Flink 从 Kakfa 的一个 topic 读取消费数据,然后进行处理转换,最终将结果数据写入 Kafka 的另一个 topic——数据从 Kafka 流入、经 Flink处理后又流回到 Kafka 去,这就是所谓的“数据管道”应用。

4、输出到 Redis

Redis 是一个开源的内存式的数据存储,提供了像字符串( string )、哈希表( hash )、列表
list )、集合( set )、排序集合( sorted set )、位图( bitmap )、地理索引和流( stream )等一系 列常用的数据结构。因为它运行速度快、支持的数据类型丰富,在实际项目中已经成为了架构
优化必不可少的一员,一般用作数据库、缓存,也可以作为消息代理。

5、输出到 Elasticsearch

ElasticSearch 是一个分布式的开源搜索和分析引擎,适用于所有类型的数据。ElasticSearch 有着简洁的 REST 风格的 API,以良好的分布式特性、速度和可扩展性而闻名,在大数据领域 应用非常广泛。

6、输出到 MySQLJDBC

尽管在大数据处理中直接与 MySQL 交互的场景不多,但最终处理的计算结果是要给外部应用消费使用的,而外部应用读取的数据存储往往就是 MySQL 。所以我们也需要知道如何将数据输出到 MySQL 这样的传统数据库。
1)添加依赖
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-jdbc_${scala.binary.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
(2)启动 MySQL,在 database 库下建表 clicks
mysql> create table clicks(
-> user varchar(20) not null,
-> url varchar(100) not null);
(3)编写输出到 MySQL 的代码
stream.addSink(JdbcSink.sink(
                "INSERT INTO clicks (user, url) VALUES (?, ?)",
                (statement, r) -> {
                    statement.setString(1, r.user);
                    statement.setString(2, r.url);
                },
                new JdbcConnectionOptions.JdbcConnectionOptionsBuilder()   
                // MySQL5.7的写法
                        .withUrl("jdbc:mysql://localhost:3306/ct_2022")
                        .withDriverName("com.mysql.jdbc.Driver")
                        .withUsername("root")
                        .withPassword("root")
                        .build()
            )
 );

7、自定义 Sink 输出

例如, Flink 并没有提供 HBase 的连接器,所以需要我们自己写。
1)导入依赖
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>${hbase.version}</version>
</dependency>
(2)编写输出到 HBase 的代码
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值