flink水位线
1. 时间语义
有两个非常重要的时间概念:数据的产生时间(数据自带的创建时间)和处理时间(执行处理操作的机器的系统时间)。
另外还有一个摄入时间,指的是数据进入flink数据流的时间,也就是source 算子读入数据的时间。
一般以事件时间为基准,比如我们统计PV、UV 等指标,我们就需要以事件时间为基准。且flink的时间处理默认就是事件时间。
2. 水位线
1. 什么是水位线
水位线可以看做是插入到数据流中的一个标记点,主要内容就是一个时间戳,用来指示当前的事件时间(实际就是用来度量事件时间的)。而它插入流中的位置,就应该是在某个数据到来之后;这样就可以从这个数据中提取时间戳,作为当前水位线的时间戳了。
水位线是是数据流中的一部分,随着数据一起流动,在不同任务之间传输。
并不是每条数据都会生成水位线。水位线也是一条数据,是流数据的一部分,watermark是一个全局的值,不是某一个key下的值,所以即使不是同一个key的数据,其warmark也会增加。
1. 有序流中的水位线
在有序数据流的情况下,如果每条数据都插入一个水位线会浪费大量的无用功,所以一般会每隔一段时间生成一个水位线,这个水位线的时间戳,就是当前最新数据的时间戳。
2. 乱序流中的水位线
乱序是指数据的先后顺序不一致,主要就是基于数据的产生时间而言。插入新的水位线时,要先判断一下时间戳是否比之前的大,否则就不再生成新的水位线。也就是只有数据的时间戳比当前时钟大,才能推动时钟前进,这时才插入水位线。
考虑大量数据的情况,也可以周期性插入水位线。这时要保存一下之前所有数据中的最大时间戳。
关于迟到的数据:
问题:上面操作可以定义出一个事件时钟,但是有一个问题就是无法处理迟到的数据。比如当9s的数据到达之后,我们就直接将时钟推到了9s;如果有一个窗口的结束时间是0-9s,这时候窗口就应该关闭、将收集到的数据计算输出结果。但事实上由于乱序的数据,可能时间戳为7s、8s的数据在9s之后才到来,按照上面的逻辑会导致丢失这部分数据。
解决办法:等上2s,也就是用当前已有数据最大时间戳 减去2s(这个时间需要自己根据数据的情况动态设置)。这样9s的数据到了之后,时间钟进展到7s;必须等待11s的数据到达之后,事件时钟才会进展到9s。修改后水位线如下:
3. 水位线的特点
水位线代表了当前的事件时间时钟,而且可以在数据的时间戳基础上加一些延迟来保证不丢数据,这一点对于乱序流的正确处理非常重要。水位线用来保证窗口处理结果的正确性。
- 水位线是插入到数据流中的一个标记,可以认为是一条特殊的数据
- 水位线的主要内容是基于数据的时间戳生成的一个时间戳,用来表示当前事件时间的进展
- 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
- 水位线可以通过设置延迟,来保证正确处理乱序数据
- 一个水位线Watermark(t), 表示在当前流中事件事件已经达到了事件戳t,这代表t之前的所有数据都已经到齐了,之后流中不会出现时间戳 <= t 的数据
2. 如何生成水位线
在flink的dataStream 中,有一个单独用于生成水位线的方法,assignTimestampsAndWatermarks 用于为流中的数据分配时间戳,并生成水位线来指示事件时间。
public SingleOutputStreamOperator<T> assignTimestampsAndWatermarks(
WatermarkStrategy<T> watermarkStrategy) {
该方法接收的是一个水位策略WatermarkStrategy。WatermarkStrategy 包含一个时间戳分配器 TimestampAssigner 和 一个水位线生成器 WatermarkGenerator。
- 时间戳分配器
org.apache.flink.api.common.eventtime.TimestampAssigner
@Public
@FunctionalInterface
public interface TimestampAssigner<T> {
/**
* The value that is passed to {@link #extractTimestamp} when there is no previous timestamp
* attached to the record.
*/
long NO_TIMESTAMP = Long.MIN_VALUE;
/**
* Assigns a timestamp to an element, in milliseconds since the Epoch. This is independent of
* any particular time zone or calendar.
*
* <p>The method is passed the previously assigned timestamp of the element. That previous
* timestamp may have been assigned from a previous assigner. If the element did not carry a
* timestamp before, this value is {@link #NO_TIMESTAMP} (= {@code Long.MIN_VALUE}: {@value
* Long#MIN_VALUE}).
*
* @param element The element that the timestamp will be assigned to.
* @param recordTimestamp The current internal timestamp of the element, or a negative value, if
* no timestamp has been assigned yet.
* @return The new timestamp.
*/
long extractTimestamp(T element, long recordTimestamp);
}
主要负责从流数据元素的某个字段提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础。
- 水位线生成器
org.apache.flink.api.common.eventtime.WatermarkGenerator
public interface WatermarkGenerator<T> {
/**
* 每个数据到来都会调用
*/
void onEvent(T event, long eventTimestamp, WatermarkOutput output);
/**
* 周期性调用的方法,可以由WatermarkOutput 发出水位线,周期时间为处理时间,默认为200ms。 可以通过executionEnvironment.getConfig().setAutoWatermarkInterval()设置
*/
void onPeriodicEmit(WatermarkOutput output);
}
主要负责按照既定的方式,基于时间戳生成水位线。
1. 内置水位线
使用内置的有序和无序水位线。
package cn.qz.time;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class WatermarkTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<MyEvent> dataStreamSource = executionEnvironment.fromElements(
new MyEvent("zs", "/user", 1000L),
new MyEvent("zs", "/order", 1100L),
new MyEvent("zs", "/product?id=1", 1200L),
new MyEvent("ls", "/user", 1200L),
new MyEvent("ls", "/product", 2000L),
new MyEvent("ww", "/product", 4000L),
new MyEvent("ww", "/order", 6000L),
new MyEvent("zl", "/order", 10000L)
);
// 有序流
/*dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
@Override
public long extractTimestamp(MyEvent element, long recordTimestamp) {
*//**
* 时间戳和水位线的单位都必须是毫秒
*//*
return element.getTimestamp();
}
}));*/
// 无序流(且延迟时间是5s)
dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofMillis(5))
.withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
@Override
public long extractTimestamp(MyEvent element, long recordTimestamp) {
return element.getTimestamp();
}
}))
// 根据user分组,开窗统计
.keyBy(data -> data.user)
.window(TumblingEventTimeWindows.of(Time.seconds(1)))
.process(new WatermarkTestResult())
.print();
/**
* 事实上。 有序流的水位线生成器本质和乱序流是一样的,相当于延迟设为0的乱序流水线生成器
* forMonotonousTimestamps
* forBoundedOutOfOrderness
* @see AscendingTimestampsWatermarks
*/
executionEnvironment.execute();
}
// 自定义处理窗口函数,输出当前的水位线和窗口信息以及每个窗口的数据信息
public static class WatermarkTestResult extends ProcessWindowFunction<MyEvent, String, String, TimeWindow> {
@Override
public void process(String s, Context context, Iterable<MyEvent> elements, Collector<String> out) throws Exception {
Long start = context.window().getStart();
Long end = context.window().getEnd();
Long currentWatermark = context.currentWatermark();
Long count = elements.spliterator().getExactSizeIfKnown();
// 收集元素, 然后汇总到结果集
List<String> result = new ArrayList<>();
Iterator<MyEvent> iterator = elements.iterator();
while (iterator.hasNext()) {
result.add(iterator.next().toString());
}
out.collect("窗口" + start + " ~ " + end + "中共有" + count + "个元素,窗口闭合计算时,水位线处于:" + currentWatermark + " result: " + result);
}
}
}
结果:
3> 窗口4000 ~ 5000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ww, url=/product, timestamp=4000)]
6> 窗口10000 ~ 11000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=zl, url=/order, timestamp=10000)]
1> 窗口1000 ~ 2000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ls, url=/user, timestamp=1200)]
3> 窗口6000 ~ 7000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ww, url=/order, timestamp=6000)]
7> 窗口1000 ~ 2000中共有3个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=zs, url=/user, timestamp=1000), MyEvent(user=zs, url=/order, timestamp=1100), MyEvent(user=zs, url=/product?id=1, timestamp=1200)]
1> 窗口2000 ~ 3000中共有1个元素,窗口闭合计算时,水位线处于:9223372036854775807 result: [MyEvent(user=ls, url=/product, timestamp=2000)]
也可以强行将所有数据分到一个组:(keyBy 直接返回一个固定的值即可)
package cn.qz.time;
import org.apache.flink.api.common.eventtime.SerializableTimestampAssigner;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class WatermarkTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<MyEvent> dataStreamSource = executionEnvironment.fromElements(
new MyEvent("zs", "/user", 1000L),
new MyEvent("zs", "/order", 1100L),
new MyEvent("zs", "/product?id=1", 1200L),
new MyEvent("ls", "/user", 1200L),
new MyEvent("ls", "/product", 2000L),
new MyEvent("ww", "/product", 4000L),
new MyEvent("ww", "/order", 6000L),
new MyEvent("zl", "/order", 10000L)
);
// 有序流
/*dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forMonotonousTimestamps()
.withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
@Override
public long extractTimestamp(MyEvent element, long recordTimestamp) {
*//**
* 时间戳和水位线的单位都必须是毫秒
*//*
return element.getTimestamp();
}
}));*/
// 无序流(且延迟时间是5s)
dataStreamSource.assignTimestampsAndWatermarks(WatermarkStrategy.<MyEvent>forBoundedOutOfOrderness(Duration.ofMillis(5))
.withTimestampAssigner(new SerializableTimestampAssigner<MyEvent>() {
@Override
public long extractTimestamp(MyEvent element, long recordTimestamp) {
return element.getTimestamp();
}
}))
// 根据user分组,开窗统计
.keyBy(data -> true)
.window(TumblingEventTimeWindows.of(Time.seconds(1)))
.process(new WatermarkTestResult())
.print();
/**
* 事实上。 有序流的水位线生成器本质和乱序流是一样的,相当于延迟设为0的乱序流水线生成器
* forMonotonousTimestamps
* forBoundedOutOfOrderness
* @see AscendingTimestampsWatermarks
*/
executionEnvironment.execute();
}
// 自定义处理窗口函数,输出当前的水位线和窗口信息以及每个窗口的数据信息
public static class WatermarkTestResult extends ProcessWindowFunction<MyEvent, String, Boolean, TimeWindow> {
@Override
public void process(Boolean s, Context context, Iterable<MyEvent> elements, Collector<String> out) throws Exception {
Long start = context.window().getStart();
Long end = context.window().getEnd();
Long currentWatermark = context.currentWatermark();
Long count = elements.spliterator().getExactSizeIfKnown();
// 收集元素, 然后汇总到结果集
List<String> result = new ArrayList<>();
Iterator<MyEvent> iterator = elements.iterator();
while (iterator.hasNext()) {
result.add(iterator.next().toString());
}
out.collect("窗口" + start + " ~ " + end + "中共有" + count + "个元素,窗口闭合计算时,水位线处于:" + currentWatermark + " result: " + result);
}
}
}
这里注意: 乱序流中的水位线真正的时间戳,其实是 当前最大时间戳-延迟时间-1,单位毫秒。 减一的原因:
考虑时间戳为t的水位线,标识时间戳<=t 的数据全部到齐,不会再来了。考虑有序流延迟为0的情况,时间戳为7s的时间到来时,之后其实还有可能有7s的数据,所以生成水位线的水位线不是7s,而是6秒999毫秒,7s的数据还可以继续来。参考: org.apache.flink.api.common.eventtime.BoundedOutOfOrdernessWatermarks#onPeriodicEmit
@Override
public void onPeriodicEmit(WatermarkOutput output) {
output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));
}
补充一下自己的处理窗口函数:org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction
@PublicEvolving
public abstract class ProcessWindowFunction<IN, OUT, KEY, W extends Window>
extends AbstractRichFunction {
private static final long serialVersionUID = 1L;
/**
* Evaluates the window and outputs none or several elements.
*
* @param key The key for which this window is evaluated.
* @param context The context in which the window is being evaluated.
* @param elements The elements in the window being evaluated.
* @param out A collector for emitting elements.
* @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
public abstract void process(
KEY key, Context context, Iterable<IN> elements, Collector<OUT> out) throws Exception;
/**
* Deletes any state in the {@code Context} when the Window expires (the watermark passes its
* {@code maxTimestamp} + {@code allowedLateness}).
*
* @param context The context to which the window is being evaluated
* @throws Exception The function may throw exceptions to fail the program and trigger recovery.
*/
public void clear(Context context) throws Exception {}
/** The context holding window metadata. */
public abstract class Context implements java.io.Serializable {
/** Returns the window that is being evaluated. */
public abstract W window();
/** Returns the current processing time. */
public abstract long currentProcessingTime();
/** Returns the current event-time watermark. */
public abstract long currentWatermark();
/**
* State accessor for per-key and per-window state.
*
* <p><b>NOTE:</b>If you use per-window state you have to ensure that you clean it up by
* implementing {@link ProcessWindowFunction#clear(Context)}.
*/
public abstract KeyedStateStore windowState();
/** State accessor for per-key global state. */
public abstract KeyedStateStore globalState();
/**
* Emits a record to the side output identified by the {@link OutputTag}.
*
* @param outputTag the {@code OutputTag} that identifies the side output to emit to.
* @param value The record to emit.
*/
public abstract <X> void output(OutputTag<X> outputTag, X value);
}
}
2. 自定义水位线
内置的一般够用了,有时候业务逻辑复杂这时对水位线生成的逻辑有更高的要求,就必须自定义实现水位线策略。
时间戳分配器都大同小异,指定字段提取时间戳就可以了;而不同策略的关键在于WatermarkGenerator 水位生成器的实现,有两种方式,一种是周期性(Periodic), 另一种是断点式的Punctuated。
(1)周期性水位线生成器
周期性生成器一般是通过onEvent() 观察判断输入的事件,而在onPeriodicEmit() 里发出水位线。
package cn.qz.time;
import lombok.extern.slf4j.Slf4j;
import org.apache.flink.api.common.eventtime.*;
import org.apache.flink.streaming.api.datastream.DataStreamSource;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.util.Collector;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CustomerWatermarkTest {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment executionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment();
DataStreamSource<MyEvent> dataStreamSource = executionEnvironment.fromElements(
new MyEvent("zs", "/user", 1000L),
new MyEvent("zs", "/order", 1100L),
new MyEvent("zs", "/product?id=1", 1200L),
new MyEvent("ls", "/user", 1200L),
new MyEvent("ls", "/product", 2000L),
new MyEvent("ww", "/product", 4000L),
new MyEvent("ww", "/order", 6000L),
new MyEvent("zl", "/order", 10000L)
);
dataStreamSource.assignTimestampsAndWatermarks(new CustomWatermarkStrategy())
// 根据user分组,开窗统计
.keyBy(data -> data.user)
.window(TumblingEventTimeWindows.of(Time.seconds(1)))
.process(new WatermarkTestResult())
.print();
/**
* 事实上。 有序流的水位线生成器本质和乱序流是一样的,相当于延迟设为0的乱序流水线生成器
* forMonotonousTimestamps
* forBoundedOutOfOrderness
* @see AscendingTimestampsWatermarks
*/
executionEnvironment.execute();
}
public static class CustomWatermarkStrategy implements WatermarkStrategy<MyEvent> {
@Override
public TimestampAssigner<MyEvent> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
return new SerializableTimestampAssigner<MyEvent>() {
@Override
public long extractTimestamp(MyEvent element, long recordTimestamp) {
return element.timestamp; // 告诉程序数据源里的时间戳是哪一个字段
}
};
}
@Override
public WatermarkGenerator<MyEvent> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
return new CustomPeriodicGenerator();
}
}
public static class CustomPeriodicGenerator implements WatermarkGenerator<MyEvent> {
private Long delayTime = 5000L; // 延迟时间
private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳
@Override
public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
// 每来一条数据就调用一次
maxTs = Math.max(event.timestamp, maxTs); // 更新最大时间戳
}
@Override
public void onPeriodicEmit(WatermarkOutput output) {
// 发射水位线,默认200ms调用一次
output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
}
}
/**
* ProcessWindowFunction 依次为: <IN, OUT, KEY, W extends Window>
*/
private static class WatermarkTestResult extends ProcessWindowFunction<MyEvent, String, String, TimeWindow> {
@Override
public void process(String s, Context context, Iterable<MyEvent> elements, Collector<String> out) throws Exception {
Long start = context.window().getStart();
Long end = context.window().getEnd();
Long currentWatermark = context.currentWatermark();
Long count = elements.spliterator().getExactSizeIfKnown();
// 收集元素, 然后汇总到结果集
List<String> result = new ArrayList<>();
Iterator<MyEvent> iterator = elements.iterator();
while (iterator.hasNext()) {
result.add(iterator.next().toString());
}
out.collect("窗口" + start + " ~ " + end + "中共有" + count + "个元素,窗口闭合计算时,水位线处于:" + currentWatermark + " result: " + result);
}
}
}
(2)断点式水位线生成器
断点式水位线会不停地检测onEvent()中的事件,当发现带有水位线信息的特殊事件时,就立即发出水位线。一般不会通过onPeriodicEmit()发出水位线。
3. 水位线的传递
水位线是数据流中插入的一个标记,用来表示时间的进展,它会随着数据一起在任务间传递。
如果是直通式(forward)的传输,数据和水位线都是按照本身的顺序依次传递、依次处理的。一旦水位线到达了算子任务,这个子任务就会将它内部的时钟设为这个水位线的时间戳。
对于重分区(redistributing)模式,一个任务有可能来自不同分区上游子任务的数据。而不同分区的子任务时钟并不同步,比如某个任务收到上游两个任务不同的水位线分别为5s、7s。水位线的本质:当前时间之前的数据都已经到齐了,会取短板的为准,处理5s之前的数据。
用下图理解其水位线传递规则:
上游四个并行子任务,下游有三个并行子任务,所以会向三个分区发出水位线。
(1)当前任务自己的时钟就是所有分区时钟里最小的那个。
(2)传来4的时候,更新对应分区的时钟,然后然后对比四个分区最小的时钟是3,于是当当前的事件时钟更新到3,并且广播给所有下游子任务
(3)收到第二份分区的7,然后找到最小的还是3,没变化就不更新时钟
(4)收到第三个分区的6,对比最小是4,然后更新自己的时钟为4,并且向所有下游子任务广播自己的时钟
4. 总结
1.水位线在事件事件的世界里承担了时钟的角色;并且插入在数据流同数据一起流向下游子任务
2.水位线的默认计算公式:水位线=观察到的最大事件时间-最大延迟时间-1毫秒
3.在数据流开始之前,flink会插入一个大小是负无穷大(-Long.MAX_VALUE)的水位线;数据流结束时,插入一个正无穷大(Long.MAX_VALUE)的水位线,以保证所有的窗口闭合以及所有的定时器都被处罚。
总结:
- 核心概念
1.flink中对时间的处理满足前开后闭,包含前面不包含后面。且窗口时间、事件时间的单位都是ms。
2.时间语义:事件时间(时间自带的时间戳)、处理时间(当前处理事件的服务器的时间)、摄入时间(进入Fflink流的时间)。时间用于窗口函数中基于时间的窗口函数。可以参考: org.apache.flink.streaming.api.TimeCharacteristic
3.窗口:处理无界流的关键,时间窗口有开始时间(start_time)和结束时间(end_time), 这个时间是系统时间。可以理解为用窗口将数据划分成定长的buckets分开计算。
4.水位线:可以理解为是插入数据流的一条特殊数据,不存在与每条数据。水位线在不断的变化,一旦大于某个window的end_time,就会触发此window的计算,水位线就是用来触发window计算的。
- 用水位线处理乱序流的流程
假设设置的10s的时间窗口(window),那么0-10s,10-20s 都是一个窗口。以0-10为例子,0为开始时间、10位结束时间。假设四条数据的事件时间分别为: A(8)B(12.5)C(9)D(13.5)。 设置水位线延迟时间为事件时间减去3.5s。下面分析四条数据到达的时候其过程:
A:watermark = max{8}-3.5=4.5, 不会触发计算
B:watermark = max{8, 12.5}-3.5=9 不会触发计算
C:watermark = max{12.5, 9}-3.5=9 不会触发计算
D:watermark = max{12.5, 13.5}-3.5=10,触发计算。会将AC都计算进去,因为他们都小于10。