时间语义:flink中有三种时间,分别是
(1)EventTime:即事件时间,或者说是数据本身所携带的时间,这个是非常常用的
(2)IngestionTime:数据进入时间,即数据进入flink的时间
(3)ProcessingTime:计算时间,即该数据在被flink计算处理的时间
Flink默认采用ProcessingTime,但是大部分开发中,我们都是以EventTime进行开发,使用EventTime开发时,由于分布式、网络延迟等原因,会出现数据乱序现象。如下图所示
而这种乱序数据会导致我们的数据计算错误,假如我们定义窗口大小为5,那么第一个窗口应该是[0,1,2,3,4],当 5 进入后应该立即关闭第一个窗口,开启第二个窗口,这是正常情况下的处理。
但如果是乱序数据,当[1,4]进来后, 5 就进入数据流了,此时如果窗口立即关闭并进行数据计算,那么得到的结果肯定是错误结果。
为了解决这个问题,Flink推出了watermark机制。
1. watermark机制
1.1 什么是watermark机制
正如上面所述,watermark机制是为了解决时间乱序数据导致的计算结果错误问题,其实简单来说,watermark机制的思想就是:虽然单条数据之间的时间顺序无法保证,但我们可以保证批量数据(或者说窗口)之间的时间顺序,即上一批数据的最大时间戳一定小于下一批数据的最小时间戳。如下图所示
一个 watermark 本质上就代表了这个窗口内所有数据的最大 timestamp 数值,表示以后到来的数据已经再也没有小于或 等于这个时间的了。当窗口设置watermark后,一旦窗口到达了watermark后,窗口就会关闭,并立即进行一次计算,得到计算结果。
举个例子:
数据的时间戳进入顺序为:1 3 5 7 2 4 6 8 9
假设我们设置窗口宽度为 5 ,watermark的计算规则是延迟 3 ,第一个窗口的左右边界应该是[1-5],也即是说当watermark为大于5的时候,立即关闭第一个窗口进行计算。
(1)1 进入第一个窗口,计算watermark 1-3=-2,那么当前的watermark变为-2,-2小于5,所以第一个窗口不关闭。
(2)2 进入第一个窗口,计算watermark 3-3=0,那么当前的watermark变为0,0小于5,所以第一个窗口不关闭。
(3)5 进入第一个窗口,计算watermark 5-3=2,那么当前的watermark变为2,2小于5,所以第一个窗口不关闭。
(4)7 进入第二个窗口(按照窗口宽度划分),计算watermark 7-3=4,那么当前的watermark变为4,4小于5,所以第一个窗口不关闭。
(5)2 进入第一个窗口,计算watermark 2-3=-1,-1小于当前的watermark=4,,所以watermark不变,第一个窗口不关闭。
(6)6 进入第二个窗口,计算watermark 6-3=3,3小于当前的watermark=4,,所以watermark不变,第一个窗口不关闭。
(7)8 进入第二个窗口,计算watermark 8-3=5,5大于当前的watermark=4,,所以watermark变为5,第一个窗口不关闭。
(8)9 进入第二个窗口,计算waterm
ark 9-3=6,6大于当前的watermark=5,所以watermark变为6,超过了第一个窗口的边界,所以第一个窗立即关闭并处理窗口内数据。
通过上面的例子,你可以简单理解为其实watermark机制就是将窗口关闭和数据处理计算的时间推迟了,好等待那些迟到得到数据进入窗口进行计算,并没有什么特别复杂的东西。就好像电影《摔跤吧,爸爸》,电影里两个女儿为了偷懒调整了爸爸的闹钟时间(闹钟时间就是watermark)的做法,本来8点时间一到(到达窗口关闭时间),就应该起床锻炼(窗口关闭,数据计算),但如果将闹钟时间往后调两个小时(设置watermark,延迟2小时),即使实际上8点到了(原本窗口的最大时间戳已经进入窗口),也还可以多睡两个小时(不触发关闭),等到实际上10点,闹钟时间8点的时候,在起床(watermark到达,窗口关闭)。
但注意,watermark机制可不是和上一篇文章所说的window api中的allowedLateness()方法一样,虽然最终效果一样。
watermark机制是延迟了窗口的关闭和处理计算时间,但是allowedLateness()方法只延迟了窗口关闭时间,并没有延迟数据处理计算时间,到了时间点,窗口仍然会触发一次计算操作,并且将计算结果传递给下游算子任务,而watermark机制则不会,这点是最重要的区别。
当然,如果有一部分数据还是在我们设定的watermark之后到达,仍然无法解决数据计算的准确度问题,在这种情况下,可以通过采用方法allowedLateness()方法来使窗口延迟关闭,先输出一次近似结果,再进行一次等待,如果等待时间结束后还有这个窗口的数据进入,那么就只能通过侧输出流来进行二次处理,这三步保证基本可以解决数据准确度问题。
watermark设置的过小会造成很多迟到数据无法参与运算导致结果错误,但如果watermark设置的过大,又会造成数据处理延迟问题,所以,如何平衡数据处理结果延迟与数据处理准确度的问题,得由自己依据业务情况衡量解决。
1.2 watermark的特点
(1)watermark 本身是一条特殊的数据记录,它是由进入Fink的数据中的时间戳所计算得到的,代表着当前流处理的时间进度。
(2)watermark 必须单调递增,以确保任务的事件时间时钟在向前推进,而不是在后退。
(3)watermark 与数据中所携带的时间戳相关。
1.3 watermark的上下游传播
watermark会在整条数据流中以广播的形式自上游向下游进行传播,其传播的方式简单概括来讲就是:单输入取其大,多输入取其小。
单输入取其大:对于同一个上游子任务发送的watermark,支取最大的作为watermark。
多输入取其小:由于并行运行的原因,同一个算子任务会有多个线程运行,每个线程内部的watermark自然也不一样,所以,下游算子任务会收到来自上游的多个watermark,此时下游的算子任务就要从这些watermark中取最小的作为当前算子任务的最小watermark。
下图比较形象的描述了这个过程
1.4 watermark相关代码API
(1)自定义SourceFunction实现数据时间戳提取和watermark生成:
我们在SourceFunction接口中定义run方法的逻辑代码时,可以通过调用参数sourceContext对象的 collectWithTimestamp() 方法发送一条数据,其中第一个参数就是我们要发送的数据,第二个参数就是这个数据所对应的时间戳;
而调用sourceContext对象的emitWatermark() 方法可以提交一条 watermark,表示接下来不会再有时间戳小于等于这个数值记录。
public interface SourceFunction<T> extends Function, Serializable {
void run(SourceFunction.SourceContext<T> sourceContext) throws Exception;
void cancel();
public interface SourceContext<T> {
void collect(T var1);
void collectWithTimestamp(T var1, long var2);
void emitWatermark(Watermark var1);
void markAsTemporarilyIdle();
Object getCheckpointLock();
void close();
}
}
public class MySourceFunction implements SourceFunction<String> {
@Override
public void run(SourceContext<String> sourceContext) throws Exception {
//数据时间戳提取
sourceContext.collectWithTimestamp("test", 1L);
//生成Watermark
sourceContext.emitWatermark(new Watermark(1L));
}
@Override
public void cancel() {
}
}
(2)assignTimestampsAndWatermarks()方法指定数据时间戳提取与watermark生成:
此方法属于DataStream API, 调用这个方法,能够接收不同的 timestamp 和 watermark 的生成器。watermark 的生成器分为两类
第一类是定时生成器,即每隔一段时间就调用生成器生成watermark,需要自己实现AssignerWithPeriodicWatermarks接口来定义时间戳提取逻辑和watermark生成生成规则。Flink提供了一个该接口的实现类BoundedOutOfOrdernessTimestampExtractor,可以参考该类的具体实现。
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//首先,必须要设置为事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//如果是周期性的生成Watermark,可以自己设置这个周期时间,单位是ms,默认是200ms
env.getConfig().setAutoWatermarkInterval(1000);
//设置为事件时间后,需要指定提取时间字段,并且设置watermark
DataStreamSource<String> streamSource = env.socketTextStream("192.168.127.128", 9999);
SingleOutputStreamOperator<TestBean> map = streamSource.map(new MyMapper());
//该方法用于提取数据中的时间戳,BoundedOutOfOrdernessTimestampExtractor表示是一个有界无序数据流的时间戳提取器
//参数表示最大乱序程度,或者说延迟等待时间
map.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor<TestBean>(Time.seconds(10)) {
@Override
public long extractTimestamp(TestBean testBean) {
return testBean.getTimestamp();
}
});
//自定义AssignerWithPeriodicWatermarks
map.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<TestBean>() {
/**
* @Author
* @Description //TODO 在该方法内定义Watermark生成逻辑
* @param
* @Return org.apache.flink.streaming.api.watermark.Watermark
* @Modified By:
*/
@Nullable
@Override
public Watermark getCurrentWatermark() {
return null;
}
/**
* @Author
* @Description //TODO 数据时间戳提取逻辑
* @Date
* @param testBean 传入的当前数据
* @param l 上一个数据时间戳
* @Return long 返回生成的数据时间戳
* @Modified By:
*/
@Override
public long extractTimestamp(TestBean testBean, long l) {
return 0;
}
});
第二类是根据一些在流处理数据流中的数据生成的,由数据驱动,需要实现AssignerWithPunctuatedWatermarks接口。
map.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<TestBean>() {
/**
* @Author dinggang
* @Description //TODO 在该方法内基于数据时间戳生成Watermark
* @param testBean 上一个数据
* @param l 数据的时间戳
* @Return org.apache.flink.streaming.api.watermark.Watermark
* @Modified By:
*/
@Nullable
@Override
public Watermark checkAndGetNextWatermark(TestBean testBean, long l) {
return null;
}
//时间戳提取逻辑
@Override
public long extractTimestamp(TestBean testBean, long l) {
return 0;
}
});