【Flink】(六)ProcessFunction API(底層 API)

寫在前面:我是「雲祁」,一枚熱愛技術、會寫詩的大數據開發猿。暱稱來源於王安石詩中一句 [ 雲之祁祁,或雨於淵 ] ,甚是喜歡。


寫博客一方面是對自己學習的一點點總結及記錄,另一方面則是希望能夠幫助更多對大數據感興趣的朋友。如果你也對 數據中臺、數據建模、數據分析以及Flink/Spark/Hadoop/數倉開發 感興趣,可以關注我的動態 https://blog.csdn.net/BeiisBei ,讓我們一起挖掘大數據的價值~


每天都要進步一點點,生命不是要超越別人,而是要超越自己! (ง •_•)ง

一、前言

我們之前學習的轉換算子是無法訪問事件的時間戳信息和水位線信息的。而這在一些應用場景下,極爲重要。例如 MapFunction 這樣的 map 轉換算子就無法訪問時間戳或者當前事件的事件時間。

基於此,DataStream API 提供了一系列的 Low-Level 轉換算子。可以訪問時間戳watermark 以及註冊定時事件。還可以輸出特定的一些事件,例如超時事件等。Process Function 用來構建事件驅動的應用以及實現自定義的業務邏輯(使用之前的window 函數和轉換算子無法實現)。例如,Flink SQL 就是使用 Process Function 實現的。

Flink 提供了 8 個 Process Function:

  • ProcessFunction
  • KeyedProcessFunction
  • CoProcessFunction
  • ProcessJoinFunction
  • BroadcastProcessFunction
  • KeyedBroadcastProcessFunction
  • ProcessWindowFunction
  • ProcessAllWindowFunction

二、KeyedProcessFunction

重點介紹下 KeyedProcessFunction。

KeyedProcessFunction 用來操作 KeyedStream。KeyedProcessFunction 會處理流的每一個元素,輸出爲 0 個、1 個或者多個元素。所有的 Process Function 都繼承自RichFunction 接口,所以都有 open()、close()和 getRuntimeContext()等方法。而KeyedProcessFunction[KEY, IN, OUT]還額外提供了兩個方法:

  • processElement(v: IN, ctx: Context, out: Collector[OUT]), 流中的每一個元素都會調用這個方法,調用結果將會放在 Collector 數據類型中輸出。Context可以訪問元素的時間戳,元素的 key,以及 TimerService 時間服務。Context還可以將結果輸出到別的流(side outputs)。

  • onTimer(timestamp: Long, ctx: OnTimerContext, out: Collector[OUT])是一個回調函數。當之前註冊的定時器觸發時調用。參數 timestamp 爲定時器所設定的觸發的時間戳。Collector 爲輸出結果的集合。OnTimerContext 和 processElement 的 Context 參數一樣,提供了上下文的一些信息,例如定時器觸發的時間信息(事件時間或者處理時間)。

三、TimerService 和 定時器(Timers)

Context 和 OnTimerContext 所持有的 TimerService 對象擁有以下方法:

  • currentProcessingTime(): Long 返回當前處理時間
  • currentWatermark(): Long 返回當前 watermark 的時間戳
  • registerProcessingTimeTimer(timestamp: Long): Unit 會註冊當前 key 的processing time 的定時器。當 processing time 到達定時時間時,觸發 timer。
  • registerEventTimeTimer(timestamp: Long): Unit 會註冊當前 key 的 event time 定時器。當水位線大於等於定時器註冊的時間時,觸發定時器執行回調函數。
  • deleteProcessingTimeTimer(timestamp: Long): Unit 刪除之前註冊處理時間定時器。如果沒有這個時間戳的定時器,則不執行。

當定時器 timer 觸發時,會執行回調函數 onTimer()。注意定時器 timer 只能在keyed streams 上面使用。

下面舉個例子說明 KeyedProcessFunction 如何操作 KeyedStream。

需求:監控溫度傳感器的溫度值,如果溫度值在一秒鐘之內(processing time)連續上升,則報警。

val warnings = readings
	.keyBy(_.id) 
	.process(new TempIncreaseAlertFunction)

看一下 TempIncreaseAlertFunction 如何實現, 程序中使用了 ValueState 這樣一個狀態變量。

class TempIncreaseAlertFunction extends KeyedProcessFunction[String, SensorReading, String] {
 // 保存上一個傳感器溫度值
 lazy val lastTemp: ValueState[Double] = getRuntimeContext.getState(
	 new ValueStateDescriptor[Double]("lastTemp", Types.of[Double])
 )

 // 保存註冊的定時器的時間戳
 lazy val currentTimer: ValueState[Long] = getRuntimeContext.getState(
	 new ValueStateDescriptor[Long]("timer", Types.of[Long])
 )

 override def processElement(r: SensorReading,
				 ctx: KeyedProcessFunction[String, SensorReading, String]#Context,
				 out: Collector[String]): Unit = {
 // 取出上一次的溫度
 val prevTemp = lastTemp.value()
 // 將當前溫度更新到上一次的溫度這個變量中
 lastTemp.update(r.temperature)

 val curTimerTimestamp = currentTimer.value()
 if (prevTemp == 0.0 || r.temperature < prevTemp) {
	 // 溫度下降或者是第一個溫度值,刪除定時器
	 ctx.timerService().deleteProcessingTimeTimer(curTimerTimestamp)
	 // 清空狀態變量
	 currentTimer.clear()
 } else if (r.temperature > prevTemp && curTimerTimestamp == 0) {
 // 溫度上升且我們並沒有設置定時器
		 val timerTs = ctx.timerService().currentProcessingTime() + 1000
		 ctx.timerService().registerProcessingTimeTimer(timerTs)

		 currentTimer.update(timerTs)
 	}
 }
 
 override def onTimer(ts: Long,
						 ctx: KeyedProcessFunction[String, SensorReading, String]#OnTimerContext,
						 out: Collector[String]): Unit = {
 out.collect("傳感器 id 爲: " + ctx.getCurrentKey + "的傳感器溫度值已經連續 1s 上升了。")
 currentTimer.clear()
 	}
  }

四、側輸出流(SideOutput)

大部分的 DataStream API 的算子的輸出是單一輸出,也就是某種數據類型的流。除了 split 算子,可以將一條流分成多條流,這些流的數據類型也都相同。process function 的 side outputs 功能可以產生多條流,並且這些流的數據類型可以不一樣。一個 side output 可以定義爲 OutputTag[X]對象,X 是輸出流的數據類型。process function 可以通過 Context 對象發射一個事件到一個或者多個 side outputs。

下面是一個示例程序:

val monitoredReadings: DataStream[SensorReading] = readings
	 .process(new FreezingMonitor)

monitoredReadings
 	.getSideOutput(new OutputTag[String]("freezing-alarms"))
 	.print()
 	
readings.print()

接下來我們實現 FreezingMonitor 函數,用來監控傳感器溫度值,將溫度值低於32F 的溫度輸出到 side output。

class FreezingMonitor extends ProcessFunction[SensorReading, SensorReading] {
 // 定義一個側輸出標籤
 lazy val freezingAlarmOutput: OutputTag[String] =
 	new OutputTag[String]("freezing-alarms")

 override def processElement(r: SensorReading,
 								ctx: ProcessFunction[SensorReading, SensorReading]#Context,
								out: Collector[SensorReading]): Unit = {
 // 溫度在 32F 以下時,輸出警告信息
 if (r.temperature < 32.0) {
	 ctx.output(freezingAlarmOutput, s"Freezing Alarm for ${r.id}")
 }

 // 所有數據直接常規輸出到主流
 out.collect(r)
 	}
 }

五、CoProcessFunction

對於兩條輸入流,DataStream API 提供了 CoProcessFunction 這樣的 low-level操作。CoProcessFunction 提供了操作每一個輸入流的方法: processElement1()和processElement2()。

類似於 ProcessFunction,這兩種方法都通過 Context 對象來調用。這個 Context對象可以訪問事件數據,定時器時間戳,TimerService,以及 side outputs。CoProcessFunction 也提供了 onTimer()回調函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章