「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」
一、概述
而当在流式计算环境中数据从 Source 产生,再到转换和输出,这个过程由于网络和反压的原因会导致消息乱序。
因此,需要有一个机制来解决这个问题,这个特别的机制就是“水印”。
二、 水印(WaterMark)
WaterMark 在正常的英文翻译中是水位,但是在 Flink 框架中,翻译为“水位线”更为合理,它在本质上是一个时间戳。
Flink 中的时间:
-
EventTime(事件时间):每条数据都携带时间戳;事件发生的时间, 例如: 点击网站上的某个链接的时间, 每一条日志都会记录自己的生成时间。
-
ProcessingTime(处理时间):数据不携带任何时间戳的信息;某个
Flink节点执行某个operation的时间, 例如:timeWindow处理数据时的系统时间, 默认的时间属性就是Processing Time。 -
IngestionTime(摄入时间):和EventTime类似,不同的是Flink会使用系统时间作为时间戳绑定到每条数据,可以防止Flink内部处理数据是发生乱序的情况,但无法解决数据到达Flink之前发生的乱序问题。数据进入
Flink的时间, 如某个Flink节点的source operator接收到数据的时间, 例如 : 某个source消费到kafka中的数据。
所以,在处理消息乱序的情况时,会用 EventTime 和 WaterMark 进行配合使用。
如图:
(1)水印的本质
水印的出现是为了 解决实时计算中的数据乱序问题,它的本质是 DataStream 中一个带有时间戳的元素。
如果
Flink系统中出现了一个WaterMark T,那么就意味着EventTime < T的数据都已经到达,窗口的结束时间和 T 相同的那个窗口被触发进行计算了。
也就是说:水印是 Flink 判断迟到数据的标准,同时也是窗口触发的标记。
在程序并行度大于 1 的情况下,会有多个流产生水印和窗口,这时候
Flink会选取时间戳最小的水印。
(2)水印如何生成
Flink 提供了 assignTimestampsAndWatermarks() 方法来实现水印的提取和指定,该方法接受的入参有 AssignerWithPeriodicWatermarks 和 AssignerWithPunctuatedWatermarks 两种。
(3)水印种类
种类分为:
- 周期性水印(
Periodic WaterMark) - 间歇性水印(
Punctuated Watermark) - 递增式水印(
Assigner With Ascending Timestamp)
1)周期性水印(Periodic WaterMark)
周期性水印:根据事件或处理时间周期性地触发水印生成器(Assigner),两个水印时间戳之间并不一定具有固定时间间隔。
2)间歇性水印(Punctuated Watermark)
间歇性水印:在观察到事件后,会计算某个条件来决定是否发射水印。
举个简单的例子,假如发现接收到的数据 MyData 中以字符串 watermark 开头则产生一个水印:
data.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<UserActionRecord>() {
@Override
public Watermark checkAndGetNextWatermark(MyData data, long l) {
return data.getRecord.startsWith("watermark") ? new Watermark(l) : null;
}
@Override
public long extractTimestamp(MyData data, long l) {
return data.getTimestamp();
}
});
class MyData{
private String record;
private Long timestamp;
public String getRecord() {
return record;
}
public void setRecord(String record) {
this.record = record;
}
public Timestamp getTimestamp() {
return timestamp;
}
public void setTimestamp(Timestamp timestamp) {
this.timestamp = timestamp;
}
}
3)递增式水印(Assigner With Ascending Timestamp)
递增式水印:生成完美水印,用于顺序的无界数据集。
这水印的含义:小于其时间戳的事件均以送达且大于其时间戳的事件还没有被观察到。
举个栗子:
在使用 Kafka 作为数据源时,每个分区的消息时间通常是递增的,但 Source 节点从多个消息分区并行拉取数据时这种时间特征会被破坏。
这时可以在连接器端创建 Kafka 分区水印(Kafka-partition-aware watermark),以确保多分区消息的升序排列。
三、案例
步骤:
- 获取数据源
- 转化
- 声明水印(
watermark) - 分组聚合, 调用
window的操作 - 保存处理结果
注意:
当使用 EventTimeWindow 时, 所有的 Window 在 EventTime 的时间轴上进行划分, 也就是说, 在 Window 启动后,会根据初始的 EventTime 时间每隔一段时间划分一个窗口, 如果 Window 大小是3秒, 那么1分钟内会把 Window 划分为如下的形式:
[00:00:00,00:00:03)
[00:00:03,00:00:06)
[00:00:03,00:00:09)
[00:00:03,00:00:12)
[00:00:03,00:00:15)
[00:00:03,00:00:18)
[00:00:03,00:00:21)
[00:00:03,00:00:24)
注意:
-
窗口是左闭右开的, 形式为:
[window_start_time, window_end_time)。 -
Window的设定基于第一条消息的事件时间, 也就是说,Window会一直按照指定的时间间隔进行划分, 不论这个Window中有没有数据,EventTime在这个Window期间的数据会进入这个Window。 -
Window会不断产生, 属于这个Window范围的数据会被不断加入到Window中, 所有未被触发的Window都会等待触发, 只要Window还没触发, 属于这个Window范围的数据就会一直被加入到Window中, 直到Window被触发才会停止数据的追加, 而当Window触发之后才接受到的属于被触发Window的数据会被丢弃。 -
Window会在以下的条件满足时被触发执行:- 在
[window_start_time,window_end_time)窗口中有数据存在 watermark时间 >=window_end_time;
- 在
-
一般会设置水印时间, 比事件时间小几秒钟, 表示最大允许数据延迟达到多久 (即,水印时间 = 事件时间 - 允许延迟时间)
当接收到的 水印时间 >= 窗口结束时间且 窗口内有数据, 则触发计算 (即,事件时间 - 允许延迟时间 >= 窗口结束时间 或 事件时间 >= 窗口结束时间 + 允许延迟时间)