代碼GitHub:https://github.com/SmallScorpion/flink-tutorial.git
Flink中的時間語義
Event Time:是事件創建的時間。它通常由事件中的時間戳描述,例如採集的日誌數據中,每一條日誌都會記錄自己的生成時間,Flink通過時間戳分配器訪問事件時間戳。
Ingestion Time:是數據進入Flink的時間。
Processing Time:是每一個執行基於時間操作的算子的本地系統時間,與機器相關,默認的時間屬性就是Processing Time。
例子
某些應用場合,不應該使用 Processing Time,Event Time 可以從日誌數據的時間戳(timestamp)中提取
例如,一條日誌進入Flink的時間爲2017-11-12 10:00:00.123,到達Window的系統時間爲2017-11-12 10:00:01.234,日誌的內容如下:
2017-11-02 18:37:15.624 INFO Fail over to rm2
對於業務來說,要統計1min內的故障日誌個數,哪個時間是最有意義的?—— eventTime,因爲我們要根據日誌的生成時間進行統計。
EventTime的引入
在Flink的流式處理中,絕大部分的業務都會使用eventTime,一般只在eventTime無法使用時,纔會被迫使用ProcessingTime或者IngestionTime。如果要使用EventTime,那麼需要引入EventTime的時間屬性,引入方式如下所示:
val env = StreamExecutionEnvironment.getExecutionEnvironment
// 從調用時刻開始給env創建的每一個stream追加時間特徵
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
Watermark的產生背景
我們知道,流處理從事件產生,到流經source,再到operator,中間是有一個過程和時間的,雖然大部分情況下,流到operator的數據都是按照事件產生的時間順序來的,但是也不排除由於網絡、分佈式等原因,導致亂序的產生,所謂亂序,就是指Flink接收到的事件的先後順序不是嚴格按照事件的Event Time順序排列的。
那麼此時出現一個問題,一旦出現亂序,如果只根據eventTime決定window的運行,我們不能明確數據是否全部到位,但又不能無限期的等下去,此時必須要有個機制來保證一個特定的時間後,必須觸發window去進行計算了,這個特別的機制,就是Watermark。
水位線(Watermark)
- Watermark是一種衡量Event Time進展的機制。
- Watermark是用於處理亂序事件的,而正確的處理亂序事件,通常用Watermark機制結合window來實現。
- 數據流中的Watermark用於表示timestamp小於Watermark的數據,都已經到達了,因此,window的執行也是由Watermark觸發的。
- Watermark可以理解成一個延遲觸發機制,我們可以設置Watermark的延時時長t,每次系統會校驗已經到達的數據中最大的maxEventTime,然後認定eventTime小於maxEventTime - t的所有數據都已經到達,如果有窗口的停止時間等於maxEventTime – t,那麼這個窗口被觸發執行。
- watermark 用來讓程序自己平衡延遲和結果正確性
有序流的Watermarker如下圖所示:(Watermark設置爲0)
亂序流的Watermarker如下圖所示:(Watermark設置爲2)
小結:
WaterMark的特點
- watermark 是一條特殊的數據記錄
- watermark 必須單調遞增,以確保任務的事件時間時鐘在向前推進,而不是在後退
- watermark 與數據的時間戳相關
WaterMark的傳遞
WaterMark引入
Event Time的使用一定要指定數據源中的時間戳。否則程序無法知道事件的事件時間是什麼(數據源裏的數據沒有時間戳的話,就只能使用Processing Time了)
TimestampAssigner
AssignerWithPeriodicWatermarks:
- 週期性的生成 watermark:系統會週期性的將 watermark 插入到流中
- 默認週期是200毫秒,可以使用 ExecutionConfig.setAutoWatermarkInterval() 方法進行設置
- 升序和前面亂序的處理 BoundedOutOfOrderness ,都是基於週期性 watermark 的。
AssignerWithPunctuatedWatermarks :
- 沒有時間週期規律,可打斷的生成 watermark
assignTimestampsAndWatermarks底層調用:
BoundedOutOfOrdernessTimestampExtractor底層生成WaterMark爲週期性
WaterMark的設定
自定義一個週期性的時間戳抽取
產生watermark的邏輯:每隔5秒鐘,Flink會調用AssignerWithPeriodicWatermarks的getCurrentWatermark()方法。如果方法返回一個時間戳大於之前水位的時間戳,新的watermark會被插入到流中。這個檢查保證了水位線是單調遞增的。如果方法返回的時間戳小於等於之前水位的時間戳,則不會產生新的watermark。
import com.atguigu.bean.SensorReading
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
object PeriodicAssignerCustomTest {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 從調用時刻開始給env創建的每一個stream追加時間特徵
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 設置watermark的默認生成周期 -> 100毫秒生成一個WaterMark
env.getConfig.setAutoWatermarkInterval(100L)
val inputDStream: DataStream[String] = env.readTextFile("D:\\MyWork\\WorkSpaceIDEA\\flink-tutorial\\src\\main\\resources\\SensorReading.txt")
val dataDstream: DataStream[SensorReading] = inputDStream
.map( data => {
val dataArray: Array[String] = data.split(",")
SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
})
// 自定義一個週期性
.assignTimestampsAndWatermarks( MyPeriodicAssigner() )
dataDstream.print("WaterMark")
env.execute("eventTime test job")
}
}
case class MyPeriodicAssigner() extends AssignerWithPeriodicWatermarks[SensorReading]{
// 延時爲1分鐘
val bound: Long = 60 * 1000
// 觀察到的最大時間戳
var maxTs: Long = Long.MinValue
override def getCurrentWatermark: Watermark = new Watermark(maxTs - bound)
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = {
maxTs = maxTs.max(element.timestamp)
element.timestamp
}
}
自定義一個間斷性生成的WaterMark
間斷式地生成watermark。和週期性生成的方式不同,這種方式不是固定時間的,而是可以根據需要對每條數據進行篩選和處理。直接上代碼來舉個例子,我們只給sensor_1的傳感器的數據流插入watermark:
import com.atguigu.bean.SensorReading
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.AssignerWithPunctuatedWatermarks
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.watermark.Watermark
object PunctuatedAssignerCustomTest {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 從調用時刻開始給env創建的每一個stream追加時間特徵
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 設置watermark的默認生成周期 -> 100毫秒生成一個WaterMark
env.getConfig.setAutoWatermarkInterval(100L)
val inputDStream: DataStream[String] = env.readTextFile("D:\\MyWork\\WorkSpaceIDEA\\flink-tutorial\\src\\main\\resources\\SensorReading.txt")
val dataDstream: DataStream[SensorReading] = inputDStream
.map( data => {
val dataArray: Array[String] = data.split(",")
SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
})
// 自定義一個週期性
.assignTimestampsAndWatermarks( MyPunctuatedAssigner() )
dataDstream.print("WaterMark")
env.execute("eventTime test job")
}
}
case class MyPunctuatedAssigner() extends AssignerWithPunctuatedWatermarks[SensorReading]{
val bound: Long = 60 * 1000
override def checkAndGetNextWatermark(lastElement: SensorReading, extractedTimestamp: Long): Watermark = {
if(lastElement.id == "sensor_1") {
new Watermark(extractedTimestamp - bound)
} else {
null
}
}
override def extractTimestamp(element: SensorReading, previousElementTimestamp: Long): Long = element.timestamp
}
滾動窗口和EventTime
import com.atguigu.bean.SensorReading
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
/**
* 滾動窗口 + EventTime
*/
object TumblingEventTimeWindowsTest {
def main(args: Array[String]): Unit = {
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 從調用時刻開始給env創建的每一個stream追加時間特徵
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 設置watermark的默認生成周期 -> 100毫秒生成一個WaterMark
env.getConfig.setAutoWatermarkInterval(100L)
val inputDStream: DataStream[String] = env.readTextFile("D:\\MyWork\\WorkSpaceIDEA\\flink-tutorial\\src\\main\\resources\\SensorReading.txt")
val dataDstream: DataStream[SensorReading] = inputDStream
.map( data => {
val dataArray: Array[String] = data.split(",")
SensorReading(dataArray(0), dataArray(1).toLong, dataArray(2).toDouble)
})
// 自定義一個週期性
.assignTimestampsAndWatermarks( new BoundedOutOfOrdernessTimestampExtractor[SensorReading]
( Time.seconds(1000)) {
override def extractTimestamp(element: SensorReading): Long = element.timestamp * 1000L
} )
val eventTimeDStream: DataStream[SensorReading] = dataDstream
.keyBy(0)
.timeWindow( Time.seconds(15))
.reduce( (r1, r2) =>
SensorReading(r1.id, (r2.temperature-r1.temperature).toLong, r1.timestamp.min(r2.timestamp).toDouble) )
dataDstream.print("WaterMark")
eventTimeDStream.print("EventTime")
env.execute("eventTime test job")
}
}
滑動窗口
val eventTimeDStream: DataStream[SensorReading] = dataDstream
.keyBy(0)
.window(SlidingEventTimeWindows.of(Time.seconds(2),Time.milliseconds(500)))
// .timeWindow( Time.seconds(2), Time.milliseconds(500))
.reduce( (r1, r2) =>
SensorReading(r1.id, (r2.temperature-r1.temperature).toLong, r1.timestamp.min(r2.timestamp).toDouble) )
會話窗口
相鄰兩次數據的EventTime的時間差超過指定的時間間隔就會觸發執行。如果加入Watermark, 會在符合窗口觸發的情況下進行延遲。到達延遲水位再進行窗口觸發。
val eventTimeDStream: DataStream[SensorReading] = dataDstream
.keyBy(0)
.window(EventTimeSessionWindows.withGap(Time.milliseconds(500)) )
.reduce( (r1, r2) =>
SensorReading(r1.id, (r2.temperature-r1.temperature).toLong, r1.timestamp.min(r2.timestamp).toDouble) )