一.理解Flink的乱序问题
理解Flink的乱序问题,的先理解Flink的时间语义.
Flink有3中时间语义:
Event Time:事件创建的时间
Ingestion Time:数据进入Flink的时间,后续版本好像这个时间语义.也就不讨论了.
Processing Time:执行操作算子的本地系统时间,与机器相关.(Event Time的使用,必须配合WaterMark)
Flink的时间语义的使用,需要搭配window机制.没有window开窗也就不存在乱序问题.反正所有数据最终都会被处理. 只有开窗了,窗口关闭了,有的数据没进来才会导致数据丢失,进而导致计算结果错误.
那么思考一个问题:什么时间语义才会导致乱序呢?
答:Event Time.
解释:ProcessingTime以算子处理时间为准,就不存在乱序问题.
但是Event Time以数据产生时间为准.当 Flink 以 Event Time 模式处理数据流时,它会根据数据里的时间戳来处理基于时间的算子.由于网络、分布式等原因,会导致乱序数据的产生.乱序数据会让窗口计算不准确
总结:Flink的乱序,需要基于Event Time 并且 后续有开窗操作
二.Flink的乱序问题处理
1.WaterMark
1.1WaterMark的理解
1.在事件时间体系中, 时间的进度依赖于数据本身, 和任何设备的时间无关. 事件时间程序必须制定如何产生Event Time Watermarks(水印) . 在事件时间体系中, 水印是表示时间进度的标志(作用就相当于现实时间的时钟).
2.数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了,因此,window 的执行也是由 Watermark 触发的(但是这种事理想情况,所谓的理想情况就是设置乱序程度刚好满足最大乱序时间差)
3.是一个特殊的时间戳,生成之后随着流的流动而向后传递。
4.单调递增的(时间不能倒退)。
==我的理解:==WaterMark主要用来控制窗口的关闭,和计算时间. 至于数据应该进入到哪个窗口里边还是基于数据自身的Event Time
1.2WaterMark的分类
有序流中的WaterMark
事件是有序的(生成数据的时间和被处理的时间顺序是一致的), watermark是流中一个简单的周期性的标记。
有序场景:
1、 底层调用的也是乱序的Watermark生成器,只是乱序程度传了一个0ms。
2、 Watermark = maxTimestamp – outOfOrdernessMills – 1ms
= maxTimestamp – 0ms – 1ms
=>事件时间 – 1ms
//TODO 设置WaterMark 通常在开始时候设置
/**
* 参入填入 生成WaterMark的策略:有两个:单调增长和固定延迟的策略
*/
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = map.assignTimestampsAndWatermarks(
WatermarkStrategy
//有序流WaterMark
.<WaterSensor>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
//指定哪个字段作为事件时间字段
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
// waterSensorSingleOutputStreamOperator.print();
//TODO 将相同key聚合到一起
KeyedStream<WaterSensor, String> keyedStream = waterSensorSingleOutputStreamOperator.keyBy(r -> r.getId());
//TODO 开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, String, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
//TODO 窗口函数
window.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key:" + key + "窗口:[" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ")一共有" +
elements.spliterator().estimateSize() + "条数据";
out.collect(msg);
}
}).print();
乱序流中的WaterMark
在下图中, 按照他们时间戳来看, 这些事件是乱序的, 则watermark对于这些乱序的流来说至关重要.
通常情况下, 水印是一种标记, 是流中的一个点, 所有在这个时间戳(水印中的时间戳)前的数据应该已经全部到达. 一旦水印到达了算子, 则这个算子会提高他内部的时钟的值为这个水印的值.
乱序场景:
1、 什么是乱序 => 时间戳大的比时间戳小的先来
2、 乱序程度设置多少比较合适?
a) 经验值 => 对自身集群和数据的了解,大概估算。
b) 对数据进行抽样。
c) 肯定不会设置为几小时,一般设为 秒 或者 分钟。
3、 Watermark = maxTimestamp – outOfOrdernessMills – 1ms
=>当前最大的事件时间 – 乱序程度(等待时间)- 1ms
//TODO 设置WaterMark(允许固定延迟)乱序程度或者说固定延迟时间为3s
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = map.assignTimestampsAndWatermarks(WatermarkStrategy
//参数是固定延迟时间
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs()*1000;
}
})
);
//TODO keyBy
KeyedStream<WaterSensor, String> keyedStream = waterSensorSingleOutputStreamOperator.keyBy(r -> r.getId());
//TODO 开启 基于事件时间的5s滚动窗口
WindowedStream<WaterSensor, String, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)));
//TODO 窗口聚合
window.process(new ProcessWindowFunction<WaterSensor, String, String, TimeWindow>() {
@Override
public void process(String key, Context context, Iterable<WaterSensor> elements, Collector<String> out) throws Exception {
String msg = "当前key:" + key + "窗口:[" + context.window().getStart() / 1000 + "," + context.window().getEnd() / 1000 + ")一共有" +
elements.spliterator().estimateSize() + "条数据";
out.collect(msg);
}
}).print();
1.3WaterMark产生方式
周期型(后期看源码补)
周期型产生Watermark中,源码有两个重要属性.
1.记录当前当前最大时间戳 currentWatermark 记录200ms内最大的时间戳
2.上次发送的WaterMark: lastEmittedWatermark
默认每200ms,生成一个WaterMark.
当前(最大时间戳-乱序程度)与lastEmittedWatermark比较,谁大往下游发送
间歇型
每来一条生成一个Watermark
多并行度下WaterMark的传递
总结:
1.多并行度的条件下, 向下游传递WaterMark的时候是以广播的方式传递的
2.总是以最小的那个WaterMark为准! 木桶原理!
3.并且当watermark值没有增长的时候不会向下游传递,注意:生成不变。
2.窗口允许迟到数据
已经添加了wartemark之后, 仍有数据会迟到怎么办?
Flink的窗口, 也允许迟到数据. 当触发了窗口计算后, 会先计算当前的结果, 但是此时并不会关闭窗口.以后每来一条迟到数据, 则触发一次这条数据所在窗口计算(增量计算).
那么什么时候会真正的关闭窗口呢?
wartermark 超过了窗口结束时间+等待时间
//TODO 设置WaterMark(允许固定延迟)乱序程度或者说叫固定延迟为3秒
SingleOutputStreamOperator<WaterSensor> waterSensorSingleOutputStreamOperator = map.assignTimestampsAndWatermarks(
WatermarkStrategy
.<WaterSensor>forBoundedOutOfOrderness(Duration.ofSeconds(3))
//指定哪个字段作为事件时间字段
.withTimestampAssigner(new SerializableTimestampAssigner<WaterSensor>() {
@Override
public long extractTimestamp(WaterSensor element, long recordTimestamp) {
return element.getTs() * 1000;
}
})
);
//将相同key的数据聚和到一块
KeyedStream<WaterSensor, Tuple> keyedStream = waterSensorSingleOutputStreamOperator.keyBy("id");
//TODO 开启一个基于事件时间的滚动窗口
WindowedStream<WaterSensor, Tuple, TimeWindow> window = keyedStream.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//TODO 设置允许迟到的时间为3S
.allowedLateness(Time.seconds(3))
;
3.侧输出流
当迟到窗口关闭后,还有数据过来.我们可以将其放入侧输出流中
public class SideOutput {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<String> dataStream = env.socketTextStream("hadoop102", 9999);
//端口中数据转为SensorReading
SingleOutputStreamOperator<SensorReading> mapDS = dataStream.map(line -> {
String[] splits = line.split(" ");
return new SensorReading(splits[0], new Long(splits[1]), new Double(splits[2]));
});
//侧输出流标签
OutputTag<SensorReading> outputTag = new OutputTag<SensorReading>("late") {
};
//提取Watermark
SingleOutputStreamOperator<SensorReading> resultDS = mapDS.assignTimestampsAndWatermarks(
WatermarkStrategy.<SensorReading>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<SensorReading>() {
@Override
public long extractTimestamp(SensorReading element, long recordTimestamp) {
return element.getTs();
}
})
)
.keyBy(r -> r.getId())
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//设置允许迟到时间
.allowedLateness(Time.milliseconds(1))
//迟到数据放入侧输出
.sideOutputLateData(outputTag)
.max(2);
//获取侧输出流
resultDS.getSideOutput(outputTag);
env.execute();
}
}
三.代码注意事项
1.WaterMark生成策略时:泛型方法
mapDS.assignTimestampsAndWatermarks(
WatermarkStrategy.====forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner() {
@Override
public long extractTimestamp(SensorReading element, long recordTimestamp) {
return element.getTs();
}
})
)
2.定义侧输出流标签
OutputTag outputTag = new OutputTag(“late”) {
};
//侧输出流标签
OutputTag<SensorReading> outputTag = new OutputTag<SensorReading>("late") {
};
//提取Watermark
SingleOutputStreamOperator<SensorReading> resultDS = mapDS.assignTimestampsAndWatermarks(
WatermarkStrategy.<SensorReading>forBoundedOutOfOrderness(Duration.ofSeconds(3))
.withTimestampAssigner(new SerializableTimestampAssigner<SensorReading>() {
@Override
public long extractTimestamp(SensorReading element, long recordTimestamp) {
return element.getTs();
}
})
)
.keyBy(r -> r.getId())
.window(TumblingEventTimeWindows.of(Time.seconds(5)))
//设置允许迟到时间
.allowedLateness(Time.milliseconds(1))
//迟到数据放入侧输出
.sideOutputLateData(outputTag)
.max(2);
//获取侧输出流
resultDS.getSideOutput(outputTag);