Flink 实时计算中的 Watermark 机制| 青训营笔记

646 阅读10分钟

这是我参与「第四届青训营 」笔记创作活动的第8天。

1 Event time 和 Watermark 的关系

1.1 Event time 和 Processing time介绍

Event time 事件时间和Processing time 处理时间主要区别是产生时间不同,前者是事件的实际发生时间,后者是机器的系统处理时间,如下图所示。

image.png

① Event time 事件时间事件在其设备上发生的时间

Event time 是事件在进入 Flink 之前已经嵌入到记录的时间,其大小取决于事件本身,与网络延时、系统时区等因素无关。

② Processing time 处理时间:作业正在执行相应操作机器系统时间

Processing time 提供了最佳的性能和最低的延迟,但是不能提供确定性,即计算结果是不确定的。 例如,时间窗口为5min的求和统计,应用程序在 9:00 开始运行,则第一个时间窗口处理 [9:00, 9:05) 的事件,下一个窗口处理 [9:05, 9:10) 的事件,依此类推。通信延迟、作业故障重启等问题,可能导致窗口的计算结果是不一样的。如下图所示,假设事件(事件时间, 数值) 遇到上述问题,场景一:事件 B 有网络延迟落在[9:10, 9:15),场景二:作业故障重启导致事件 A 和事件 B落在[9:10, 9:15)。

64d167123de149fe394bc75319debdb.jpg

1.2 Event time 和 Watermark

问题:Flink 支持事件时间,如何测量事件时间的进度? 例如,5min 的事件时间窗口,当事件时间超过 5min 时,需要通知 Flink 触发窗口计算。 解答:Watermark 机制

Watermark 本质是时间戳,与业务数据一样无差别地传递下去,目的是衡量事件时间的进度(通知 Flink 触发事件时间相关的操作,例如窗口)。

说明: Watermark(T) 表示目前系统的时间事件是 T,即系统后续没有 T'<T 的事件即 Event(T'<T)

/**
 * 1.Watermark 是一个时间戳, 它表示小于该时间戳的事件都已经到达了。
 * 2.Watermark 一般情况在源位置产生(也可以在流图中的其它节点产生), 通过流图节点传播。
 * 3.Watermark 也是 StreamElement, 和普通数据一起在算子之间传递。
 * 4.Watermark 可以触发窗口计算, 时间戳为 Long.MAX_VALUE 表示算子后续没有任何数据。
 */
public final class Watermark extends StreamElement {
    // 省略...

    /**
     * The timestamp of the watermark in milliseconds.
     */
    private final long timestamp;

    /**
     * Creates a new watermark with the given timestamp in milliseconds.
     */
    public Watermark(long timestamp) {
        this.timestamp = timestamp;
    }

    /**
     * Returns the timestamp associated with this {@link Watermark} in milliseconds.
     */
    public long getTimestamp() {
        return timestamp;
    }
       // 省略...
}

换言之Watermark表示已经收集完毕的数据的最大 event time,换句话说 event time 小于 Watermark 的数据不应该再出现,基于这个前提我们才有可能将 event time 窗口视为完整并输出结果。Watermark 设计的初衷是处理 event time 和 processing time 之间的延迟问题,三者的关系可以用下图展示:

  • 理想的情况下数据没有延迟,因此 processing time 是等于 event time 的,理想的 Watermark 应该是斜率为 45 度的直线。

  • 然而在真实环境下,processing time 和 event time 之间总有不确定的延迟,表现出来的 Watermark 会类似图 1 中的红色的曲线。

其中红色曲线与理想 Watermark 的纵坐标差值称为 processing-time lag,表示在真实世界中的数据延迟,而横坐标的差值表示 event-time skew,表示该延迟带来的 event-time 落后量。

Watermark 通常是基于已经观察到的数据的 event time 来判断(当然也可以引入 processing time 或者其他外部参数),具体需要用户根据数据流的 event time 特征来决定,比如最简单的算法就是取目前为止观察到的最大 event time。

在数据流真实 event time 曲线是单调非减的情况下,比如 event time 是 Kafka producer timestamp 时,我们是可以计算出完美符合实际的 Watermark 的,然而绝大多数情况下数据流的 event time 都是乱序的,因此计算完美的 Watermark 是不现实的(实际上也是没有必要的),通常我们会以启发性的 Watermark 算法来代替。

启发性的 Watermark 算法目的在于在计算结果的延迟和准确性之间找到平衡点。

如果采用激进的 Watermark 算法,那么 Watermark 会快于真实的 event time,导致在窗口数据还不完整的情况下过早输地出计算结果,影响数据的准确性;如果采用保守的 Watermark 算法,那么 Watermark 会落后于真实的 event time,导致窗口数据收集完整后不能及时输出计算结果,造成数据的延迟。

实际上上文所说的 Watermark 取观察到的最大 event time 和批处理使用的设置一个足够大的安全延迟的办法分别就属于 Watermark 算法的两个极端。

很多情况下用户偏向于牺牲一定的延时来换取准确性,不过在像金融行业的欺诈检测场景中,低延迟是首要的,否则准确性再高也没有意义。

针对这种情况 The Dataflow Model 提供了 Allow Lateness 的机制,工作的原理是用户可以设置一个 event time 阈值,如果在计算结果输出后再接收到迟到的数据,且 event time 与当前 Watermark 的差值在阈值以内,计算结果会被重新计算和输出,但超出这个阈值的迟到数据就会被丢弃。

这时你们可以看到要开发一个高质量的实时作业是多么不易了,这也是很多实时应用开发者最为头疼的地方,或许以后利用机器学习去计算 Watermark 是个不错的主意。

解释一下 event time 的顺序和乱序概念

如下图所示,事件 Event 是按照事件时间 EventTime 顺序上报的。

498433f6a1b8c7cc1035f3da28f0d08.jpg

如下图所示,事件 Event 是不按照事件时间 EventTime 乱序上报的。

d3b9e52a947138f20923bb334e8e463.jpg

2 Watermark 的产生

2.1 Watermark 类型

说明:flink-1.12 支持 WatermarkStrategy 和 WatermarkGenerator

flink 采用 WatermarkStrategy 设置自定义 Watermark 类型,WatermarkGenerator 是 Watermark 的基类。flink 实现了 Punctuated Watermarks 从事件获取事件的时间戳、Periodic Watermarks 周期获取事件的时间戳。

/**
 * The {@code WatermarkGenerator} generates watermarks either based on events or
 * periodically (in a fixed interval).
 *
 * <p><b>Note:</b> This WatermarkGenerator subsumes the previous distinction between the
 * {@code AssignerWithPunctuatedWatermarks} and the {@code AssignerWithPeriodicWatermarks}.
 */
@Public
public interface WatermarkGenerator<T> {

    /**
     * 从事件获取事件的时间戳
     */
    void onEvent(T event, long eventTimestamp, WatermarkOutput output);

    /**
     * 周期获取事件的时间戳
     */
    void onPeriodicEmit(WatermarkOutput output);
}

使用 WatermarkStrategy 的样例,如下代码。

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<String> input = env.fromElements("data");

        // 使用 WatermarkStrategy 设置 Watermark 类型
        input.assignTimestampsAndWatermarks(
                WatermarkStrategy
                        .forBoundedOutOfOrderness(Duration.ofMillis(10)));

2.2 Watermark 的产生

Watermark 是算子 TimestampsAndWatermarksOperator 产生的,WatermarkStrategy 相当于 UDFFunction(封装于TimestampsAndWatermarksOperator 内部)。processElement 方法实现事件产生 Watermark,processWatermark 方法阻断上游传过来的 Watermark,onProcessingTime 方法实现周期产生 Watermark。

public class TimestampsAndWatermarksOperator<T>
        extends AbstractStreamOperator<T>
        implements OneInputStreamOperator<T, T>, ProcessingTimeCallback {
// 省略...
    @Override
    public void processElement(final StreamRecord<T> element) throws Exception {
        final T event = element.getValue();
        final long previousTimestamp = element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE;
        final long newTimestamp = timestampAssigner.extractTimestamp(event, previousTimestamp);

        element.setTimestamp(newTimestamp);
        output.collect(element);
        // 事件产生 Watermark
        watermarkGenerator.onEvent(event, newTimestamp, wmOutput);
    }

    // 阻断上游传过来的 watermark
    @Override
    public void processWatermark(org.apache.flink.streaming.api.watermark.Watermark mark) throws Exception {
        // if we receive a Long.MAX_VALUE watermark we forward it since it is used
        // to signal the end of input and to not block watermark progress downstream
        if (mark.getTimestamp() == Long.MAX_VALUE) {
            wmOutput.emitWatermark(Watermark.MAX_WATERMARK);
        }
    }

    @Override
    public void onProcessingTime(long timestamp) throws Exception {
        // 采用定时器, 周期产生 Watermark
        watermarkGenerator.onPeriodicEmit(wmOutput);

        final long now = getProcessingTimeService().getCurrentProcessingTime();
        // 更新定时器
        getProcessingTimeService().registerTimer(now + watermarkInterval, this);
    }
// 省略...
}

(1)Watermark 周期产生

public class TimePeriodicWatermarkGenerator implements WatermarkGenerator<MyEvent> {

    private final long maxTimeLag = 5000; // 5 seconds

    @Override
    public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
        // don't need to do anything because we work on processing time
    }

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        output.emitWatermark(new Watermark(System.currentTimeMillis() - maxTimeLag));
    }
}

结合算子 TimestampsAndWatermarksOperator 和 TimePeriodicWatermarkGenerator,分析 Watermark 的产生流程。如下图所示,横轴表示 processing time,圆形表示事件,圆形中的时间 t 表示事件时间,圆形落在横轴表示事件在算子中的处理,其中 Watermark 的产生周期为 60s 和允许延迟时间为 10s。以第一个周期 [0,60) 为例,获取事件中的最大事件时间 max,向下游发送 watermark(最大事件时间 - 允许延迟时间 - 1)

33d9186895c8414b32f779587411fd7.jpg

(2)Watermark 事件产生

public class PunctuatedAssigner implements WatermarkGenerator<MyEvent> {

    @Override
    public void onEvent(MyEvent event, long eventTimestamp, WatermarkOutput output) {
        if (event.hasWatermarkMarker()) {
            output.emitWatermark(new Watermark(event.getWatermarkTimestamp()));
        }
    }

    @Override
    public void onPeriodicEmit(WatermarkOutput output) {
        // don't need to do anything because we emit in reaction to events above
    }
}

3 Watermark 的传递

Watermark 的传递方式是广播,即广播方式发送到下游。Watermark 与业务数据一样,无差别地传递下去。

9313069c7563007ba4a44e599bb00c4.jpg

例子:多并发的场景下,Watermark 是 source task 产生,经过 keyby 分组后触发窗口计算。 说明:① Watermark 要单调递增。② 如果算子有多个上游(广播)即输入多个 Watermark(T),则该算子取最小 Watermark 即 min(Watermark(T1), Watermark(T2))

image.png

从 WindowOperator 源码分析窗口是如何传递 Watermark。 首先分析 WindowOperator 类图,可知 WindowOperator 间接继承AbstractStreamOperator,而 AbstractStreamOperator 实现了接口 Input 的 processWatermark 方法、接口 TwoInputStreamOperator 的 processWatermark1 方法 和 processWatermark2 方法。

接着分析一下 AbstractStreamOperator 实现的 processWatermark 、processWatermark1 和 processWatermark2。

// 省略 ....
    public void processWatermark(Watermark mark) throws Exception {
        if (timeServiceManager != null) {
            timeServiceManager.advanceWatermark(mark);
        }
        // 发送 watermark
        output.emitWatermark(mark);
    }

    /**
     * 2个上游的watermark
     * 计算最小watermark, 并设置为当前算子的watermark
     */
    public void processWatermark1(Watermark mark) throws Exception {
        input1Watermark = mark.getTimestamp();
        long newMin = Math.min(input1Watermark, input2Watermark);
        if (newMin > combinedWatermark) {
            combinedWatermark = newMin;
            processWatermark(new Watermark(combinedWatermark));
        }
    }

    /**
     * 2个上游的watermark
     * 计算最小watermark, 并设置为当前算子的watermark
     */
    public void processWatermark2(Watermark mark) throws Exception {
        input2Watermark = mark.getTimestamp();
        long newMin = Math.min(input1Watermark, input2Watermark);
        if (newMin > combinedWatermark) {
            combinedWatermark = newMin;
            processWatermark(new Watermark(combinedWatermark));
        }
    }
// 省略 ....

4 关于Watermark的一些常见问题

  • 怎么观察一个任务中的watermark是多少,是否是正常的

    • 一般通过Flink Web UI上的信息来观察当前任务的watermark情况
    • 这个问题是生产实践中最容易遇到的问题,大家在开发事件时间的窗口任务的时候,经常会忘记了设置watermark,或者数据太少,watermark没有及时的更新,导致窗口一直不能触发。
  • Per-partition / Per-subtask 生成watermark的优缺点

    • 在Flink里早期都是per-subtask的方式进行watermark的生成,这种方式比较简单。但是如果每个source task如果有消费多个partition的情况的话,那多个partition之间的数据可能会因为消费的速度不同而最终导致数据的乱序程度增加。
    • 后期(上面图中)就逐步的变成了per-partition的方式来产生watermark,来避免上面的问题。
  • 如果有部分partition/subtask会断流,应该如何处理

    • 数据断流是很常见的问题,有时候是业务数据本身就有这种特点,比如白天有数据,晚上没有数据。在这种情况下,watermark默认是不会更新的,因为它要取上游subtask发来的watermark中的最小值。此时我们可以用一种IDLE状态来标记这种subtask,被标记为这种状态的subtask,我们在计算watermark的时候,可以把它先排除在外。这样就可以保证有部分partition断流的时候,watermark仍然可以继续更新。
  • 算子对于时间晚于watermark的数据的处理

    • 对于迟到数据,不同的算子对于这种情况的处理可以有不同的实现(主要是根据算子本身的语义来决定的)
    • 比如window对于迟到的数据,默认就是丢弃;比如双流join,对于迟到数据,可以认为是无法与之前正常数据join上。