本文转载自https://zhuanlan.zhihu.com/p/93932720
1、问题的引入:
我们在计算流式数据时,往往会用到数据窗口的概念。比如说需要计算每一个五分钟内新增还款的金额,数据是源源不断的流进来的,那么我们就需要考虑以下几个问题:
-
1.五分钟是指哪个时间,数据产生的时间,数据流入系统的时间,还是数据计算的时间。
-
2.分布式系统,由于网络或者其他的外部因素往往数据不能及时的传入到fink系统中,导致数据可能会存在乱序或者延迟到达的问题。
-
3.数据乱序或延迟后,如何保证窗口内的准确性。对于延时的数据又进行如何处理。
2、要考虑上面的问题,我们需要理解以下这几个概念。
- 1.窗口的概念:
在flink里面大致有两种窗口:滚动窗口和滑动窗口。
比如每多长时间统计一次(基于时间)
比如每多少数量统计一次(基于数量)
比如每隔30秒统计过去1分钟的数据量(基于时间)
比如每隔10个元素统计过去100个元素的数据量(基于数量)
- 2.时间的概念:
对于流式数据的处理,最大的特点就是数据具有时间的属性特征。Flink将数据的分为三种时间概念:
1.Event Time(事件的产生时间)
2.Ingestion Time(事件流入系统的时间)
3.Processing Time(事件的处理时间)
三种时间概念如下图所示:
1.事件生成时间是每个独立时间在产生他的设备上发生的时间,这个时间通常在事件进入Flink前就已经进入到事件当中了
2.事件接入时间是数据进入Flink系统的时间,它主要依赖于其Source Operator所在主机的系统时钟。需要注意的一点是相比于其他两者,他是不能处理乱序事件,也就不用生成对应的Watermarks
3.事件处理时间是指数据在操作算子计算过程中获取到的所在主机时间。在处理乱序事件时, 他不是最优的选择。Process Time主要用于时间计算精度要求不是特别高的计算场景,比如延时比较高的日志数据
4. Flink流式处理中,绝大部分的业务都会使用eventTime,一般只在eventTime无法使用时,考虑其他时间属性
5.而且watermark也只是在eventtime作为主时间才生效的,processtime作为主时间时,没有用,因为processtime的时候就不存在乱序问题了
3.知道窗口和时间的概念,我们就下面来解决数据乱序的问题:
当数据进入flink系统的时候,我们基于事件的时间创建window以后,我们如何确认这个window的数据是否已经全部到达,如果已经全部到达那么就可以进行窗口计算操作,如果没有全部到达,那么就需要进行等待数据的全部到达。
但是我们对于迟到的数据不能够无休止的等下去,所有我们必须有一个机制来保证特定的时间后,就必须出发window的计算操作,这个就是水印机制。
水印的作用:告知算子之后不会有小于或等于水印时间戳的事件。
横坐标是 event-time,纵坐标是 processing-time。理想的处理机制是,12:01分的 event-time 时间,应该就在 12:01分的 processing-time 时刻被处理。但是实际情况下,横向蓝色实线对应的 processing-time 晚于12:01分。所以图中横向黑色实线表示 event-time 的偏离,纵向红色实线表示 processing-time 的偏离,这些都比较容易理解。 接下来,按照原图的注释,浅色虚线表示 理想的 watermark,深色虚线表示 实际的 watermark。
Watermark 是从 event-time 维度描述输入数据的完整性, 换句话说,是系统评价数据流中处理 event-time 事件进度和完整性的方式"。在图2中,随着 processing-time 的推移,深色虚线逐步获取了 event-time 的完成进度。从概念上说,可以理解成一个函数F§ -> E,给定一个 processing-time 值,函数返回对应的 event-time 值。这个 event-time 时间值 E 就被系统认为所有早于 E 时刻的 event-time 事件都已经被观察处理过,或者说系统断言,以后都不会再有早于 E 时刻的 event-time 事件出现了。至此,对于流处理的算子(window … )而言,对于这个时间段的计算告一段落,可以生成结果了。
4、水印的产生方式:
水印有两种产生方式,第一种是定时产生(PeriodicWatermarks),需要依托于事件的属性,也就是从事件中提取得到的,但是由于数据乱序,需要设置允许延迟时间,例如事件时间是10,允许延迟时间是2,那么此时得到的watermark值就是8;第二种是遇到特定的事件时产生(PunctuatedWatermarks),特定事件由用户指定,当在流处理中遇到一条特殊标记则产生watermark。
一般生产中都是使用第一个。
5、水印的实例演示:
数据在源源不断的进入flink,我们设置好window的大小为5s,flink会以5s来将每分钟划分为连续的多个窗口,则flink划分的时间窗口为(左闭右开):
窗口的开始时间
窗口的结束时间
0
5
5
10
10
15
…….
……
50
55
55
60
进入flink的第一条数据会落在一个时间窗口内,假设数据的事件时间为13s,则落入的窗口是【10-15】。对于存在延迟的数据,我们能容忍的时间是3s,超过3s我就不等你了,继续进行窗口操作。
演示代码:
FastDateFormat instance = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss:SSS");
String host = "test-hdp-06";
int port = 9999;
StreamExecutionEnvironment environment = StreamExecutionEnvironment.getExecutionEnvironment();
environment.setParallelism(1);
//将eventTime设置为事件时间
environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//默认是\n
DataStreamSource<String> stringDataStreamSource = environment.socketTextStream(host, port);
//数据的格式: flink:1559201892000
SingleOutputStreamOperator<Tuple2<String, Long>> filter = stringDataStreamSource.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
try {
String[] split = value.split(":");
return new Tuple2<>(split[0], Long.parseLong(split[1]));
} catch (Exception e) {
e.printStackTrace();
}
return new Tuple2<>("0", 0L);
}
}).filter(new FilterFunction<Tuple2<String, Long>>() {
@Override
public boolean filter(Tuple2<String, Long> value) throws Exception {
if (!value.f0.equals("0") && value.f1 != 0) {
return true;
}
return false;
}
});
SingleOutputStreamOperator<Tuple2<String, Long>> tuple2SingleOutputStreamOperator = filter.
assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
//当前时间
long currentMaxTimestamp = 0L;
//控制最大延迟时间
long maxOutOfOrderness = 3000L;
//初始化水印时间
long lastEmittedWatermark = Long.MIN_VALUE;
@Nullable
@Override
public Watermark getCurrentWatermark() {
//System.out.println("============================================1");
long l = currentMaxTimestamp - maxOutOfOrderness;
if ((l >= lastEmittedWatermark)) {
lastEmittedWatermark = l;
}
// System.out.println("==========第一次时间" + lastEmittedWatermark);
return new Watermark(lastEmittedWatermark);
}
@Override
//为元素生成时间戳
public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
//System.out.println("============================================2============="+previousElementTimestamp);
long time = element.f1;
if (time > currentMaxTimestamp) {
currentMaxTimestamp = time;
}
String s = String.format("key : %s EventTime : %s waterMark : %s", element.f0, instance.format(time), instance.format(getCurrentWatermark().getTimestamp()));
System.out.println(s);
return time;
}
});
SingleOutputStreamOperator<String> apply = tuple2SingleOutputStreamOperator.keyBy(0).window(TumblingEventTimeWindows.of(Time.seconds(5L)))
.allowedLateness(Time.seconds(5L))
.apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<String> out) throws Exception {
Iterator<Tuple2<String, Long>> iterator = input.iterator();
List<String> builder = new ArrayList<>();
while (iterator.hasNext()) {
Tuple2<String, Long> next = iterator.next();
builder.add(instance.format(next.f1));
}
String result = String.format(“key: %s data : %s startTime: %s endTime : %s”, tuple.toString(), builder.toString(), instance.format(window.getStart()),
instance.format(window.getEnd()));
out.collect(result);
}
});
apply.print(“window 的计算结果:”);
environment.execute(“test watermark”);
输入
flink1:1559201892000
flink2:1559201893000
flink3:1559201894000
flink4:1559201895000
flink5:1559201896000
flink6:1559201897000
flink7:1559201898000
flink8:1559201899000
flink8:1559201900000
flink8:1559201901000
flink9:1559201902000
flink10:1559201903000
flink11:1559201905000
flink12:1559201906000
flink14:1559201908000
flink15:1559201910000
flink16:1559201911000
flink17:1559201916000
flink:1549201916000
输出
在这里插入图片描述
key : flink1 EventTime : 2019-05-30 15:38:12:000 waterMark : 2019-05-30 15:38:09:000
key : flink2 EventTime : 2019-05-30 15:38:13:000 waterMark : 2019-05-30 15:38:10:000
key : flink3 EventTime : 2019-05-30 15:38:14:000 waterMark : 2019-05-30 15:38:11:000
key : flink4 EventTime : 2019-05-30 15:38:15:000 waterMark : 2019-05-30 15:38:12:000
key : flink5 EventTime : 2019-05-30 15:38:16:000 waterMark : 2019-05-30 15:38:13:000
key : flink6 EventTime : 2019-05-30 15:38:17:000 waterMark : 2019-05-30 15:38:14:000
key : flink7 EventTime : 2019-05-30 15:38:18:000 waterMark : 2019-05-30 15:38:15:000
window 的计算结果:> key: (flink1) data : [2019-05-30 15:38:12:000] startTime: 2019-05-30 15:38:10:000 endTime : 2019-05-30 15:38:15:000
window 的计算结果:> key: (flink3) data : [2019-05-30 15:38:14:000] startTime: 2019-05-30 15:38:10:000 endTime : 2019-05-30 15:38:15:000
window 的计算结果:> key: (flink2) data : [2019-05-30 15:38:13:000] startTime: 2019-05-30 15:38:10:000 endTime : 2019-05-30 15:38:15:000
key : flink8 EventTime : 2019-05-30 15:38:19:000 waterMark : 2019-05-30 15:38:16:000
key : flink8 EventTime : 2019-05-30 15:38:20:000 waterMark : 2019-05-30 15:38:17:000
key : flink8 EventTime : 2019-05-30 15:38:21:000 waterMark : 2019-05-30 15:38:18:000
key : flink9 EventTime : 2019-05-30 15:38:22:000 waterMark : 2019-05-30 15:38:19:000
key : flink10 EventTime : 2019-05-30 15:38:23:000 waterMark : 2019-05-30 15:38:20:000
window 的计算结果:> key: (flink5) data : [2019-05-30 15:38:16:000] startTime: 2019-05-30 15:38:15:000 endTime : 2019-05-30 15:38:20:000
window 的计算结果:> key: (flink6) data : [2019-05-30 15:38:17:000] startTime: 2019-05-30 15:38:15:000 endTime : 2019-05-30 15:38:20:000
window 的计算结果:> key: (flink7) data : [2019-05-30 15:38:18:000] startTime: 2019-05-30 15:38:15:000 endTime : 2019-05-30 15:38:20:000
window 的计算结果:> key: (flink8) data : [2019-05-30 15:38:19:000] startTime: 2019-05-30 15:38:15:000 endTime : 2019-05-30 15:38:20:000
window 的计算结果:> key: (flink4) data : [2019-05-30 15:38:15:000] startTime: 2019-05-30 15:38:15:000 endTime : 2019-05-30 15:38:20:000
key : flink11 EventTime : 2019-05-30 15:38:25:000 waterMark : 2019-05-30 15:38:22:000
key : flink12 EventTime : 2019-05-30 15:38:26:000 waterMark : 2019-05-30 15:38:23:000
key : flink14 EventTime : 2019-05-30 15:38:28:000 waterMark : 2019-05-30 15:38:25:000
window 的计算结果:> key: (flink9) data : [2019-05-30 15:38:22:000] startTime: 2019-05-30 15:38:20:000 endTime : 2019-05-30 15:38:25:000
window 的计算结果:> key: (flink10) data : [2019-05-30 15:38:23:000] startTime: 2019-05-30 15:38:20:000 endTime : 2019-05-30 15:38:25:000
window 的计算结果:> key: (flink8) data : [2019-05-30 15:38:20:000, 2019-05-30 15:38:21:000] startTime: 2019-05-30 15:38:20:000 endTime : 2019-05-30 15:38:25:000
key : flink15 EventTime : 2019-05-30 15:38:30:000 waterMark : 2019-05-30 15:38:27:000
key : flink16 EventTime : 2019-05-30 15:38:31:000 waterMark : 2019-05-30 15:38:28:000
key : flink17 EventTime : 2019-05-30 15:38:36:000 waterMark : 2019-05-30 15:38:33:000
window 的计算结果:> key: (flink14) data : [2019-05-30 15:38:28:000] startTime: 2019-05-30 15:38:25:000 endTime : 2019-05-30 15:38:30:000
window 的计算结果:> key: (flink12) data : [2019-05-30 15:38:26:000] startTime: 2019-05-30 15:38:25:000 endTime : 2019-05-30 15:38:30:000
window 的计算结果:> key: (flink11) data : [2019-05-30 15:38:25:000] startTime: 2019-05-30 15:38:25:000 endTime : 2019-05-30 15:38:30:000
key : flink EventTime : 2019-02-03 21:51:56:000 waterMark : 2019-05-30 15:38:33:000
6、通过上面的演示我们可以看到窗口的触发条件:
窗口触发的条件是什么,也就是窗口是什么时候开始执行的?
1.窗口里面存在数据
2.存在一条数据:事件时间产生的水印时间≥窗口的结束时间
即:event Time-water Mark ≥ window End Time
假如窗口的结束时间是15s,并且当前的水印是延迟3秒,那么当到结束时间是15s的时候,我不进行窗口的计算。我等到有一条数据的事件时间是18秒,或18s以后到达(18-3≥15)再进行窗口计算(其实不是watermark触发的,watermark只是一个标记,有一个专门的trigger逻辑来控制这一切)。
7、通过演示我们得到的结论:
1.Flink 的窗口划分,不是基于数据的时间来划分的,而是基于自然时间来划分的。
比如我们设置窗口大小为5s,事件时间为 2019-11-21 01:00:02
那窗口的时间范围并不是想象中的:
[2019-11-21 01:00:02, 2019-11-21 01:00:07]
而是一个前闭后开的区间:
[2019-11-21 01:00:00 , 2019-11-21 01:00:05 )
[2019-11-21 01:00:10 , 2019-11-21 01:00:15 )
[2019-11-21 01:00:20 , 2019-11-21 01:00:15 )
- watermark 到达 窗口的结束时间点的时候,会触发计算。时间窗口是前闭后开的,不包括结束时间。(演示结果如下图:)
我们再演示的过程中可以看到,我们输入前两条数据是不会进行窗口计算的。
3.水印不是和某一条数据绑定的,而是一个全局的概念。也就是水印时间是一直往上的,不会下降。
我们再演示的过程中可以看到,我们输入的数据产生的水印小于当前的水印,那么当前的水印不会减小。
4.窗口计算完毕后,就直接销毁了。
我们再演示的过程中可以看到,我们输入的属于第一个窗口的数据不会再进行计算:
通过上面的结果可以看到,真正的数据可能会丢,因为有的数据真正的在窗口触发之后到达,那么迟到的数据又是我们需要的,我们又该如何处理:
我们可以在窗口上添加allowedLateness参数,这个参数是最后的等待时间,也就是在窗口结束时间再等allowedLateness参数的时间。如果还没到,那么就认为,数据再到了就没有意义了。(实时数据,如果延迟太大,就没有把必要了。)
作者:大数据部–柴晓雪