Flink Transformation API
(二)DataStream API
DataStream是Flink编写流处理作业的API。我们前面说过一个完整的Flink处理程序应该包含三部分:数据源(Source)、转换操作(Transformation)、结果接收(Sink)。下面我们从这三部分来看DataStream API。
(四)数据转换(Transformation)
数据处理的核心就是对数据进行各种转化操作,在Flink上就是通过转换将一个或多个DataStream转换成新的DataStream。
为了更好的理解transformation函数,下面给出匿名类的方式来实现各个函数。
所有转换函数都是依赖以下基础:
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStream<String> dataStream = env.readTextFile("/opt/yjz/flink/flink-1.7.2/README.txt");
Aggregations
1.介绍
Aggregations 为聚合函数的总称,常见的聚合函数包括但不限于 sum、max、min 等。
事实上,对于 Aggregations 函数,Flink 帮助我们封装了状态数据,这些状态数据不会被清理,所以在实际生产环境中应该尽量避免在一个无限流上使用 Aggregations。而且,对于同一个 keyedStream ,只能调用一次 Aggregation 函数。
不建议的是那些状态无限增长的聚合,实际应用中一般会配合窗口使用。使得状态不会无限制扩张。
2.例子
滚动聚合具有相同key的数据流元素,我们可以指定需要聚合的字段(field)。DataStream<T>中的T为聚合之后的结果。
//对KeyedStream中元素的第一个Filed求和 DataStream<String> dataStream1 = keyedStream.sum(0); //对KeyedStream中元素的“count”字段求和 keyedStream.sum("count"); //获取keyedStream中第一个字段的最小值 keyedStream.min(0); //获取keyedStream中count字段的最小值的元素 keyedStream.minBy("count"); keyedStream.max("count"); keyedStream.maxBy(0);
min和minBy的区别是:min返回指定字段的最小值,而minBy返回最小值所在的元素。
AggregateOperator<Tuple2<String, Long>> sum = wordAndOneUG.sum(1);
SingleOutputStreamOperator<Tuple2<String, Integer>> sumDS = wordAndOneKS.sum(1);
转换前后数据类型:KeyedStream->DataStream。
broadcast
使用broadcast可以向每个分区广播元素。
dataStream.broadcast();
转换前后的数据类型:DataStream->DataStream。
CoFlatMap/CoMap
可以对连接流执行类似map和flatMap操作。
connectedStreams.map(new CoMapFunction<String, String, String>() { @Override public String map1(String value) throws Exception { return value.toUpperCase(); } @Override public String map2(String value) throws Exception { return value.toLowerCase(); } });
转换前后的数据类型:ConnectedDataStreams->DataStream。
Connect
连接(connect)两个流,并且保留其类型。两个数据流之间可以共享状态。ConnectedStreams<IN1,IN2>中IN1代表第一个数据流中的数据类型,IN2代表第二个数据流中的数据类型。
ConnectedStreams<String,String> connectedStreams = dataStream.connect(dataStream);
转换前后的数据类型:DataStream,DataStream->ConnectedDataStreams。
Extract Timestamps(Deprecated)
从记录中提取时间戳,以便使用事件时间语义窗口。之后会专门来看Flink的Event Time。
dataStream.assignTimestamps(new TimestampExtractor<String>() { @Override public long extractTimestamp(String element, long currentTimestamp) { return 0; } @Override public long extractWatermark(String element, long currentTimestamp) { return 0; } @Override public long getCurrentWatermark() { return 0; } });
该方法以被标记为废弃!
转换前后的数据类型:DataStream->DataStream。
Filter
1.介绍
顾名思义,Fliter 的意思就是过滤掉不需要的数据,每个元素都会被 filter 函数处理,如果 filter 函数返回 true 则保留,否则丢弃。
2.例子
过滤指定元素数据,如果返回true则该元素继续向下传递,如果为false则将该元素过滤掉。FilterFunction<T>中T代表输入元素的数据类型。
dataStream.filter(new FilterFunction<String>() { @Override public boolean filter(String line) throws Exception { if(line.contains("flink")) return true; else return false; } });
转换前后数据类型:DataStream->DataStream。
FlatMap
1.介绍
FlatMap 接受一个元素,返回零到多个元素。接受一个元素作为输入,并且根据开发者自定义的逻辑处理后输出,但是当返回值是列表的时候,FlatMap 会将列表“平铺”,也就是以单个元素的形式进行输出。
2.例子
输入一个元素,输出0个、1个或多个元素。FlatMapFunction<T,V>中T代表输入元素数据类型(flatMap方法的第一个参数类型),V代表输出集合中元素类型(flatMap中的Collector类型参数)
SingleOutputStreamOperator<Tuple2<String, Integer>> wordAndOneDS = lineDS .flatMap(new FlatMapFunction<String, Tuple2<String, Integer>>() { @Override public void flatMap(String value, Collector<Tuple2<String, Integer>> out) throws Exception { // 按照 空格 切分 String[] words = value.split(" "); for (String word : words) { // 转换成 二元组 (word,1) Tuple2<String, Integer> wordsAndOne = Tuple2.of(word, 1); // 通过 采集器 向下游发送数据 out.collect(wordsAndOne); } } });
转换前后数据类型:DataStream->DataStream。
3.return
... .flatMap( (String value, Collector<String> out) -> { String[] words = value.split(" "); for (String word : words) { out.collect(word); } } ) .returns(Types.STRING) ...
Fold(Deprecated)
Fold功能和Reduce类似,但是Fold提供了初始值,从初始值开始滚动对相同的key记录进行滚动合并。FoldFunction<T,V>中的T为KeyStream中元素数据类型,V为初始值类型和fold方法返回值类型。
keyedStream.fold(0, new FoldFunction<String, Integer>() { @Override public Integer fold(Integer accumulator, String value) throws Exception { return accumulator + value.split(" ").length; } });
该方法已经标记为废弃!
转换前后数据类型:KeyedStream->DataStream。
Interval Join
对指定的时间间隔内使用公共key来连接两个KeyedStream。ProcessJoinFunction<IN1,IN2,OUT>中IN1为第一个DataStream中元素数据类型,IN2为第二个DataStream中的元素数据类型,OUT为结果输出类型。
keyedStream .intervalJoin(keyedStream) .between(Time.milliseconds(-2),Time.milliseconds(2))//间隔时间 .lowerBoundExclusive()//并不包含下限时间 .upperBoundExclusive() .process(new ProcessJoinFunction<String, String, String>() { @Override public void processElement(String left, String right, Context ctx, Collector<String> out) throws Exception { //... } });
Iterate
可以使用iterate方法来获取IterativeStream。
IterativeStream<String> iterativeStream = dataStream.iterate();
转换前后的数据类型:DataStream->IterativeStream。
KeyBy
1.介绍
在介绍 KeyBy 函数之前,需要你理解一个概念:KeyedStream。 在实际业务中,我们经常会需要根据数据的某种属性或者单纯某个字段进行分组,然后对不同的组进行不同的处理。举个例子,当我们需要描述一个用户画像时,则需要根据用户的不同行为事件进行加权;再比如,我们在监控双十一的交易大盘时,则需要按照商品的品类进行分组,分别计算销售额。
我们在使用 KeyBy 函数时会把 DataStream 转换成为 KeyedStream,事实上 KeyedStream 继承了 DataStream,KeyedStream 中的元素会根据用户传入的参数进行分组。
在生产环境中使用 KeyBy 函数时要十分注意!该函数会把数据按照用户指定的 key 进行分组,那么相同分组的数据会被分发到一个 subtask 上进行处理,在大数据量和 key 分布不均匀的时非常容易出现数据倾斜和反压,导致任务失败。
常见的解决方式是把所有数据加上随机前后缀,这个需要进行深入讲解。
groupBy用于批计算(DataSet),keyBy用于流计算(DataStream)
2.例子
逻辑上将数据流元素进行分区,具有相同key的记录将被划分到同一分区。指定Key的方式有多种,这个我们在之前说过了。返回类型KeyedStream<T,KEY>中T代表KeyedStream中元素数据类型,KEY代表虚拟KEY的数据类型。
KeyedStream<String,Tuple> keyedStream = dataStream.keyBy(0);
以下情况的元素不能作为key使用:
- POJO类型,但没有重写hashCode(),而是依赖Object.hashCode()。
- 该元素是数组类型。
KeyedStream<Tuple2<String, Integer>, String> wordAndOneKS = wordAndOneDS.keyBy( new KeySelector<Tuple2<String, Integer>, String>() { @Override public String getKey(Tuple2<String, Integer> value) throws Exception { return value.f0; } } );
keyBy内部使用散列来实现的。
转换前后数据类型:DataStream->KeyedStream。
Map
1.介绍
Map 接受一个元素作为输入,并且根据开发者自定义的逻辑处理后输出。
2.例子
接受一个元素,输出一个元素。MapFunction<T,V>中T代表输入数据类型(map方法的参数类型),V代表操作结果输出类型(map方法返回数据类型)。
dataStream.map(new MapFunction<String, String>() { @Override public String map(String line) throws Exception { return line.toUpperCase(); } });
3.return
在一般情况下,Java会擦除泛型类型信息。 Flink尝试使用Java保留的少量位(主要是函数签名和子类信息)通过反射重建尽可能多的类型信息。对于函数的返回类型取决于其输入类型的情况,此逻辑还包含一些简单类型推断:
在Flink无法重建已擦除的泛型类型信息的情况下,Java API提供所谓的类型提示。类型提示告诉系统函数生成的数据流或数据集的类型:
... .map(word -> Tuple2.of(word, 1)) .returns(Types.TUPLE(Types.STRING,Types.INT)) ...
min & minBy
min和minBy之间的区别是min返回最小值,而minBy返回在此字段中具有最小值的元素(与max和maxBy相同)。
但是事实上,min与max 也会返回整个元素。
不同的是min会根据指定的字段取最小值,并且把这个值保存在对应的位置上,对于其他的字段取了最先获取的值,不能保证每个元素的数值正确,max同理。
而minBy会返回指定字段取最小值的元素,并且会覆盖指定字段小于当前已找到的最小值元素。maxBy同理。
DataStreamSource<Event> stream = env.fromElements( new Event("Mary", "./home", 1000L), new Event("Mary", "./cart", 2000L) ); stream.keyBy(e -> e.user).max("timestamp").print("max"); stream.keyBy(e -> e.user).maxBy("timestamp").print("maxBy");
max> Event{user='Mary', url='./home', timestamp=1970-01-01 08:00:01.0}
maxBy> Event{user='Mary', url='./home', timestamp=1970-01-01 08:00:01.0}
max> Event{user='Mary', url='./home', timestamp=1970-01-01 08:00:02.0}
maxBy> Event{user='Mary', url='./cart', timestamp=1970-01-01 08:00:02.0}
partitionCustom
自定义分区。使用用户自定义的分区函数对指定key进行分区,partitionCustom只支持单分区。
dataStream.partitionCustom(new Partitioner<String>() { @Override public int partition(String key, int numPartitions) { return key.hashCode() % numPartitions; } },1);
转换前后的数据类型:DataStream->DataStream。
Project
对元组类型的DataStream可以使用Project选取子元组。
DataStream<Tuple2<String,Integer>> dataStream2 = dataStream.project(0,2);
转换前后的数据类型:DataStream->DataStream。
rebalance
以轮询的方式为每个分区均衡分配元素,对于优化数据倾斜该方法非常有效。
dataStream.rebalance();
转换前后的数据类型:DataStream->DataStream。
Reduce
1.介绍
Reduce 函数的原理是,会在每一个分组的 keyedStream 上生效,它会按照用户自定义的聚合逻辑进行分组聚合。
2.例子
对指定的“虚拟”key相同的记录进行滚动合并,也就是最后一次的reduce结果与当前元素进行reduce操作。ReduceFunction<T>中的T代表KeyStream中元素的数据类型。
对第一个参数,即最后一次的reduce结果,进行改变;同时返回值也是最后一次的reduce结果,即第一个参数。
...
.keyBy(r -> r.f0) // 使用用户名来进行分流 .reduce(new ReduceFunction<Tuple2<String, Long>>() { @Override public Tuple2<String, Long> reduce(Tuple2<String, Long> value1, Tuple2<String, Long> value2) throws Exception { return Tuple2.of(value1.f0, value1.f1 + value2.f1); } })
...
rescale
根据上下游task数进行分区。
dataStream.rescale();
转换前后的数据类型:DataStream->DataStream。
Select
我们可以对SplitStream分开的流进行选择,可以将其转换成一个或多个DataStream。
splitStream.select("flink");
splitStream.select("other");
shuffle
随机分区。均匀随机将元素进行分区。
dataStream.shuffle();
Split(Deprecated)
我们可以根据某些规则将数据流切分成两个或多个数据流。
dataStream.split(new OutputSelector<String>() { @Override public Iterable<String> select(String value) { List<String> outList = new ArrayList<>(); if(value.contains("flink")) outList.add("flink"); else outList.add("other"); return outList; } });
该方法底层引用以被标记为废弃!
转换前后的数据类型:DataStream->SplitStream
Union
联合(Union)两个或多个DataStream,所有DataStream中的元素都会组合成一个新的DataStream。如果联合自身,则每个元素在新的DataStream出现两次。
dataStream.union(dataStream1);
转换前后的数据类型:DataStream*->DataStream。
window
对已经分区的KeyedStream上定义窗口,Window会根据某些规则(比如在最后5s到达的数据)对虚拟“key”相同的记录进行分组。WindowedStream<T, K, W extends Window>中的T为KeyedStream中元素数据类型,K为指定Key的数据类型,W为我们所使用的窗口类型
WindowedStream<String,Tuple,TimeWindow> windowedStream = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
转换前后的数据类型:KeyedStream->WindowedStream。
Window Apply
将整个窗口应用在指定函数上,可以对WindowedStream和AllWindowedStream使用。WindowFunction<IN, OUT, KEY, W extends Window>中的IN为输入元素类型,OUT为输出类型元素,KEY为指定的key类型,W为所使用的窗口类型。
windowedStream.apply(new WindowFunction<String, String, Tuple, TimeWindow>() { @Override public void apply(Tuple tuple, TimeWindow window, Iterable<String> input, Collector<String> out) throws Exception { int sumCount = 0; for(String line : input){ sumCount += line.split(" ").length; } out.collect(String.valueOf(sumCount)); } });
AllWindowedStream使用与WindowedStream类似。
转换前后的数据类型:WindowedStream->DataStream或AllWindowedStream->DataStream。
windowAll
我们也可以在常规DataStream上使用窗口,Window根据某些条件(比如最后5s到达的数据)对所有流事件进行分组。AllWindowedStream<T,W extends Window>中的T为DataStream中元素的数据类型,W为我们所使用的窗口类型。
AllWindowedStream<String,TimeWindow> allWindowedStream = dataStream.windowAll(TumblingEventTimeWindows.of(Time.seconds(5)));
注意:该方法在许多时候并不是并行执行的,所有记录都会收集到一个task中
转换前后的数据类型:DataStream->AllWindowedStream。
Window CoGroup
对两个指定的key的DataStream在公共窗口上执行CoGroup,和Join功能类似,但是更加灵活。CoGroupFunction<IN1,IN2,OUT>,IN1代表第一个DataStream中元素类型,IN2代表第二个DataStream中元素类型,OUT为结果输出集合类型。
dataStream .coGroup(dataStream1) .where(new MyKeySelector()).equalTo(new MyKeySelector()) .window(TumblingEventTimeWindows.of(Time.seconds(5))) .apply(new CoGroupFunction<String, String, String>() { @Override public void coGroup(Iterable<String> first, Iterable<String> second, Collector<String> out) throws Exception { } });
转换前后的数据类型:DataStream,DataStream->DataStream。
Window Join
在指定key的公共窗口上连接两个数据流。JoinFunction<IN1,IN2,OUT>中的IN1为第一个DataStream中元素数据类型,IN2为第二个DataStream中元素数据类型,OUT为Join结果数据类型。
dataStream .join(dataStream1) .where(new MyKeySelector()).equalTo(new MyKeySelector()) .window(TumblingEventTimeWindows.of(Time.seconds(5))) .apply(new JoinFunction<String, String, String>() { @Override public String join(String first, String second) throws Exception { return first + second; } });
转换前后的数据类型:DataStream,DataStream->DataStream。
Window Reduce/Fold/Aggregation/
对于WindowedStream数据流我们同样也可以应用Reduce、Fold、Aggregation函数。
windowedStream.reduce(new ReduceFunction<String>() { @Override public String reduce(String value1, String value2) throws Exception { return value1 + value2; } }); windowedStream.fold(0, new FoldFunction<String, Integer>() { @Override public Integer fold(Integer accumulator, String value) throws Exception { return accumulator + value.split(" ").length; } }); windowedStream.sum(0); windowedStream.max("count");
转换前后的数据类型:WindowedStream->DataStream。