DataStream API
富函数类(Rich Function Classes)
“富函数类” 也是 DataStream API 提供的一个函数类的接口,所有的 Flink 函数类都有其 Rich 版本
Rich Function 有生命周期的概念,典型的生命周期方法有:
open()方法,是 Rich Function 的初始化方法,也就是会开启一个算子的生命周期。当一个算子的实际工作方法例如 map()或者 filter()方法被调用之前, open()会首先被调用close()方法,是生命周期中的最后一个调用的方法,类似于解构方法。一般用来做一些清理工作
需要注意的是,这里的生命周期方法,对于一个并行子任务来说只会调用一次;
而对应的,实际工作方法,例如 RichMapFunction 中的 map(),在每条数据到来后都会触发一次调用
另外,富函数类提供了 getRuntimeContext()方法,可以获取到运行时上下文的一些信息,例如程序执行的并行度,任务名称,以及状态(state)
这使得我们可以大大扩展程序的功能,特别是对于状态的操作,使得 Flink 中的算子具备了处理复杂业务的能力
所有的 Flink 程序都可以归纳为由三部分构成 :Source、 Transformation 和 Sink
- Source 表示 “源算子”,负责读取数据源
- Transformation 表示 “转换算子”,利用各种算子进行处理加工
- Sink 表示 “下沉算子”,负责数据的输出
编码流程
- 获取执行环境
- 读取数据源
- 定义基于数据的转换操作
- 定义计算结果的输出位置
- 触发程序执行
创建执行环境
1. 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 包
);
4. 执行模式
// 批处理环境
ExecutionEnvironment batchEnv = ExecutionEnvironment.getExecutionEnvironment();
// 流处理环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Flink 程序默认为流执行模式
BATCH 模式的配置方法如下:
# 命令行配置
bin/flink run -Dexecution.runtime-mode=BATCH ...
# 代码中配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setRuntimeMode(RuntimeExecutionMode.BATCH);
触发程序执行
Flink 是由事件驱动的,只有等到数据到来,才会触发真正的计算,这也被称为“延迟执行”或“懒执行”(lazy execution)
所以我们需要显式地调用执行环境的 execute() 方法,来触发程序执行。 execute()方法将一直等待作业完成,然后返回一个执行结果(JobExecutionResult)
env.execute();
源算子
# 方法传入一个对象参数,需要实现 SourceFunction 接口
DataStream<String> stream = env.addSource(...);
# 从集合中读取数据
DataStream<Event> stream = env.fromCollection(clicks);
# 从文件读取数据
DataStream<String> stream = env.readTextFile("clicks.csv");
# 从 Socket 读取数据
DataStream<String> stream = env.socketTextStream("localhost", 7777);
从 Kafka 读取数据
-
导入依赖
<dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId> <version>${flink.version}</version> </dependency> -
调用
env.addSource(),传入FlinkKafkaConsumer的对象实例StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(1); // kafka的相关配置 Properties properties = new Properties(); properties.setProperty("bootstrap.servers", "hadoop101:9092"); //可进行更多的配置比如 //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"); // 从kafka中读取数据, 参数:(topic, 序列化方法, 相关配置) DataStreamSource<String> kafkaStream = env.addSource(new FlinkKafkaConsumer<String>("clicks", new SimpleStringSchema(), properties)); kafkaStream.print(); env.execute();
转换算子
基本转换算子
转换算子:对数据进行转换,基于当前数据去做处理和输出
map
输入一条数据,输出一条数据
public <R> SingleOutputStreamOperator<R> map(MapFunction<T, R> mapper){}
filter
数量上的变化,将部分不符合条件的数据过滤掉
flatMap
输入一条数据,输出多条数据
聚合算子
对于海量数据,进行聚合之前,肯定是需要先进行分区才能并行处理,从而提高效率,所以在Flink中,要做聚合,需要先进行分区
keyBy
keyBy 得到的结果将不再是 DataStream,而是会将 DataStream 转换为KeyedStream,按key进行逻辑分区 - “软分区”,但是一个物理分区(slot)中还是包含不同key(减小占用)
KeyedStream 是一个非常重要的数据结构,只有基于它才可以做后续的聚合操作(比如 sum, reduce);而且它可以将当前算子任务的状态(state)也按照 key 进行划分、限定为仅对当前 key 有效
keyBy 参数:比如对于 Tuple 数据类型,可以指定字段的位置或者多个位置的组合;对于 POJO 类 型,可以指定字段的名称(String);另外,还可以传入 Lambda 表达式或者实现一个键选择器 (KeySelector),用于说明从数据中提取 key 的逻辑
// 传入一个KeySelector作为参数
// max只会更新 最大的timestamp字段,其他字段都会保留,即第一条数据的相应字段
stream.keyBy(new KeySelector<Event, String>() {
@Override
public String getKey(Event event) throws Exception {
return event.user;
}
}).max("timestamp").print("max: ");
// 传入一个 Lambda 表达式 作为参数
// maxBy会更新整条数据
stream.keyBy(data -> data.user).maxBy("timestamp").print("maxby: ");
KeyedStream后可接一些简单聚合 api 如下:
sum():在输入流上,对指定的字段做叠加求和的操作min():在输入流上,对指定的字段求最小值max():在输入流上,对指定的字段求最大值minBy():与 min()类似,在输入流上针对指定字段求最小值。不同的是, min()只计算指定字段的最小值,其他字段会保留最初第一个数据的值;而 minBy()则会返回包含字段最小值的整条数据maxBy():与 max()类似,在输入流上针对指定字段求最大值。两者区别与min()/minBy()完全一致
reduce
与简单聚合类似, reduce 操作也会将 KeyedStream 转换为 DataStream。它不会改变流的元素数据类型,所以输出类型和输入类型是一样的
物理分区(Physical Partitioning)
分区算子并不对数据进行转换处理,只是定义了数据的传输方式
1. 随机分区(shuffle)
完全随机地进行分区,所以对于同样的输入数据,每次执行得到的结果也不会相同
// 经洗牌后打印输出,并行度为 4
stream.shuffle().print("shuffle").setParallelism(4);
2. 轮询分区(Round-Robin)
即 “发牌”,按照先后顺序将数据分区
// 经轮询重分区后打印输出,并行度为 4
stream.rebalance().print("rebalance").setParallelism(4);
3. 重缩放分区(rescale)
重缩放分区和轮询分区非常相似,只是它是小团体内“发牌”
由于 rebalance 是所有分区数据的 “重新平衡”,当 TaskManager 数据量较多时,这种跨节点的网络传输必然影响效率;而如果我们配置的 task slot 数量合适,用 rescale 的方式进行“局部重缩放”,就可以让数据只在当前 TaskManager 的多个 slot 之间重新分配,从而避免了网络传输带来的损耗
从底层实现上看,rebalance 和 rescale 的根本区别在于任务之间的连接机制不同
rebalance 将会针对所有上游任务(发送数据方)和所有下游任务(接收数据方)之间建立通信通道,这 是一个笛卡尔积的关系;而 rescale 仅仅针对每一个任务和下游对应的部分任务之间建立通信通道,节省了很多资源
DataStreamSource<Integer> numStream = env.addSource(new RichParallelSourceFunction<Integer>() {
@Override
public void run(SourceContext<Integer> sourceContext) throws Exception {
for (int i = 1; i < 9; i++) {
// 将奇偶数分别发送到0号和1号并行分区
if (i % 2 == getRuntimeContext().getIndexOfThisSubtask()) {
sourceContext.collect(i);
}
}
}
@Override
public void cancel() {
}
});
numStream.print("ori");
//numStream.rebalance().print("rebalance").setParallelism(4);
numStream.rescale().print("rescale").setParallelism(4);
输出结果分析:
测试分区数据如下,偶数在1分区,奇数在2分区
ori:2> 1
ori:1> 2
ori:2> 3
ori:1> 4
ori:2> 5
ori:2> 7
ori:1> 6
ori:1> 8
rebalance之后,对于先后到来的数据轮询发送到了1、2、3、4分区,
rebalance:3> 5
rebalance:2> 3
rebalance:1> 1
rebalance:4> 7
rebalance:3> 6
rebalance:1> 2
rebalance:2> 4
rebalance:4> 8
而rescale之后,偶数发送到了1、2分区,奇数发送到了3、4分区
rescale:1> 2
rescale:4> 3
rescale:2> 4
rescale:3> 1
rescale:2> 8
rescale:4> 7
rescale:1> 6
rescale:3> 5
4. 广播(broadcast)
将数据复制并发送到下游算子的所有并行任务中去
// 经广播后打印输出,并行度为 4
stream. broadcast().print("broadcast").setParallelism(4);
5. 全局分区(global)
将所有的输入流数据都发送到下游算子的第一个并行子任务中去
这就相当于强行让下游任务并行度变成了 1,所以使用这个操作需要非常谨慎,可能对程序造成很大的压力
stream.global().print().setParallelism(4);
6. 自定义分区(Custom)
我们可以调用partitionCustom()方法来自定义分区策略
在调用时,方法需要传入两个参数,第一个是自定义分区器(Partitioner)对象,第二个 是应用分区器的字段,它的指定方式与 keyBy 指定 key 基本一样:可以通过字段名称指定, 也可以通过字段位置索引来指定,还可以实现一个 KeySelector
env.fromElements(1, 2, 3, 4, 5, 6, 7, 8).partitionCustom(new Partitioner<Integer>() {
@Override
public int partition(Integer integer, int i) {
return integer % 2;
}
}, new KeySelector<Integer, Integer>() {
@Override
public Integer getKey(Integer value) throws Exception {
return value;
}
}).print().setParallelism(4);
输出算子
stream.addSink(new SinkFunction(…));
比如输出到kafka中,可结合添加kafka部分内容查看:
FlinkKafkaProducer 继承了抽象类 TwoPhaseCommitSinkFunction,这是一个实现了“两阶段提交”的 RichSinkFunction。两阶段提交提供了 Flink 向 Kafka 写入数据的事务性保证,能够真正做到精确一次(exactly once)的状态一致性
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(4);
//1. 从kafka中读取数据
Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "hadoop101:9092");
DataStreamSource<String> kafkaStream = env.addSource(new FlinkKafkaConsumer<String>("clicks", new SimpleStringSchema(), properties));
//2. 利用flink进行转换处理
SingleOutputStreamOperator<String> result = kafkaStream.map(new MapFunction<String, String>() {
@Override
public String map(String s) throws Exception {
String[] fields = s.split(",");
return new Event(fields[0].trim(), fields[1].trim(), Long.valueOf(fields[2].trim())).toString();
}
});
//3. 结果写入kafka,参数:(brokerList, topic, 序列化方法)
result.addSink(new FlinkKafkaProducer<String>("hadoop101:9092","events",new SimpleStringSchema()));
env.execute();
}

浙公网安备 33010602011771号