Flink万物之中Transform算子二
如果你看完了上篇算子一,那么这一篇可以暂时先放一放,为什么呢?因为算子学习很枯燥,可以适当结合后面对应的知识点去理解可能会没那么让人看的想睡觉,当然如果你头铁非要看,我也不反对。【摊手.jpg】
一、窗口算子
1.1 图解关系
1.2 开启窗口window与windowAll
Window()经过keyBy的数据流将形成多组数据,下游算子的多个实例可以并行计算。windowAll()不对数据流进行分组,所有数据将发送到下游算子单个实例上。
// Keyed Window
stream
.keyBy(<key selector>) <- 按照一个Key进行分组
.window(<window assigner>) <- 将数据流中的元素分配到相应的窗口中
// Non-Keyed Window
stream
.windowAll(<window assigner>) <- 不分组,将数据流中的所有元素分配到相应的窗口中
1.3 窗口处理函数
1.3.1 分类
窗口函数会对每个窗口内的数据进行处理。窗口函数主要分为两种,一种是增量计算,如reduce、aggregate。一种是全量计算,如process。增量计算指的是窗口保存一份中间数据,每流入一个新元素,新元素与中间数据两两合一,生成新的中间数据,再保存到窗口中。全量计算指的是窗口先缓存该窗口所有元素,等到触发条件后对窗口内的全量元素执行计算,所以操作效率不高,但它可以跟其他的窗口函数(ReduceFunction, AggregateFunction, or FoldFunction)结合使用,其他函数接受增量信息,ProcessWindowFunction接受窗口的元数据。
1.3.2 ReduceFunction
使用reduce算子时,我们要重写一个ReduceFunction。它接受两个相同类型的输入,生成一个输出,即两两合一地进行汇总操作,生成一个同类型的新元素,并保存一个窗口状态数据,这个状态数据的数据类型和输入的数据类型是一致的,是之前两两计算的中间结果数据。当数据流中的新元素流入后,ReduceFunction将中间结果和新流入数据两两合一,生成新的数据替换之前的状态数据。
case class StockPrice(symbol: String, price: Double)
val input: DataStream[StockPrice] = ...
senv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
// reduce的返回类型必须和输入类型StockPrice一致,否则会报错
val sum = input
.keyBy(s => s.symbol)
.timeWindow(Time.seconds(10))
.reduce((s1, s2) => StockPrice(s1.symbol, s1.price + s2.price))
缺点:能实现的功能非常有限,因为中间状态数据的数据类型、输入类型以及输出类型三者必须一致,而且只保存了一个中间状态数据,当我们想对整个窗口内的数据进行操作时,仅仅一个中间状态数据是远远不够的。
1.3.3 AggregateFunction
AggregateFunction也是一种增量计算窗口函数,也只保存了一个中间状态数据,但AggregateFunction使用起来更复杂一些。
源码:
public interface AggregateFunction<IN, ACC, OUT> extends Function, Serializable {
// 在一次新的aggregate发起时,创建一个新的Accumulator,Accumulator是我们所说的中间状态数据,简称ACC
// 这个函数一般在初始化时调用
ACC createAccumulator();
// 当一个新元素流入时,将新元素与状态数据ACC合并,返回状态数据ACC
ACC add(IN value, ACC accumulator);
// 将两个ACC合并
ACC merge(ACC a, ACC b);
// 将中间数据转成结果数据
OUT getResult(ACC accumulator);
}
输入类型是IN,输出类型是OUT,中间状态数据是ACC,这样复杂的设计主要是为了解决输入类型、中间状态和输出类型不一致的问题,同时ACC可以自定义,我们可以在ACC里构建我们想要的数据结构。
举例:求窗口内某个字段的平均值,需要ACC中保存总和以及个数
case class StockPrice(symbol: String, price: Double)
// IN: StockPrice
// ACC:(String, Double, Int) - (symbol, sum, count)
// OUT: (String, Double) - (symbol, average)
class AverageAggregate extends AggregateFunction[StockPrice, (String, Double, Int), (String, Double)] {
override def createAccumulator() = ("", 0, 0)
override def add(item: StockPrice, accumulator: (String, Double, Int)) =
(item.symbol, accumulator._2 + item.price, accumulator._3 + 1)
override def getResult(accumulator:(String, Double, Int)) = (accumulator._1 ,accumulator._2 / accumulator._3)
override def merge(a: (String, Double, Int), b: (String, Double, Int)) =
(a._1 ,a._2 + b._2, a._3 + b._3)
}
senv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
val input: DataStream[StockPrice] = ...
val average = input
.keyBy(s => s.symbol)
.timeWindow(Time.seconds(10))
.aggregate(new AverageAggregate)
这几个函数的工作流程如下图所示。在计算之前要创建一个新的ACC,这时ACC还没有任何实际表示意义,当有新数据流入时,Flink会调用add方法,更新ACC,并返回最新的ACC,ACC是一个中间状态数据。当有一些跨节点的ACC融合时,Flink会调用merge,生成新的ACC。当所有的ACC最后融合为一个ACC后,Flink调用getResult生成结果。
1.3.4 FoldFunction
FoldFunction指定窗口的输入元素如何与输出类型的元素组合。 对于添加到窗口的每个元素和当前输出值,将逐步调用FoldFunction。 第一个元素与输出类型的预定义初始值组合。
val input: DataStream[(String, Long)] = ...
input
.keyBy(<key selector>)
.window(<window assigner>)
.fold("") { (acc, v) => acc + v._2 }
注意:fold() cannot be used with session windows or other mergeable windows.
1.3.5 ProcessWindowFunction
与前两种方法不同,ProcessWindowFunction要对窗口内的全量数据都缓存。在Flink所有API中,process算子以及其对应的函数是最底层的实现,使用这些函数能够访问一些更加底层的数据,比如,直接操作状态等。
源码:
/**
* IN 输入类型
* OUT 输出类型
* KEY keyBy中按照Key分组,Key的类型
* W 窗口的类型
*/
public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window> extends AbstractRichFunction {
/**
* 对一个窗口内的元素进行处理,窗口内的元素缓存在Iterable<IN>,进行处理后输出到Collector<OUT>中
* 我们可以输出一到多个结果
*/
public abstract void process(KEY key, Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception;
/**
* 当窗口执行完毕被清理时,删除各类状态数据。
*/
public void clear(Context context) throws Exception {}
/**
* 一个窗口的上下文,包含窗口的一些元数据、状态数据等。
*/
public abstract class Context implements java.io.Serializable {
// 返回当前正在处理的Window
public abstract W window();
// 返回当前Process Time
public abstract long currentProcessingTime();
// 返回当前Event Time对应的Watermark
public abstract long currentWatermark();
// 返回某个Key下的某个Window的状态
public abstract KeyedStateStore windowState();
// 返回某个Key下的全局状态
public abstract KeyedStateStore globalState();
// 迟到数据发送到其他位置
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
}
优点:可访问时间和状态信息的Context对象,能够提供比其他窗口函数更多的灵活性。在数据量大窗口多的场景下,可能会占用大量的存储资源,使用不慎可能导致整个程序宕机。
缺点:以性能和资源消耗为代价的,因为元素不能以递增方式聚合,而是需要在内部进行缓冲,直到认为窗口已准备好进行处理。可以与其他增量计算相结合。
例:对价格出现的次数做统计,选出出现次数最多的输出出来
case class StockPrice(symbol: String, price: Double)
class FrequencyProcessFunction extends ProcessWindowFunction[StockPrice, (String, Double), String, TimeWindow] {
override def process(key: String, context: Context, elements: Iterable[StockPrice], out: Collector[(String, Double)]): Unit = {
// 股票价格和该价格出现的次数
var countMap = scala.collection.mutable.Map[Double, Int]()
for(element <- elements) {
val count = countMap.getOrElse(element.price, 0)
countMap(element.price) = count + 1
}
// 按照出现次数从高到低排序
val sortedMap = countMap.toSeq.sortWith(_._2 > _._2)
// 选出出现次数最高的输出到Collector
if (sortedMap.size > 0) {
out.collect((key, sortedMap(0)._1))
}
}
}
senv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
val input: DataStream[StockPrice] = ...
val frequency = input
.keyBy(s => s.symbol)
.timeWindow(Time.seconds(10))
.process(new FrequencyProcessFunction)
Context中有两种状态,一种是针对Key的全局状态,它是跨多个窗口的,多个窗口都可以访问;另一种是该Key下单窗口的状态,单窗口的状态只保存该窗口的数据,主要是针对process函数多次被调用的场景,比如处理迟到数据或自定义Trigger等场景。当使用单个窗口的状态时,要在clear函数中清理状态。
1.3.6 ProcessWindowFunction与增量计算相结合
那么问题来了当我们既想访问窗口里的元数据,又不想缓存窗口里的所有数据时,我们该如何处理呢?解答:可以将ProcessWindowFunction与增量计算函数相reduce和aggregate结合。对于一个窗口来说,Flink先增量计算,窗口关闭前,将增量计算结果发送给ProcessWindowFunction作为输入再进行处理。
例:Lambda函数对所有内容进行最大值和最小值的处理,这一步是增量计算。计算的结果以数据类型(String, Double, Double)传递给WindowEndProcessFunction,WindowEndProcessFunction只需要将窗口结束的时间戳添加到结果MaxMinPrice中即可。
case class StockPrice(symbol: String, price: Double)
case class MaxMinPrice(symbol: String, max: Double, min: Double, windowEndTs: Long)
class WindowEndProcessFunction extends ProcessWindowFunction[(String, Double, Double), MaxMinPrice, String, TimeWindow] {
override def process(key: String,
context: Context,
elements: Iterable[(String, Double, Double)],
out: Collector[MaxMinPrice]): Unit = {
val maxMinItem = elements.head
val windowEndTs = context.window.getEnd
out.collect(MaxMinPrice(key, maxMinItem._2, maxMinItem._3, windowEndTs))
}
}
senv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
val input: DataStream[StockPrice] = ...
// reduce的返回类型必须和输入类型相同
// 为此我们将StockPrice拆成一个三元组 (股票代号,最大值、最小值)
val maxMin = input
.map(s => (s.symbol, s.price, s.price))
.keyBy(s => s._1)
.timeWindow(Time.seconds(10))
.reduce(
((s1: (String, Double, Double), s2: (String, Double, Double)) => (s1._1, Math.max(s1._2, s2._2), Math.min(s1._3, s2._3))),
new WindowEndProcessFunction
)
1.2.7 Trigger
触发器(Trigger)决定了何时启动Window Function来处理窗口中的数据以及何时将窗口内的数据清理。每个WindowAssigner都带有一个默认触发器。比如前文这些例子都是基于Processing Time的时间窗口,当到达窗口的结束时间时,Trigger以及对应的计算被触发。如果我们有一些个性化的触发条件,比如窗口中遇到某些特定的元素、元素总数达到一定数量或窗口中的元素到达时满足某种特定的模式时,我们可以自定义一个Trigger。我们甚至可以在Trigger中定义一些提前计算的逻辑,比如在Event Time语义中,虽然Watermark还未到达,但是我们可以定义提前计算输出的逻辑,以快速获取计算结果,获得更低的延迟。
1.2.7.1 Trigger返回结果
当满足某个条件,Trigger会返回一个名为TriggerResult的结果:
- CONTINUE:什么都不做。
- FIRE:启动计算并将结果发送给下游,不清理窗口数据。
- PURGE:清理窗口数据但不执行计算。
- FIRE_AND_PURGE:启动计算,发送结果然后清理窗口数据。
WindowAssigner都有一个默认的Trigger。比如基于Event Time的窗口会有一个EventTimeTrigger,每当窗口的Watermark时间戳到达窗口的结束时间,Trigger会发送FIRE。此外,ProcessingTimeTrigger对应Processing Time窗口,CountTrigger对应Count-based窗口。
源码:
/**
* T为元素类型
* W为窗口
*/
public abstract class Trigger<T, W extends Window> implements Serializable {
/**
* 当某窗口增加一个元素时调用onElement方法,返回一个TriggerResult
*/
public abstract TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx) throws Exception;
/**
* 当一个基于Processing Time的Timer触发了FIRE时调用onProcessTime方法
*/
public abstract TriggerResult onProcessingTime(long time, W window, TriggerContext ctx) throws Exception;
/**
* 当一个基于Event Time的Timer触发了FIRE时调用onEventTime方法
*/
public abstract TriggerResult onEventTime(long time, W window, TriggerContext ctx) throws Exception;
/**
* 如果这个Trigger支持状态合并,则返回true
*/
public boolean canMerge() {
return false;
}
/**
* 当多个窗口被合并时调用onMerge
*/
public void onMerge(W window, OnMergeContext ctx) throws Exception {
throw new UnsupportedOperationException("This trigger does not support merging.");
}
/**
* 当窗口数据被清理时,调用clear方法来清理所有的Trigger状态数据
*/
public abstract void clear(W window, TriggerContext ctx) throws Exception
/**
* 上下文,保存了时间、状态、监控以及定时器
*/
public interface TriggerContext {
/**
* 返回当前Processing Time
*/
long getCurrentProcessingTime();
/**
* 返回MetricGroup
*/
MetricGroup getMetricGroup();
/**
* 返回当前Watermark时间
*/
long getCurrentWatermark();
/**
* 将某个time注册为一个Timer,当系统时间到达time这个时间点时,onProcessingTime方法会被调用
*/
void registerProcessingTimeTimer(long time);
/**
* 将某个time注册为一个Timer,当Watermark时间到达time这个时间点时,onEventTime方法会被调用
*/
void registerEventTimeTimer(long time);
/**
* 将注册的Timer删除
*/
void deleteProcessingTimeTimer(long time);
/**
* 将注册的Timer删除
*/
void deleteEventTimeTimer(long time);
/**
* 获取该窗口Trigger下的状态
*/
<S extends State> S getPartitionedState(StateDescriptor<S, ?> stateDescriptor);
}
/**
* 将多个窗口下Trigger状态合并
*/
public interface OnMergeContext extends TriggerContext {
<S extends MergingState<?, ?>> void mergePartitionedState(StateDescriptor<S, ?> stateDescriptor);
}
}
例:在股票或任何交易场景中,我们比较关注价格急跌的情况,默认窗口长度是60秒,如果价格跌幅超过5%,则立即执行Window Function,如果价格跌幅在1%到5%之内,那么10秒后触发Window Function。
class MyTrigger extends Trigger[StockPrice, TimeWindow] {
override def onElement(element: StockPrice,
time: Long,
window: TimeWindow,
triggerContext: Trigger.TriggerContext): TriggerResult = {
val lastPriceState: ValueState[Double] = triggerContext.getPartitionedState(new ValueStateDescriptor[Double]("lastPriceState", classOf[Double]))
// 设置返回默认值为CONTINUE
var triggerResult: TriggerResult = TriggerResult.CONTINUE
// 第一次使用lastPriceState时状态是空的,需要先进行判断
// 状态数据由Java端生成,如果是空,返回一个null
// 如果直接使用Scala的Double,需要使用下面的方法判断是否为空
if (Option(lastPriceState.value()).isDefined) {
if ((lastPriceState.value() - element.price) > lastPriceState.value() * 0.05) {
// 如果价格跌幅大于5%,直接FIRE_AND_PURGE
triggerResult = TriggerResult.FIRE_AND_PURGE
} else if ((lastPriceState.value() - element.price) > lastPriceState.value() * 0.01) {
val t = triggerContext.getCurrentProcessingTime + (10 * 1000 - (triggerContext.getCurrentProcessingTime % 10 * 1000))
// 给10秒后注册一个Timer
triggerContext.registerProcessingTimeTimer(t)
}
}
lastPriceState.update(element.price)
triggerResult
}
// 我们不用EventTime,直接返回一个CONTINUE
override def onEventTime(time: Long, window: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
TriggerResult.CONTINUE
}
override def onProcessingTime(time: Long, window: TimeWindow, triggerContext: Trigger.TriggerContext): TriggerResult = {
TriggerResult.FIRE_AND_PURGE
}
override def clear(window: TimeWindow, triggerContext: Trigger.TriggerContext): Unit = {
val lastPrice: ValueState[Double] = triggerContext.getPartitionedState(new ValueStateDescriptor[Double]("lastPrice", classOf[Double]))
lastPrice.clear()
}
}
senv.setStreamTimeCharacteristic(TimeCharacteristic.ProcessingTime)
val input: DataStream[StockPrice] = ...
val average = input
.keyBy(s => s.symbol)
.timeWindow(Time.seconds(60))
.trigger(new MyTrigger)
.aggregate(new AverageAggregate)
注:在自定义Trigger时,如果使用了状态,一定要使用clear方法将状态数据清理,否则随着窗口越来越多,状态数据会越积越多。
1.2.8 Evictor
清除器(Evictor)是在WindowAssigner和Trigger的基础上的一个可选选项,用来清除一些数据。我们可以在Window Function执行前或执行后调用Evictor。
源码:
/**
* T为元素类型
* W为窗口
*/
public interface Evictor<T, W extends Window> extends Serializable {
/**
* 在Window Function前调用
*/
void evictBefore(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/**
* 在Window Function后调用
*/
void evictAfter(Iterable<TimestampedValue<T>> elements, int size, W window, EvictorContext evictorContext);
/**
* Evictor的上下文
*/
interface EvictorContext {
long getCurrentProcessingTime();
MetricGroup getMetricGroup();
long getCurrentWatermark();
}
}
evictBefore和evictAfter分别在Window Function之前和之后被调用,窗口的所有元素被放在了Iterable<TimestampedValue>。当然,对于增量计算的ReduceFunction和AggregateFunction,我们没必要使用Evictor。
flink中有三种已经实现好的evictor:
- CountEvictor:从窗口保持用户指定数量的元素,并从窗口缓冲区的开头丢弃剩余的元素。
- DeltaEvictor:采用DeltaFunction和阈值,计算窗口缓冲区中最后一个元素与其余每个元素之间的差值,并删除delta大于或等于阈值的值。
- TimeEvictor:将参数作为一个间隔(以毫秒为单位),对于给定的窗口,它查找其元素中的最大时间戳max_ts,并删除时间戳小于(max_ts - interval)的所有元素。
1.2.9 windows Apply
apply方法处理windows数据,是通过windowFunction实现的,通过这个算子,可以对window数据进行处理
/**
* Base interface for functions that are evaluated over keyed (grouped) windows.
*
* @param <IN> The type of the input value.
* @param <OUT> The type of the output value.
* @param <KEY> The type of the key.
* @param <W> The type of {@code Window} that this window function can be applied on.
*/
@Public
public interface WindowFunction<IN, OUT, KEY, W extends Window> extends Function, Serializable {
/**
* Evaluates the window and outputs none or several elements.
*
* @param key The key for which this window is evaluated.
* @param window The window that is being evaluated.
* @param input The elements in the window being evaluated.
* @param out A collector for emitting elements.
*
* @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
void apply(KEY key, W window, Iterable<IN> input, Collector<OUT> out) throws Exception;
}
1.2.10 windows process
process算子是个底层的方法,常见的有ProcessFunction和KeyedProcessFunction两种计算的方式,具体的实现可以看源码。process和apply算子最大的区别在于process可以自己定时触发计算的定时器,在processElement方法定义定时器 context.timerService().registerEventTimeTimer(timestamp); ,当定时器时间到达,会回调onTimer()方法的计算任务,这是和apply最大的区别
/**
* A function that processes elements of a stream.
*
* <p>For every element in the input stream {@link #processElement(Object, Context, Collector)}
* is invoked. This can produce zero or more elements as output. Implementations can also
* query the time and set timers through the provided {@link Context}. For firing timers
* {@link #onTimer(long, OnTimerContext, Collector)} will be invoked. This can again produce
* zero or more elements as output and register further timers.
*
* <p><b>NOTE:</b> Access to keyed state and timers (which are also scoped to a key) is only
* available if the {@code ProcessFunction} is applied on a {@code KeyedStream}.
*
* <p><b>NOTE:</b> A {@code ProcessFunction} is always a
* {@link org.apache.flink.api.common.functions.RichFunction}. Therefore, access to the
* {@link org.apache.flink.api.common.functions.RuntimeContext} is always available and setup and
* teardown methods can be implemented. See
* {@link org.apache.flink.api.common.functions.RichFunction#open(org.apache.flink.configuration.Configuration)}
* and {@link org.apache.flink.api.common.functions.RichFunction#close()}.
*
* @param <I> Type of the input elements.
* @param <O> Type of the output elements.
*/
@PublicEvolving
public abstract class ProcessFunction<I, O> extends AbstractRichFunction {
private static final long serialVersionUID = 1L;
/**
* Process one element from the input stream.
*
* <p>This function can output zero or more elements using the {@link Collector} parameter
* and also update internal state or set timers using the {@link Context} parameter.
*
* @param value The input value.
* @param ctx A {@link Context} that allows querying the timestamp of the element and getting
* a {@link TimerService} for registering timers and querying the time. The
* context is only valid during the invocation of this method, do not store it.
* @param out The collector for returning result values.
*
* @throws Exception This method may throw exceptions. Throwing an exception will cause the operation
* to fail and may trigger recovery.
*/
public abstract void processElement(I value, Context ctx, Collector<O> out) throws Exception;
/**
* Called when a timer set using {@link TimerService} fires.
*
* @param timestamp The timestamp of the firing timer.
* @param ctx An {@link OnTimerContext} that allows querying the timestamp of the firing timer,
* querying the {@link TimeDomain} of the firing timer and getting a
* {@link TimerService} for registering timers and querying the time.
* The context is only valid during the invocation of this method, do not store it.
* @param out The collector for returning result values.
*
* @throws Exception This method may throw exceptions. Throwing an exception will cause the operation
* to fail and may trigger recovery.
*/
public void onTimer(long timestamp, OnTimerContext ctx, Collector<O> out) throws Exception {}
/**
* Information available in an invocation of {@link #processElement(Object, Context, Collector)}
* or {@link #onTimer(long, OnTimerContext, Collector)}.
*/
public abstract class Context {
/**
* Timestamp of the element currently being processed or timestamp of a firing timer.
*
* <p>This might be {@code null}, for example if the time characteristic of your program
* is set to {@link org.apache.flink.streaming.api.TimeCharacteristic#ProcessingTime}.
*/
public abstract Long timestamp();
/**
* A {@link TimerService} for querying time and registering timers.
*/
public abstract TimerService timerService();
/**
* Emits a record to the side output identified by the {@link OutputTag}.
*
* @param outputTag the {@code OutputTag} that identifies the side output to emit to.
* @param value The record to emit.
*/
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
/**
* Information available in an invocation of {@link #onTimer(long, OnTimerContext, Collector)}.
*/
public abstract class OnTimerContext extends Context {
/**
* The {@link TimeDomain} of the firing timer.
*/
public abstract TimeDomain timeDomain();
}
}
注:apply算子和process算子
(1) window数据 apply 算子对应的fuction : WindowFunction(keyed window) 与 AllWindowFunction(no key window)
(2) window数据 process 算子对应的fuction : ProcessWindowFunction(keyed-window) 和 ProcessAllWindowFunction(no keyd window) keyed-window 和 nokeyed-window
(3) 普通流数据,process算子对应的fuction : KeyedProcessFunction: A keyed function that processes elements of a stream. (可实现定时任务)ProcessFunction:A function that processes elements of a stream.(可实现定时任务)
二、 流关联算子
2.1 union和connect算子
connect之后生成ConnectedStreams,会对两个流的数据应用不同的处理方法,并且双流 之间可以共享状态(比如计数)。这在第一个流的输入会影响第二个流 时, 会非常有用; union 合并多个流,新的流包含所有流的数据。connect只能连接两个流,而union可以连接多于两个流 。connect连接的两个流类型可以不一致,而union连接的流的类型必须一致
2.1.1 union
union是DataStream* → DataStream,数据将按照先进先出(First In First Out)的模式合并,且不去重。下图union对白色和深色两个数据流进行合并,生成一个数据流。
例:
val aStream: DataStream[Price] = ...
val bStream: DataStream[Price] = ...
val cStream: DataStream[Price] = ...
val unionStockStream: DataStream[Price] = aStream.union(bStream, cStream)
2.1.2 connect
DataStream, DataStream → connectedStream,其只能连接两个DataStream,个DataStream经过connect之后被转化为ConnectedStreams,ConnectedStreams会对两个流的数据应用不同的处理方法,且双流之间可以共享状态。
connect经常被应用在对一个数据流使用另外一个流进行控制处理的场景上,如下图所示。控制流可以是阈值、规则、机器学习模型或其他参数。
对于ConnectedStreams,我们需要重写CoMapFunction或CoFlatMapFunction。这两个接口都提供了三个泛型,这三个泛型分别对应第一个输入流的数据类型、第二个输入流的数据类型和输出流的数据类型。在重写函数时,对于CoMapFunction,map1处理第一个流的数据,map2处理第二个流的数据;对于CoFlatMapFunction,flatMap1处理第一个流的数据,flatMap2处理第二个流的数据。Flink并不能保证两个函数调用顺序,两个函数的调用依赖于两个数据流数据的流入先后顺序,即第一个数据流有数据到达时,map1或flatMap1会被调用,第二个数据流有数据到达时,map2或flatMap2会被调用。下面的代码对一个整数流和一个字符串流进行了connect操作。
例:
val intStream: DataStream[Int] = senv.fromElements(1, 0, 9, 2, 3, 6)
val stringStream: DataStream[String] = senv.fromElements("LOW", "HIGH", "LOW", "LOW")
val connectedStream: ConnectedStreams[Int, String] = intStream.connect(stringStream)
// CoMapFunction三个泛型分别对应第一个流的输入、第二个流的输入,map之后的输出
class MyCoMapFunction extends CoMapFunction[Int, String, String] {
override def map1(input1: Int): String = input1.toString
override def map2(input2: String): String = input2
}
val mapResult = connectedStream.map(new MyCoMapFunction)
我们知道,如果不对DataStream按照Key进行分组,数据是随机分配在各个TaskSlot上的,而绝大多数情况我们是要对某个Key进行分析和处理,Flink允许我们将connect和keyBy或broadcast结合起来使用。无论先keyBy还是先connect,我们都可以将含有相同Key的数据转发到下游同一个算子实例上。这种操作有点像SQL中的join操作。
例:
// 先将两个流connect,再进行keyBy
val keyByConnect1: ConnectedStreams[Price, Product] = PriceStream
.connect(ProductStream)
.keyBy(0,0)
// 先keyBy再connect
val keyByConnect2: ConnectedStreams[Price, Product] = PriceStream.keyBy(0)
.connect(ProductStream.keyBy(0))
2.2 Split和select
2.2.1 split
DataStream → SplitStream:根据某些特征把一个DataStream拆分成两个或者多个DataStream。
2.2.2 Select
SplitStream→DataStream:从一个SplitStream中获取一个或者多个DataStream。
例:传感器数据按照温度高低(以30度为界),拆分成两个流。
val splitStream = stream2
.split( sensorData => {
if (sensorData.temperature > 30) Seq("high") else Seq("low")
} )
val high = splitStream.select("high")
val low = splitStream.select("low")
val all = splitStream.select("high", "low")
注:Split算子已被废弃,建议使用sideOutput
2.3 Cogroup 与 join
Join 与 Cogroup都是flinkSQL中用于连接多个流的算子,但是有一定的区别,推荐使用Cogroup不要使用join,因为Cogroup更强大
2.3.1 Cogroup
实现代码骨架:
oneStream.coGroup(twoStream)
.where(_.id)// oneStream中选择key
.equalTo(_.id)// twoStream中选择key
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.apply(newCoGroupFunction[Price, Product, Product_info]{
Override def coGroup(first: lang.Iterable[Price], second: lang.Iterable[Product],out:Collector[Product_info]):Unit={
//得到两个流中相同key的集合
})
Apply方法源码详解:
public <T> DataStream<T> apply(CoGroupFunction<T1, T2, T> function, TypeInformation<T> resultType) {
function = (CoGroupFunction)this.input1.getExecutionEnvironment().clean(function);
CoGroupedStreams.UnionTypeInfo<T1, T2> unionType = new CoGroupedStreams.UnionTypeInfo(this.input1.getType(), this.input2.getType());
CoGroupedStreams.UnionKeySelector<T1, T2, KEY> unionKeySelector = new CoGroupedStreams.UnionKeySelector(this.keySelector1, this.keySelector2);
//打标签one
DataStream<CoGroupedStreams.TaggedUnion<T1, T2>> taggedInput1 = this.input1.map(new CoGroupedStreams.Input1Tagger()).setParallelism(this.input1.getParallelism()).returns(unionType);
//打标签two
DataStream<CoGroupedStreams.TaggedUnion<T1, T2>> taggedInput2 = this.input2.map(new CoGroupedStreams.Input2Tagger()).setParallelism(this.input2.getParallelism()).returns(unionType);
//合并
DataStream<CoGroupedStreams.TaggedUnion<T1, T2>> unionStream = taggedInput1.union(new DataStream[]{taggedInput2});
// stream里one或two值相同的TaggedUnion元素,会被分到同一个分区中
// 此处keyby问题为如果两个one值相同的话,也会被分入同一个分区中,也就是说同一个stream的元素会自己join
this.windowedStream = (new KeyedStream(unionStream, unionKeySelector, this.keyType)).window(this.windowAssigner);
if (this.trigger != null) {
this.windowedStream.trigger(this.trigger);
}
if (this.evictor != null) {
this.windowedStream.evictor(this.evictor);
}
if (this.allowedLateness != null) {
this.windowedStream.allowedLateness(this.allowedLateness);
}
//使用窗口函数
return this.windowedStream.apply(new CoGroupedStreams.CoGroupWindowFunction(function), resultType);
}
图解流程:
2.3.2 join
DataStream join同样表示连接两个流,也是基于窗口实现,其内部调用了CoGroup的调用链。只是其不再使用CoGroupFunction,而是JoinFunction/ FlatJoinFunction,在JoinFunction/ FlatJoinFunction里面得到的是来自不同两个流的相同key的每一对数据
实现代码骨架:
oneStream.join(twoStream)
.where(_.id)// oneStream中选择key
.equalTo(_.id)// twoStream中选择key
.window(TumblingProcessingTimeWindows.of(Time.minutes(1)))
.apply(new FlatJoinFunction[Price, Product, Product_info] {
override def join(first: Price, second: Product, out: Collector[Product_info]) = {
}
}
)
图解: