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 读取数据

  1. 导入依赖

    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-kafka_${scala.binary.version}</artifactId>
        <version>${flink.version}</version>
    </dependency>
    
  2. 调用 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();
	}
posted @ 2022-11-03 23:45  黄一洋  阅读(34)  评论(0)    收藏  举报