针对stream数据中的时间,可以分为以下三种
•Event Time:事件产生的时间,它通常由事件中的时间戳描述。
•Ingestion time:事件进入Flink的时间
Processing Time:事件被处理时当前系统的时间
l原始日志如下
•2018-10-10 10:00:01,134 INFO executor.Executor: Finished task in state 0.0
l这条数据进入Flink的时间是2018-10-10 20:00:00,102
l到达window处理的时间为2018-10-10 20:00:01,100
lFlink中,默认Time类似是ProcessingTime
l如果我们想要统计每分钟内接口调用失败的错误日志个数,就需要把时间设置成事件产生时间
-------------------------------------
l在使用eventTime的时候如何处理乱序数据?
l我们知道,流处理从事件产生,到流经source,再到operator,中间是有一个过程和时间的。虽然大部分情况下,流到operator的数据都是按照事件产生的时间顺序来的,但是也不排除由于网络延迟等原因,导致乱序的产生,特别是使用kafka的话,多个分区的数据无法保证有序。所以在进行window计算的时候,我们又不能无限期的等下去,必须要有个机制来保证一个特定的时间后,必须触发window去进行计算了。这个特别的机制,就是watermark,watermark是用于处理乱序事件的。
lwatermark可以翻译为水位线
有序的流的watermarks
无序的流的watermarks
多并行度流的watermarks
l注意:多并行度的情况下,watermark对齐会取所有channel最小的watermark
watermarks的生成方式
l通常,在接收到source的数据后,应该立刻生成watermark;但是,也可以在source后,应用简单的map或者filter操作后,再生成watermark。
l注意:如果指定多次watermark,后面指定的会覆盖前面的值。
l生成方式
•With Periodic Watermarks
•周期性的触发watermark的生成和发送,默认是100ms
•每隔N秒自动向流里注入一个WATERMARK 时间间隔由ExecutionConfig.setAutoWatermarkInterval 决定. 每次调用getCurrentWatermark 方法, 如果得到的WATERMARK 不为空并且比之前的大就注入流中
•可以定义一个最大允许乱序的时间,这种比较常用
•实现AssignerWithPeriodicWatermarks接口
•With Punctuated Watermarks
•基于某些事件触发watermark的生成和发送
•基于事件向流里注入一个WATERMARK,每一个元素都有机会判断是否生成一个WATERMARK. 如果得到的WATERMARK 不为空并且比之前的大就注入流中
•实现AssignerWithPunctuatedWatermarks接口
-----------------------------------------
由于wartermark比较复杂,下面用一个例子来说明
With Periodic Watermarks案例总结
代码
public class StreamingWindowWatermark2 {
public static void main(String[] args) throws Exception {
//定义socket的端口号
int port = 9000;
//获取运行环境
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
//设置使用eventtime,默认是使用processtime
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
//设置并行度为1,默认并行度是当前机器的cpu数量
env.setParallelism(1);
//连接socket获取输入的数据
DataStream<String> text = env.socketTextStream("hadoop100", port, "\n");
//解析输入的数据
DataStream<Tuple2<String, Long>> inputMap = text.map(new MapFunction<String, Tuple2<String, Long>>() {
@Override
public Tuple2<String, Long> map(String value) throws Exception {
String[] arr = value.split(",");
return new Tuple2<>(arr[0], Long.parseLong(arr[1]));
}
});
//抽取timestamp和生成watermark
DataStream<Tuple2<String, Long>> waterMarkStream = inputMap.assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<Tuple2<String, Long>>() {
Long currentMaxTimestamp = 0L;
final Long maxOutOfOrderness = 10000L;// 最大允许的乱序时间是10s
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
/**
* 定义生成watermark的逻辑
* 默认100ms被调用一次
*/
@Nullable
@Override
public Watermark getCurrentWatermark() {
return new Watermark(currentMaxTimestamp - maxOutOfOrderness);
}
//定义如何提取timestamp
@Override
public long extractTimestamp(Tuple2<String, Long> element, long previousElementTimestamp) {
long timestamp = element.f1;
currentMaxTimestamp = Math.max(timestamp, currentMaxTimestamp);
System.out.println("key:"+element.f0+",eventtime:["+element.f1+"|"+sdf.format(element.f1)+"],currentMaxTimestamp:["+currentMaxTimestamp+"|"+
sdf.format(currentMaxTimestamp)+"],watermark:["+getCurrentWatermark().getTimestamp()+"|"+sdf.format(getCurrentWatermark().getTimestamp())+"]");
return timestamp;
}
});
//保存被丢弃的数据
OutputTag<Tuple2<String, Long>> outputTag = new OutputTag<Tuple2<String, Long>>("late-data"){};
//注意,由于getSideOutput方法是SingleOutputStreamOperator子类中的特有方法,所以这里的类型,不能使用它的父类dataStream。
SingleOutputStreamOperator<String> window = waterMarkStream.keyBy(0)
.window(TumblingEventTimeWindows.of(Time.seconds(3)))//按照消息的EventTime分配窗口,和调用TimeWindow效果一样
//.allowedLateness(Time.seconds(2))//允许数据迟到2秒
.sideOutputLateData(outputTag)
.apply(new WindowFunction<Tuple2<String, Long>, String, Tuple, TimeWindow>() {
/**
* 对window内的数据进行排序,保证数据的顺序
* @param tuple
* @param window
* @param input
* @param out
* @throws Exception
*/
@Override
public void apply(Tuple tuple, TimeWindow window, Iterable<Tuple2<String, Long>> input, Collector<String> out) throws Exception {
String key = tuple.toString();
List<Long> arrarList = new ArrayList<Long>();
Iterator<Tuple2<String, Long>> it = input.iterator();
while (it.hasNext()) {
Tuple2<String, Long> next = it.next();
arrarList.add(next.f1);
}
Collections.sort(arrarList);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String result = key + "," + arrarList.size() + "," + sdf.format(arrarList.get(0)) + "," + sdf.format(arrarList.get(arrarList.size() - 1))
+ "," + sdf.format(window.getStart()) + "," + sdf.format(window.getEnd());
out.collect(result);
}
});
//把迟到的数据暂时打印到控制台,实际中可以保存到其他存储介质中
DataStream<Tuple2<String, Long>> sideOutput = window.getSideOutput(outputTag);
sideOutput.print();
//测试-把结果打印到控制台即可
window.print();
//注意:因为flink是懒加载的,所以必须调用execute方法,上面的代码才会执行
env.execute("eventtime-watermark");
}
}
-----
程序运行分析:
首先我们开启socket,输入第一条数据:
nc -l 9000
0001,1538359882000
输出内容并没有触发window操作
为了查看方便,我们把输入的内容汇总到表格中
Key |
Event Time |
CurrentMaxTimeStamp |
WaterMark |
0001 |
1538359882000 |
1538359882000 |
1538359872000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 |
此时,wartermark 的时间,已经落后于 currentMaxTimestamp10 秒了。我们继续输入
nc -l 9000
0001,1538359882000
0001,1538359886000
此时也没有触发window
Key |
Event Time |
CurrentMaxTimeStamp |
WaterMark |
0001 |
1538359882000 |
1538359882000 |
1538359872000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 | |
0001 |
1538359886000 |
1538359886000 |
1538359876000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:16.000 |
继续输入:
nc -l 9000
0001,1538359882000
0001,1538359886000
0001,1538359892000
也没有触发window
Key |
Event Time |
CurrentMaxTimeStamp |
WaterMark |
0001 |
1538359882000 |
1538359882000 |
1538359872000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 | |
0001 |
1538359886000 |
1538359886000 |
1538359876000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:16.000 | |
0001 |
1538359892000 |
1538359892000 |
1538359882000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:22.000 |
到这里,window 仍然没有被触发,此时 watermark 的时间已经等于了第一条数据的 Event Time 了。那么 window 到底什么时候被触发呢?我们再次输入:
nc -l 9000
0001,1538359882000
0001,1538359886000
0001,1538359892000
0001,1538359893000
Key |
Event Time |
CurrentMaxTimeStamp |
WaterMark |
0001 |
1538359882000 |
1538359882000 |
1538359872000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 | |
0001 |
1538359886000 |
1538359886000 |
1538359876000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:16.000 | |
0001 |
1538359892000 |
1538359892000 |
1538359882000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:22.000 |
0001 |
1538359893000 |
1538359893000 |
1538359883000 |
2018-10-01 10:11:33.000 |
2018-10-01 10:11:33.000 |
2018-10-01 10:11:23.000 |
window 仍然没有触发,此时,我们的数据已经发到 2018-10-01 10:11:33.000 了,根据 eventtime 来算,最早的数据已经过去了 11 秒了,window 还没有开始计算,那到底什么时 候会触发 window 呢?
我们再次增加 1 秒,输入:
nc -l 9000
0001,1538359882000
0001,1538359886000
0001,1538359892000
0001,1538359893000
0001,1538359894000
这时候触发了window
Key |
Event Time |
CurrentMaxTim eStamp |
WaterMark |
window_start _time |
window_end_ time |
0001 |
1538359882000 |
1538359882000 |
1538359872000 | ||
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 | |||
0001 |
1538359886000 |
1538359886000 |
1538359876000 | ||
2018-10-01 10:11:26.000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:16.000 | |||
0001 |
1538359892000 |
1538359892000 |
1538359882000 | ||
2018-10-01 10:11:32.000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:22.000 | |||
0001 |
1538359893000 |
1538359893000 |
1538359883000 | ||
2018-10-01 10:11:33.000 |
2018-10-01 10:11:33.000 |
2018-10-01 10:11:23.000 | |||
0001 |
1538359894000 |
1538359894000 |
1538359884000 | ||
2018-10-01 10:11:34.000 |
2018-10-01 10:11:34.000 |
2018-10-01 10:11:24.000 |
[10:11:21.000 |
到这里,我们做一个说明:
window 的触发机制,是先按照自然时间将 window 划分,如果 window 大小是 3 秒,那么 1 分钟内会把 window 划分为如下的形式【左闭右开】:
[00:00:03,00:00:06) [00:00:06,00:00:09) [00:00:09,00:00:12) [00:00:12,00:00:15) [00:00:15,00:00:18) [00:00:18,00:00:21) [00:00:21,00:00:24) [00:00:24,00:00:27) [00:00:27,00:00:30) [00:00:30,00:00:33) [00:00:33,00:00:36) [00:00:36,00:00:39) [00:00:39,00:00:42) [00:00:42,00:00:45) [00:00:45,00:00:48) [00:00:48,00:00:51) [00:00:51,00:00:54) [00:00:54,00:00:57) [00:00:57,00:01:00) ...
window 的设定无关数据本身,而是系统定义好了的。
输入的数据中,根据自身的 Event Time,将数据划分到不同的 window 中,如果 window 中 有数据,则当 watermark 时间>=Event Time 时,就符合了 window 触发的条件了,最终决定 window 触发,还是由数据本身的 Event Time 所属的 window 中的 window_end_time 决定。
上面的测试中,最后一条数据到达后,其水位线已经升至 10:11:24 秒,正好是最早的一条 记录所在 window 的 window_end_time,所以 window 就被触发了。
为了验证 window 的触发机制,我们继续输入数据:
nc -l 9000
0001,1538359882000
0001,1538359886000
0001,1538359892000
0001,1538359893000
0001,1538359894000
0001,1538359896000
Key |
Event Time |
CurrentMaxTim eStamp |
WaterMark |
window_start_ time |
window_end_t ime |
0001 |
1538359882000 |
1538359882000 |
1538359872000 | ||
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 | |||
0001 |
1538359886000 |
1538359886000 |
1538359876000 | ||
2018-10-01 10:11:26.000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:16.000 | |||
0001 |
1538359892000 |
1538359892000 |
1538359882000 | ||
2018-10-01 10:11:32.000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:22.000 | |||
0001 |
1538359893000 |
1538359893000 |
1538359883000 | ||
2018-10-01 10:11:33.000 |
2018-10-01 10:11:33.000 |
2018-10-01 10:11:23.000 | |||
0001 |
1538359894000 |
1538359894000 |
1538359884000 | ||
2018-10-01 10:11:34.000 |
2018-10-01 10:11:34.000 |
2018-10-01 10:11:24.000 |
[10:11:21.000 |
10:11:24.000) | |
0001 |
1538359896000 |
1538359896000 |
1538359886000 | ||
2018-10-01 10:11:36.000 |
2018-10-01 10:11:36.000 |
2018-10-01 10:11:26.000 |
此时,watermark 时间虽然已经达到了第二条数据的时间,但是由于其没有达到第二条数据 所在 window 的结束时间,所以 window 并没有被触发。那么,第二条数据所在的 window 时间是:
[00:00:24,00:00:27)
也就是说,我们必须输入一个 10:11:27 秒的数据,第二条数据所在的 window 才会被触发。 我们继续输入:
nc -l 9000
0001,1538359882000
0001,1538359886000
0001,1538359892000
0001,1538359893000
0001,1538359894000
0001,1538359896000
0001,1538359897000
Key |
Event Time |
CurrentMaxTim eStamp |
WaterMark |
window_start_ time |
window_end_t ime |
0001 |
1538359882000 |
1538359882000 |
1538359872000 | ||
2018-10-01 10:11:22.000 |
2018-10-01 10:11:22.000 |
2018-10-01 10:11:12.000 | |||
0001 |
1538359886000 |
1538359886000 |
1538359876000 | ||
2018-10-01 10:11:26.000 |
2018-10-01 10:11:26.000 |
2018-10-01 10:11:16.000 | |||
0001 |
1538359892000 |
1538359892000 |
1538359882000 | ||
2018-10-01 10:11:32.000 |
2018-10-01 10:11:32.000 |
2018-10-01 10:11:22.000 | |||
0001 |
1538359893000 |
1538359893000 |
1538359883000 | ||
2018-10-01 10:11:33.000 |
2018-10-01 10:11:33.000 |
2018-10-01 10:11:23.000 | |||
0001 |
1538359894000 |
1538359894000 |
1538359884000 | ||
2018-10-01 10:11:34.000 |
2018-10-01 10:11:34.000 |
2018-10-01 10:11:24.000 |
[10:11:21.000 |
10:11:24.000) | |
0001 |
1538359896000 |
1538359896000 |
1538359886000 | ||
2018-10-01 10:11:36.000 |
2018-10-01 10:11:36.000 |
2018-10-01 10:11:26.000 | |||
0001 |
1538359897000 |
1538359897000 |
1538359887000 | ||
2018-10-01 10:11:37.000 |
2018-10-01 10:11:37.000 |
2018-10-01 10:11:27.000 |
[10:11:24.000 |
10:11:27.000) |
此时,我们已经看到,window 的触发要符合以下几个条件:
1.wartermark时间 >= window_end_time
2.在[window_start_time,window_end_time)区间中有数据存在,注意是左闭右开
同时满足了以上 2 个条件,window 才会触发。
这一节有点长,放点内容在下一节中分析