1、概述
flink中比較重要的是時間和狀態,學習flink的過程中對水位線的理解一直模糊,經過一段時間的消化,在此總結總結。本文主要把水位線是什麼,怎麼來的,有什麼用描述清楚。
2、不太好理解的水位線
有些人喜歡把水位線叫成水印,不管是水印還是水位線,中文翻譯過來一點都不貼切我們的生活,比較抽象,讓人難得理解。在我們生活中水位線類似家中掛在牆上的一個掛鐘,類似我們的手錶。下面來聊聊如下的話題:
1,到底是如何產生。
2,既然是一個掛鐘,鐘錶有哪些特點呢,鐘錶每隔1s秒針往前走一小步,時間是不是越來越大,這些特點水位線是不是也有呢。
3,掛鐘有什麼用處啊?晚上看看手錶發現12點,我們肯定自我暗示:"應該睡覺了",通過時間讓我們知道什麼時間該幹什麼事情。
3、什麼叫水位線
3.1、水位線的定義
水位線就是一個邏輯時鐘,為什麼叫邏輯時鐘?正常時間是有cpu產生的,週期而固定的往前走,但是我們這個時鐘的時間是程式設計師計算出來,根據"事件時間"動態計算出來(至於什麼是時間事件,有什麼使用場景這裡就不講了),如某一時刻計算的結果為x,x值為2022-10-10 10:10:10對應的時間戳為1665367810000,x的值隨著事件時間的變大而變大,可能的結果為x,x+1,x+2,x+3,x+4 ... 連續的越來越大的時間戳是不是類似鐘錶每隔1s往前走一步呢。
3.2、水位線(邏輯時鐘)的組成
水位線由一串連續的時間戳組成,越來越大,每個時間戳都是根據事件時間動態計算出來的。時鐘也是由一連續的時間組成,也是越來越大,如2022-10-10 10:10:10,2022-10-10 10:10:11,2022-10-10 10:10:12,2022-10-10 10:10:13 。。。等,水位線就是類似生活中的時鐘,所以我把這個水位線稱為邏輯時鐘,邏輯時鐘就是水位線,水印機制。
3.3、邏輯時鐘當前時間
類似時鐘的當前時間,此處此刻為幾點幾分幾秒,這個當前時間比較重要,視窗的閉合,定時任務的觸發都是根據當前時間來判斷的。
當前值特點:越來越大,流剛剛產生的時候插入負無窮大值,結束是插入正無窮大的值。
個人覺得這個當前值類似一個指標型別的變數,他的指向是不停的變化的(個人理解)。
3.4、當前時間的計算公式
時鐘的"當前時間"對應一個具體的時間戳。時鐘的當前值xxx = 事件時間 - 最大延遲時間 - 1毫秒。
3.5、來一個案例
案例描述:從socket讀取資料,並列印當前水位的具體值。
package com.deepexi.sql;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.util.Collector;
import java.time.Duration;
public class ExampleTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env
//從socket讀取資料
.socketTextStream("192.168.117.211", 9999)
.map(r -> Tuple2.of(r.split(" ")[0], Long.parseLong(r.split(" ")[1])))
.returns(Types.TUPLE(Types.STRING, Types.LONG))
.assignTimestampsAndWatermarks(
//5s延遲時間
WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
@Override
public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
//提取事件時間
return element.f1;
}
})
)
//分流
.keyBy(r -> r.f0)
.process(new KeyedProcessFunction<String, Tuple2<String, Long>, String>() {
@Override
public void processElement(Tuple2<String, Long> value, Context ctx, Collector<String> out) throws Exception {
out.collect("當前的水位線是:" + ctx.timerService().currentWatermark());
}
})
.print();
env.execute();
}
}
nc -lk 9999 開啟socket服務,監聽9999埠
命令列輸入:a 1000
[root@localhost ~]# nc -lk 9999 a 1000
idea控制檯列印
當前的水位線是:-9223372036854775808 //-9223372036854775808是一個無窮大的數字
命令列輸入:a 2000
idea控制檯列印:
當前的水位線是:-4001 //當前水位線的值 = 事件時間 - 最大延遲時間 -1 = 1000 - 5000 -1 = -4000
為什麼用1000- 5000 -1而用2000 - 5000 -1? flink會週期往流中插入水位線,水位線也是流中的一個元素,還是看下圖理解吧。
命令列輸入:a 3000
idea控制檯列印:當前的水位線是:-3001 //2000 - 5000 -1 = -2000
命令列輸入:a 10000
idea控制檯列印:當前的水位線是:-2001 //3000 - 5000 -1 = -2000
命令列輸入:a 1000
idea控制檯列印:當前的水位線是:4999 //10000 - 5000 -1 = 4999
命令列輸入:a 1000
idea控制檯列印:當前的水位線是:4999 //10000 - 5000 -1 = 4999
命令列輸入:a 2000
idea控制檯列印:當前的水位線是:4999 //10000 - 5000 -1 = 4999
通過控制檯的列印結果發現水位線的和鐘錶一樣,值總是越來越大的,隨著事件時間的變化而變化,但是不會變小,也可能會停止某一刻,如輸入a 1000後在輸入a 1000,a 2000水位線的值始終是4999。
整個列印過程
命令列視窗:
[root@master ~]# nc -lk 9999
a 1000
a 2000
a 3000
a 10000
a 1000
a 1000
a 2000
idea列印:
當前的水位線是:-9223372036854775808
當前的水位線是:-4001
當前的水位線是:-3001
當前的水位線是:-2001
當前的水位線是:4999
當前的水位線是:4999
當前的水位線是:4999
4、如何產生的
水位線本質就是一個時間戳,這個時間戳是程式設計師根據事件時間動態計算出來,直接來一個案例吧。
案例1
自定義水位線的產生邏輯,實現WatermarkStrategy介面,flink會每隔200毫秒的呼叫onPeriodicEmit方法。
public class ExampleTest2 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
//設定每隔1分鐘插入一次水位線
//env.getConfig().setAutoWatermarkInterval(6 * 1000L);
env
.socketTextStream("192.168.117.211", 9999)
.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
String[] arr = value.split(" ");
return Tuple2.of(arr[0], Long.parseLong(arr[1]));
}
})
.assignTimestampsAndWatermarks(new CustomWatermarkGenerator())
.print();
env.execute();
}
public static class CustomWatermarkGenerator implements WatermarkStrategy<Tuple2<String, Long>> {
@Override
public TimestampAssigner<Tuple2<String, Long>> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
return new SerializableTimestampAssigner<Tuple2<String, Long>>() {
@Override
public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
return element.f1;
}
};
}
@Override
public WatermarkGenerator<Tuple2<String, Long>> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new WatermarkGenerator<Tuple2<String, Long>>() {
// 最大延遲時間
private Long bound = 5000L;
private Long maxTs = -Long.MAX_VALUE + bound + 1L;
@Override
public void onEvent(Tuple2<String, Long> event, long eventTimestamp, WatermarkOutput output) {
//更新觀察到的最大事件時間
maxTs = Math.max(maxTs, event.f1);
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
System.out.println("水位線的值:" + (maxTs - bound - 1L));
// 傳送水位線,計算公式:事件時間-延遲時間-1L
output.emitWatermark(new Watermark(maxTs - bound - 1L));
}
};
}
}
}
nc -lk 9999 開啟socket服務,監聽9999埠
啟動idea,控制檯每隔200毫秒列印結果:水位線的值:xxxxx。如下:
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
命令列輸入:a 1000
控制檯每隔200毫秒列印結果:水位線的值:xxxxx。如下:
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
命令列輸入:a 2000
控制檯每隔200毫秒列印介面:水位線的值:xxxxx。如下:
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
//預設200毫秒插入水位線到流,可以設定水位線的插入流的時間間隔
env.getConfig().setAutoWatermarkInterval(6 * 1000L);
整個列印過程
命令列視窗:
[root@master ~]# nc -lk 9999
a 1000
a 2000
idea列印:
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
(a,1000)
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
(a,2000)
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
Disconnected from the target VM, address: '127.0.0.1:58591', transport: 'socket'
水位線的值:-3001
Process finished with exit code 130
通過結果我們可以知道,水位線的值隨著事件時間1000,2000的變化而變化。如果輸入a 2000後在輸入a 1000,控制檯列印結果是怎樣的?那肯定列印的是:水位線的值:-3001,因為水位線的值和時間一樣永遠只會越來越大。
案例2
改造一下程式,新增如下程式碼,keyby後,把命令列輸入的元素列印出來。
nc -lk 9999啟動socket監聽9999埠
啟動idea
命令列輸入
[root@localhost ~]# nc -lk 9999
a 1000
a 2000
a 5000
a 6000
idea控制檯列印:
水位線的值:-9223372036854775807
水位線的值:-9223372036854775807
輸入業務資料是:(a,1000)
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
水位線的值:-4001
輸入業務資料是:(a,2000)
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
水位線的值:-3001
輸入業務資料是:(a,5000)
水位線的值:-1
水位線的值:-1
水位線的值:-1
水位線的值:-1
水位線的值:-1
水位線的值:-1
水位線的值:-1
水位線的值:-1
輸入業務資料是:(a,6000)
水位線的值:999
水位線的值:999
水位線的值:999
水位線的值:999
水位線的值:999
分析計算結果結果:
-9223372036854775807,-9223372036854775807,(a,1000),-4001,-4001,-4001,-4001,-4001,-4001,-4001,-4001,(a,2000),-3001,-3001,-3001,-3001,-3001,(a,5000),-1,-1,-1,(a,6000),999,999,999,999
不知道大家有沒有一種感覺,水位線和業務資料什麼關係?是不是類似生活中落花和流水的關係呢?業務資料就是河流中的水,水位線就像落在水中的花,他們兩一起流向大海,水位線和業務資料一樣都屬於流中的一個元素。
5、有什麼用
在流的世界邏輯時鐘就是一個參照物。還是掛鐘來舉例吧,看看掛鐘已經12點了,我們肯定在會暗示自己該放下手機了要睡覺了。針對源源不斷的資料流,把資料流拆分為多段進行處理,針對每段資料進行統計,那什麼時候觸發統計呢?這個時候就會用這個邏輯時鐘,視窗看看邏輯時間當前處於幾點鐘,發現視窗結束時間小於時鐘的時間,視窗閉合進行統計。
案例1,水位線觸發定時任務的執行
功能描述:水位線的當前時間戳大於定時任務的的觸發時間後 觸發定時任務的執行。
public class ExampleTest3 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env
.socketTextStream("192.168.117.211", 9999)
.map(r -> Tuple2.of(r.split(" ")[0], Long.parseLong(r.split(" ")[1])))
.returns(Types.TUPLE(Types.STRING, Types.LONG))
.assignTimestampsAndWatermarks(
WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
@Override
public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
return element.f1;
}
})
)
.keyBy(r -> r.f0)
.process(new KeyedProcessFunction<String, Tuple2<String, Long>, String>() {
@Override
public void processElement(Tuple2<String, Long> value, Context ctx, Collector<String> out) throws Exception {
// out.collect("當前的水位線是:" + ctx.timerService().currentWatermark());
ctx.timerService().registerEventTimeTimer(value.f1 + 5000L);
out.collect("註冊了一個時間戳是:" + new Timestamp(value.f1 + 5000L) + " 的定時器");
}
@Override
public void onTimer(long timestamp, OnTimerContext ctx, Collector<String> out) throws Exception {
super.onTimer(timestamp, ctx, out);
out.collect("定時器觸發了!");
}
})
.print();
env.execute();
}
}
nc -lk 9999 開啟socket服務,監聽9999埠
命令列輸入:a 1665367810000 //1665367810000對應的時間為2022-10-10 10:10:10
控制檯輸出:註冊了一個時間戳是:2022-10-10 10:10:15.0 的定時器 //2022-10-10 10:10:15轉換為時間戳為1665367815000
解釋一下控制檯輸出結果
當前水位線的值:2022-10-10 10:10:10 - 5s -1毫秒 = 1665367810000 - 5000 -1 = 1665367804999。當水位線的值大於1665367815000定時任務觸發。
命令列輸入:1665367821000 //命令列輸入2022-10-10 10:10:21對應的時間戳1665367821000將會觸發定時任務
控制檯輸出:定時器觸發了!
命名行列印輸入
[root@master ~]# nc -lk 9999
a 1665367810000
a 1665367821000
idea列印輸入
註冊了一個時間戳是:2022-10-10 10:10:15.0 的定時器
註冊了一個時間戳是:2022-10-10 10:10:26.0 的定時器
定時器觸發了!
案例2,水位線當前時間戳大於視窗結束時間觸發視窗閉
案例day3.Example4
public class ExampleTest4 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env
.socketTextStream("192.168.117.211", 9999)
.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
String[] arr = value.split(" ");
return Tuple2.of(arr[0], Long.parseLong(arr[1]));
}
})
.assignTimestampsAndWatermarks(
// 最大延遲時間設定為5秒
WatermarkStrategy.<Tuple2<String, Long>>forBoundedOutOfOrderness(Duration.ofSeconds(5))
.withTimestampAssigner(new SerializableTimestampAssigner<Tuple2<String, Long>>() {
@Override
public long extractTimestamp(Tuple2<String, Long> element, long recordTimestamp) {
return element.f1; // 告訴flink事件時間是哪一個欄位
}
})
)
.keyBy(r -> r.f0)
// 5秒的事件時間滾動視窗
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
.process(new ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<Tuple2<String, Long>> elements, Collector<String> out) throws Exception {
long windowStart = context.window().getStart();
long windowEnd = context.window().getEnd();
// System.out.println("當前視窗的結束值:" + context.currentWatermark());
// System.out.println("當前水位線的值:" + context.currentWatermark());
long count = elements.spliterator().getExactSizeIfKnown();
out.collect("使用者" + key + " 在視窗" +
"" + new Timestamp(windowStart) + "~" + new Timestamp(windowEnd) + "" +
"中的pv次數是:" + count);
}
})
.print();
env.execute();
}
}
命令列輸入:a 1665367810000 //flink將開啟一個2022-10-10 10:10:10.0~2022-10-10 10:10:15的視窗,當水位線當前值(當前值指上面的當前時間)大於視窗結束時間對應的時間戳會觸發視窗閉合。
命令列輸入:a 1665367821000 //此時水位線當前值為:1665367821000 - 5000 -1 = 1665367815999,1665367815999轉換為時間:2022-10-10 10:10:15,2022-10-10 10:10:15等於視窗結束時間,所以觸發視窗閉合。
控制輸出:使用者a 在視窗2022-10-10 10:10:10.0~2022-10-10 10:10:15.0中的pv次數是:1
命令列
[root@master ~]# nc -lk 9999
a 1665367810000
a 1665367821000
idea
當前視窗的結束值:1665367815999
當前水位線的值:1665367815999
使用者a 在視窗2022-10-10 10:10:10.0~2022-10-10 10:10:15.0中的pv次數是:1
如果根據"處理時間"來進行統計分析,視窗要閉合進行統計,肯定有一個參考的時間,只是這個時間是cpu幫忙產生的,視窗的閉合根據cpu產生的時間進行閉合,但邏輯時鐘的某瞬間的值是程式計算出來的,這也是為什麼把水位線稱為邏輯時鐘。
6、遲到資料的處理
6.1、什麼叫遲到資料
事件時間小於水位線當前時間戳,比如當前資料流的資料xxx攜帶的事件時間是2022:20:50,邏輯時鐘的此時的時間為2022:20:51,那麼flink認為xxx就是一條遲到資料。
案例描述:手動傳送水位線,手動傳送攜帶事件時間的元素。
public class ExampleTest5 {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<String> result = env
.addSource(new SourceFunction<String>() {
@Override
public void run(SourceContext<String> ctx) throws Exception {
// 傳送資料攜帶事件時間的資料hello world
ctx.collectWithTimestamp("hello world", 1000L);
// 傳送水位線
ctx.emitWatermark(new Watermark(999L));
// 傳送資料攜帶事件時間的資料 hello flink
ctx.collectWithTimestamp("hello flink", 2000L);
// 傳送水位線
ctx.emitWatermark(new Watermark(1999L));
// 傳送資料攜帶事件時間的資料hello late
ctx.collectWithTimestamp("hello late", 1000L);
}
@Override
public void cancel() {
}
})
.process(new ProcessFunction<String, String>() {
@Override
public void processElement(String value, Context ctx, Collector<String> out) throws Exception {
//System.out.println("當前水位線:" + ctx.timerService().currentWatermark());
//判斷事件時間是否小於水位線
if (ctx.timestamp() < ctx.timerService().currentWatermark()) {
System.out.println("遲到元素:" + value);
} else {
System.out.println("正常元素:" + value);
}
}
});
env.execute();
}
}
控制檯輸出:
正常元素:hello world
正常元素:hello flink
遲到元素:hello late
6.2、遲到元素的處理
理解了什麼叫遲到元素,至於怎麼處理,flink提供了幾種方案,如
案例:遲到資料傳送到"側輸出流"中
public class ExampleTest {
// 定義側輸出流
private static OutputTag<String> lateElement = new OutputTag<String>("late-element") {
};
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
SingleOutputStreamOperator<String> result = env
.addSource(new SourceFunction<String>() {
@Override
public void run(SourceContext<String> ctx) throws Exception {
// 傳送資料攜帶事件時間的資料hello world
ctx.collectWithTimestamp("hello world", 1000L);
// 傳送水位線
ctx.emitWatermark(new Watermark(999L));
// 傳送資料攜帶事件時間的資料 hello flink
ctx.collectWithTimestamp("hello flink", 2000L);
// 傳送水位線
ctx.emitWatermark(new Watermark(1999L));
// 傳送資料攜帶事件時間的資料hello late
ctx.collectWithTimestamp("hello late", 1000L);
}
@Override
public void cancel() {
}
})
.process(new ProcessFunction<String, String>() {
@Override
public void processElement(String value, Context ctx, Collector<String> out) throws Exception {
//判斷事件時間是否小於水位線
if (ctx.timestamp() < ctx.timerService().currentWatermark()) {
ctx.output(lateElement, "遲到元素髮送到側輸出流:" + value);
} else {
out.collect("正常到達的元素:" + value);
}
}
});
result.print("主流:");
result.getSideOutput(lateElement).print("側輸出流:");
env.execute();
}
}
idea控制檯輸出:
主流:> 正常到達的元素:hello world
主流:> 正常到達的元素:hello flink
側輸出流:> 遲到元素髮送到側輸出流:hello late
思考:視窗,遲到元素,水位線之間有什麼關聯?
7、總結
水位線類似生活中的時鐘,通過時鐘我們知道當前時間處於幾點幾分秒,這個"當前時間"在flink裡面對應一個時間戳,通過時間戳來觸發視窗的閉合,觸發定時任務的執行。也類似一個參照物的角色。