Flink时间语义、窗口,水位线(watermark)介绍与应用
1.时间语义
Flink是一个实时计算引擎,谈到实时概念,就必然会设计到时间概念。Flink的时间语义是保证实时及实时数据处理的一致性,及时性。Flink时间语义分为下面三种
Event Time:事件创建时间
Ingestion Time:事件摄入时间(数据进入Flink的时间)
Processing Time:时间创建时间(执行操作算子的本地系统时间)
通过一个实际的场景就很好理解
在实际业务场景中,为保证实时性和数据的正确性通常对Event Time<事件创建时间>处理比较常见。
2.窗口(Window)
Flink的核心在处理流式数据及无限流(个人觉得在有限流及批数据处理上Spark在应用和处理上是优于Flink),无限数据集是指一种不断增长的本质上无限的数据集,而window是一种切割无限数据为有限块进行处理的手段。这里读者就会有疑问了,这不就是微批吗?为什么不用sparkstreaming,sparkstreaming的核心就是微批处理。这里个人认为在底层逻辑上是有很大差别的。flink-window本身对摄取数据的方式不做改变,只是在算子计算中根据时间控制截取有限数据块,而且这个时间控制和有限数据块不宜过大,过大就失去核心意义了。而sparkstreaming是在摄入的时候就是一批一批的摄入,而每批的摄入不宜国小,如果过小会有急剧的性能压力,会使数据计算阻塞。
Window是无限数据流处理的核心,Window将一个无限的stream拆分成有限大小的buckets桶,我们可以在这些桶上做计算操作。
Window类型
时间窗口(Time Window),按照时间生成Window
滚动时间窗口
滑动时间窗口
会话窗口
计数窗口(Count Window),按照指定的数据条数生成一个Window,与时间无关
滚动计数窗口
滑动计数窗口
滚动窗口
依据固定的窗口长度对数据进行切分,时间对齐,窗口长度固定,没有重叠
滑动窗口
可以按照固定的长度向后滑动固定的距离,滑动窗口由固定的窗口长度和滑动间隔组成,可以有重叠(是否重叠和滑动距离有关系)
滑动窗口是固定窗口的更广义的一种形式,滚动窗口可以看做是滑动窗口的一种特殊情况(即窗口大小和滑动间隔相等)
会话窗口(Session Windows)
由一系列事件组合一个指定时间长度的timeout间隙组成,也就是一段时间没有接收到新数据就会生成新的窗口
创建不同类型的窗口
滚动时间窗口(tumbling time window)
.timeWindow(Time.seconds(15))
滑动时间窗口(sliding time window)
.timeWindow(Time.seconds(15),Time.seconds(5))
会话窗口(session window)
.window(EventTimeSessionWindows.withGap(Time.minutes(10)))
滚动计数窗口(tumbling count window)
.countWindow(5)
滑动计数窗口(sliding count window)
.countWindow(10,2)
3.水位线(watermark)
watermark概念
流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的,虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络、分布式等原因,导致乱序的产生,所谓乱序,就是指Flink接收到的事件的先后顺序不是严格按照事件的Event Time顺序排列的。
Flink对于迟到数据有三层保障,先来后到的保障顺序是:
WaterMark => 约等于放宽窗口标准
allowedLateness => 允许迟到(ProcessingTime超时,但是EventTime没超时)
sideOutputLateData => 超过迟到时间,另外捕获,之后可以自己批处理合并先前的数据
那么此时出现一个问题,一旦出现乱序,如果只根据eventTime决定window的运行,我们不能明确数据是否全部到位,但又不能无限期的等下去,此时必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了,这个特别的机制,就是Watermark。
Watermark在flink中本质是解决数据的一致性(顺序性),那么如何避免乱序数据带来的计算不正确?
a.遇到一个时间戳达到了窗口关闭时间,不应该立即触发窗口计算,而是等待一段时间,等迟到的数据来了再关闭窗口
b.Watermark是一种衡量Event Time进展的机制,可以设定延迟触发
c.Watermark是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark机制结合window来实现
d.数据流中的Watermark用于表示”timestamp小于Watermark的数据,都已经到达了“,因此,window的执行也是由Watermark触发的。
e.Watermark可以理解成一个延迟触发机制,我们可以设置Watermark的延时时长t,每次系统会校验已经到达的数据中最大的maxEventTime,然后认定eventTime小于maxEventTime - t的所有数据都已经到达,如果有窗口的停止时间等于maxEventTime – t,那么这个窗口被触发执行。
Watermark = maxEventTime-延迟时间t
watermark特点
a.watermark是一条特殊的数据记录
b.watermark必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退
c.watermark与数据的时间戳相关
案例
测试代码
package com.meijs;
import java.io.Serializable;
public class Temperature implements Serializable {
private String id;
private double temperature;
private long eventTime;
//这里必须有空构造方法,不然flink会报错
public Temperature() {
}
public Temperature(String id, double temperature, long eventTime) {
this.id = id;
this.temperature = temperature;
this.eventTime = eventTime;
}
@Override
public String toString() {
return "Temperature{" +
"id='" + id + '\'' +
", temperature=" + temperature +
", eventTime=" + eventTime +
'}';
}
public String getId() {
return id;
}
public double getTemperature() {
return temperature;
}
public long getEventTime() {
return eventTime;
}
public void setId(String id) {
this.id = id;
}
public void setTemperature(double temperature) {
this.temperature = temperature;
}
public void setEventTime(long eventTime) {
this.eventTime = eventTime;
}
}
package com.meijs;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor;
import org.apache.flink.streaming.api.windowing.time.Time;
public class WatermarkTest {
public static void main(String args[]) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.getConfig().setAutoWatermarkInterval(100);
DataStream<String> dataStream = env.socketTextStream("192.168.154.130", 7777);
SingleOutputStreamOperator<Temperature> minTemp = dataStream.map(line -> {
String[] lines = line.split(",");
return new Temperature(lines[0], Double.parseDouble(lines[1]), Long.parseLong(lines[2]));
}).assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<Temperature>(Time.seconds(2)) {
@Override
public long extractTimestamp(Temperature element) {
return element.getEventTime()*1000L;
}
}).keyBy("id")
.timeWindow(Time.seconds(5))
.minBy("temperature");
minTemp.print();
env.execute("WatermarkTest");
}
}
运行测试
a.启动一个sock服务
nc -lk 7777
b.启动Java测试类,启动后往sock中一行行输入数据,观察现象如下
可以看到第一次sock输入的数据如下
1,37.2,1642599451
1,37.3,1642599452
1,37.1,1642599453
1,38.0,1642599455
1,38.2,1642599456
2,37.3,1642599454
2,38.2,1642599457
输出的结果如下
Temperature{id='1', temperature=37.1, eventTime=1642599453}
Temperature{id='2', temperature=37.3, eventTime=1642599454}
可以看到第二次sock输入的数据如下
1,37.2,1642599451
1,37.3,1642599452
1,37.1,1642599453
1,38.0,1642599455
1,38.2,1642599456
2,37.3,1642599454
2,38.2,1642599457
2,39.3,1642599458
2,39.5,1642599459
1,39.7,1642599461
2,39.7,1642599460
2,39.5,1642599462
输出的结果如下
Temperature{id='2', temperature=38.2, eventTime=1642599457}
Temperature{id='1', temperature=38.0, eventTime=1642599455}
通过对watermark,window,时间语义的综合分析,我们知道正常情况下
第一个事件时间窗口应该是如下:
[1642599450,1642599451,1642599452,1642599453,1642599454)
第二个事件时间窗口应该是如下:
[1642599455,1642599456,1642599457,1642599458,1642599459)
在第一个时间窗口中
原本应该按时到的1642599454的数据延迟了两秒才到,1642599455,1642599456早到了一秒,而我们设置的watermark为2,刚好晚到两秒的数据可以在第一个时间窗口内。
同理第二个也是如此
思考:
a.为什么时间事件语义是从1642599450开始的,而不是从我们输入的第一个数据1642599451开始的,这里我们跟一下源码
从timeWindow->KeyedStream(从timeWindow)->TumblingProcessingTimeWindows(assignWindows)->TimeWindow(getWindowStartWithOffset)
@Override
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
final long now = context.getCurrentProcessingTime();
if (staggerOffset == null) {
staggerOffset = windowStagger.getStaggerOffset(context.getCurrentProcessingTime(), size);
}
long start = TimeWindow.getWindowStartWithOffset(now, (globalOffset + staggerOffset) % size, size);
return Collections.singletonList(new TimeWindow(start, start + size));
}
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
return timestamp - (timestamp - offset + windowSize) % windowSize;
}
这里我们的timestamp=1642599451,offset=0,windowsize=5,最后计算得出的结果即起始位置为1642599450,offset为偏移量,即对当前数据是否需要往前或往后偏移。
b.我们这里设置可以修正晚2s到来导致的数据的顺序性,如果大于2s如何处理?
在flink中对于该类数据可以设置侧输出流,如下代码
package com.meijs;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.util.OutputTag;
public class WatermarkTest {
public static void main(String args[]) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.setParallelism(1);
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
env.getConfig().setAutoWatermarkInterval(100);
DataStream<String> dataStream = env.socketTextStream("192.168.154.130", 7777);
OutputTag<Temperature> outputTag = new OutputTag<Temperature>("late") {
};
SingleOutputStreamOperator<Temperature> minTemp = dataStream.map(line -> {
String[] lines = line.split(",");
return new Temperature(lines[0], Double.parseDouble(lines[1]), Long.parseLong(lines[2]));
}).assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<Temperature>(Time.seconds(2)) {
@Override
public long extractTimestamp(Temperature element) {
return element.getEventTime()*1000L;
}
}).keyBy("id")
.timeWindow(Time.seconds(5))
.allowedLateness(Time.seconds(4))
.sideOutputLateData(outputTag)
.minBy("temperature");
minTemp.print("watermark");
minTemp.getSideOutput(outputTag).print("late");
env.execute("WatermarkTest");
}
}
输入数据和输出结果如下:
注意:
a.watermark事件时间不建议设置过大,也就是说不建议处理过大的延迟,最好小于分钟级别。也不能过小。对于过大的延迟可以采取侧输出流合并。
b.不建议时间窗口设置过大,过大会影响实时流的时效。
关于flink watermark推荐一篇文章(https://blog.csdn.net/lmalds/article/details/52704170),