【Flink】水印

891 阅读5分钟

「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战

一、概述

而当在流式计算环境中数据从 Source 产生,再到转换和输出,这个过程由于网络和反压的原因会导致消息乱序。

因此,需要有一个机制来解决这个问题,这个特别的机制就是“水印”。



二、 水印(WaterMark

WaterMark 在正常的英文翻译中是水位,但是在 Flink 框架中,翻译为“水位线”更为合理,它在本质上是一个时间戳。

Flink 中的时间:

  • EventTime (事件时间):每条数据都携带时间戳;

    事件发生的时间, 例如: 点击网站上的某个链接的时间, 每一条日志都会记录自己的生成时间。

  • ProcessingTime (处理时间):数据不携带任何时间戳的信息;

    某个 Flink节点执行某个 operation 的时间, 例如: timeWindow 处理数据时的系统时间, 默认的时间属性就是 Processing Time

  • IngestionTime (摄入时间):和 EventTime 类似,不同的是 Flink 会使用系统时间作为时间戳绑定到每条数据,可以防止 Flink 内部处理数据是发生乱序的情况,但无法解决数据到达 Flink 之前发生的乱序问题。

    数据进入 Flink 的时间, 如某个 Flink 节点的 source operator 接收到数据的时间, 例如 : 某个source 消费到 kafka 中的数据。

所以,在处理消息乱序的情况时,会用 EventTimeWaterMark 进行配合使用。

如图: time1.png

(1)水印的本质

水印的出现是为了 解决实时计算中的数据乱序问题,它的本质是 DataStream 中一个带有时间戳的元素。

如果 Flink 系统中出现了一个 WaterMark T,那么就意味着 EventTime < T 的数据都已经到达,窗口的结束时间和 T 相同的那个窗口被触发进行计算了。

也就是说:水印是 Flink 判断迟到数据的标准,同时也是窗口触发的标记。

在程序并行度大于 1 的情况下,会有多个流产生水印和窗口,这时候 Flink 会选取时间戳最小的水印。

(2)水印如何生成

Flink 提供了 assignTimestampsAndWatermarks() 方法来实现水印的提取和指定,该方法接受的入参有 AssignerWithPeriodicWatermarksAssignerWithPunctuatedWatermarks 两种。

(3)水印种类

种类分为:

  1. 周期性水印(Periodic WaterMark
  2. 间歇性水印(Punctuated Watermark
  3. 递增式水印(Assigner With Ascending Timestamp

1)周期性水印(Periodic WaterMark

周期性水印:根据事件或处理时间周期性地触发水印生成器(Assigner),两个水印时间戳之间并不一定具有固定时间间隔。

2)间歇性水印(Punctuated Watermark

间歇性水印:在观察到事件后,会计算某个条件来决定是否发射水印。

举个简单的例子,假如发现接收到的数据 MyData 中以字符串 watermark 开头则产生一个水印:

data.assignTimestampsAndWatermarks(new AssignerWithPunctuatedWatermarks<UserActionRecord>() {

      @Override
      public Watermark checkAndGetNextWatermark(MyData data, long l) {
        return data.getRecord.startsWith("watermark") ? new Watermark(l) : null;
      }

      @Override
      public long extractTimestamp(MyData data, long l) {
        return data.getTimestamp();
      }
    });

class MyData{
    private String record;
    private Long timestamp;
    public String getRecord() {
        return record;
    }
    public void setRecord(String record) {
        this.record = record;
    }
    public Timestamp getTimestamp() {
        return timestamp;
    }
    public void setTimestamp(Timestamp timestamp) {
        this.timestamp = timestamp;
    }
}

3)递增式水印(Assigner With Ascending Timestamp

递增式水印:生成完美水印,用于顺序的无界数据集。

这水印的含义:小于其时间戳的事件均以送达且大于其时间戳的事件还没有被观察到。

举个栗子:

在使用 Kafka 作为数据源时,每个分区的消息时间通常是递增的,但 Source 节点从多个消息分区并行拉取数据时这种时间特征会被破坏。 这时可以在连接器端创建 Kafka 分区水印(Kafka-partition-aware watermark),以确保多分区消息的升序排列。

三、案例

步骤:

  1. 获取数据源
  2. 转化
  3. 声明水印( watermark )
  4. 分组聚合, 调用 window 的操作
  5. 保存处理结果

注意: 当使用 EventTimeWindow 时, 所有的 WindowEventTime 的时间轴上进行划分, 也就是说, 在 Window 启动后,会根据初始的 EventTime 时间每隔一段时间划分一个窗口, 如果 Window 大小是3秒, 那么1分钟内会把 Window 划分为如下的形式:

[00:00:00,00:00:03)
[00:00:03,00:00:06)
[00:00:03,00:00:09)
[00:00:03,00:00:12)
[00:00:03,00:00:15)
[00:00:03,00:00:18)
[00:00:03,00:00:21)
[00:00:03,00:00:24)

注意:

  1. 窗口是左闭右开的, 形式为: [window_start_time, window_end_time)

  2. Window 的设定基于第一条消息的事件时间, 也就是说, Window 会一直按照指定的时间间隔进行划分, 不论这个 Window 中有没有数据, EventTime 在这个 Window 期间的数据会进入这个 Window

  3. Window 会不断产生, 属于这个 Window 范围的数据会被不断加入到 Window 中, 所有未被触发的 Window 都会等待触发, 只要 Window 还没触发, 属于这个 Window 范围的数据就会一直被加入到 Window 中, 直到 Window 被触发才会停止数据的追加, 而当 Window 触发之后才接受到的属于被触发 Window 的数据会被丢弃。

  4. Window 会在以下的条件满足时被触发执行:

    • [window_start_time,window_end_time) 窗口中有数据存在
    • watermark 时间 >= window_end_time;
  5. 一般会设置水印时间, 比事件时间小几秒钟, 表示最大允许数据延迟达到多久 (即,水印时间 = 事件时间 - 允许延迟时间)

    当接收到的 水印时间 >= 窗口结束时间且 窗口内有数据, 则触发计算 (即,事件时间 - 允许延迟时间 >= 窗口结束时间 或 事件时间 >= 窗口结束时间 + 允许延迟时间)