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]) = {
}
}
)
圖解: