flink 时间语义、水位线(Watermark)、生成水位线、水位线的传递


1. Flink 中的时间和窗口

1.1 时间语义

  在flink中,当希望对数据按照时间窗口来进行收集计算时,时间的衡量标准就非常重要
在这里插入图片描述
  如图:在事件发生之后,生成的数据被收集起来,首先进入分布式消息队列,然后被 Flink 系统中的 Source 算子读取消费,进而向下游的转换算子(窗口算子)传递,最终由窗口算子进行计算处理。
  在这个过程中,有两个非常重要的时间点:一个是数据产生的时间,把它叫作事件时间(Event Time);另一个是数据真正被处理的时刻,叫作处理时间(Processing Time);窗口操作以那种时间作为衡量标准,就是所谓的时间语义(Notions of Time)。由于分布式系统中网络传输的延迟和时钟漂移,处理时间相对事件发生的时间会有所滞后

注:
  在事件时间语义下,对于时间的衡量,不再依赖于任何机器的系统时间,而是依赖于数据本身(Timestamp作为一个属性嵌入到数据中)。简单来说,这相当于任务处理的时候自己本身是没有时钟的,所以只好来一个数据就问一下“现在几点了”;而数据本身也没有表,只有一个自带的“出厂时间”,于是任务就基于这个时间来确定自己的时钟。由于流处理中数据是源源不断产生的,一般来说,先产生的数据也会先被处理,所以当任务不停地接到数据时,它们的时间戳也基本上是不断增长的,就可以代表时间的推进。
  当然这里有个前提,就是先产生的数据先被处理,这要求保证数据到达的顺序。但是由于分布式系统中网络传输延迟的不确定性,实际应用中数据流往往是乱序的。在这种情况下,就不能简单地把数据自带的时间戳当作时钟了,而需要用另外的标志来表示事件时间进展,在 Flink 中把它叫作事件时间的水位线(Watermarks)。

1.2 两种时间语义的对比

  在分布式环境中,处理时间其实是不确定的,各个并行任务时钟不统一;而且由于网络延迟,导致数据到达各个算子任务的时间有快有慢,对于窗口操作就可能收集不到正确的数据了,数据处理的顺序也会被打乱。这就会影响到计算结果的正确性。所以处理时间语义,一般用在对实时性要求极高、而对计算准确性要求不太高的场景
  而在事件时间语义下,水位线成为了时钟,可以统一控制时间的进度。这就保证将数据划分到正确的窗口中,比如 8 点 59 分 59 秒产生的数据,无论网络传输的延迟是多少,它永远属于 8 点~9 点的窗口,不会错分。但数据还可能是乱序的,要想让窗口正确地收集到所有数据,就必须等这些错乱的数据都到齐,这就需要一定的等待时间。所以整体上看,事件时间语义是以一定延迟为代价,换来了处理结果的正确性。由于网络延迟一般只有毫秒级,所以即使是事件时间语义,同样可以完成低延迟实时流处理的任务。
  另外,除了事件时间和处理时间,Flink 还有一个摄入时间(Ingestion Time)的概念,它是指数据进入 Flink 数据流的时间,也就是 Source 算子读入数据的时间。摄入时间相当于是事件时间和处理时间的一个中和,它是把 Source 任务的处理时间,当作了数据的产生时间添加到数据里。这样一来,水位线(watermark)就基于这个时间直接生成,不需要单独指定了。这种时间语义可以保证比较好的正确性,同时又不会引入太大的延迟。它的具体行为跟事件时间非常像,可以当作特殊的事件时间来处理。
  在 Flink 中,由于处理时间比较简单,早期版本默认的时间语义是处理时间;而考虑到事件时间在实际应用中更为广泛,从 1.12 版本开始,Flink 已经将事件时间作为了默认的时间语义


2. 水位线(Watermark)

2.1 事件时间和窗口

  事件时间语义下,基于数据的时间戳,自定义了一个逻辑时钟。这个时钟的时间不会自动流逝;它的时间进展,就是靠着新到数据的时间戳来推动的。计算的过程可以完全不依赖处理时间(系统时间),不论什么时候进行统计处理,得到的结果都是正确的。
在这里插入图片描述
  如图:事件时间语义类似于物流发车,到达车上的商品,生产时间是8 点 05 分,那么当前车上的时间就是 8 点 05 分;又来了一个 8 点 10 分生产的商品,现在车上的时间就是 8 点 10 分。直接用数据的时间戳来指示当前的时间进展,窗口的关闭自然以数据的时间戳等于窗口结束时间为准,这就不受网络传输延迟的影响了。对于8 点 59 分 59 秒生产出来的商品,到车上的时候不管实际时间(系统时间)是几点,当前时间就是 8 点 59 分 59 秒,所以它总是能赶上车的;而 9 点这班车,要等到 9 点整生产的商品到来,才认为时间到了 9 点,这时才正式发车。

2.2 水位线

  在数据流中加入一个时钟标记,记录当前的事件时间;这个标记可以直接广播到下游,当下游任务收到这个标记,就可以更新自己的时钟了。由于类似于水流中用来做标志的记号,在 Flink 中,这种用来衡量事
件时间(Event Time)进展的标记,就被称作水位线(Watermark)

2.2.1 有序流中的水位线

  在理想状态下,数据应该按照它们生成的先后顺序、排好队进入流中;也就是说,它们处理的过程会保持原先的顺序不变,遵守先来后到的原则。从每个数据中提取时间戳,就可以保证总是从小到大增长的,从而插入的水位线也会不断增长、事件时钟不断向前推进。
  实际应用中,如果当前数据量非常大,可能会有很多数据的时间戳是相同的,这时每来一条数据就提取时间戳、插入水位线就做了大量的无用功。而且即使时间戳不同,同时涌来的数据时间差会非常小(比如几毫秒),往往对处理计算也没什么影响。所以为了提高效率,一般会每隔一段时间生成一个水位线,这个水位线的时间戳,就是当前最新数据的时间戳
在这里插入图片描述
注意:对于水位线的周期性生成,周期时间是指处理时间(系统时间),而不是事件时间

2.2.2 乱序流中的水位线

  乱序指数据的先后顺序不一致,主要是基于数据的产生时间而言的
在这里插入图片描述

  乱序流中的水位线,插入新的水位线时,要先判断一下时间戳是否比之前的大,否则就不再生成新的水位线
在这里插入图片描述
  如果考虑到大量数据同时到来的处理效率,同样可以周期性地生成水位线。这时只需要保存一下之前所有数据中的最大时间戳,需要插入水位线时,就直接以它作为时间戳生成新的水位线
在这里插入图片描述
  对于迟到数据,为了让窗口能够正确收集到迟到的数据,可以等上若干秒;即用当前已有数据的最大时间戳减去 若干秒,就是要插入的水位线的时间戳,如图水位线设置延迟2秒

在这里插入图片描述

注:
  一个窗口所收集的数据,并不是之前所有已经到达的数据。因为数据属于哪个窗口,是由数据本身的时间戳决定的,一个窗口只会收集真正属于它的那些数据。如图尽管水位线 W(20)之前有时间戳为 22 的数据到来,10~20 秒的窗口中也不会收集这个数据,进行计算依然可以得到正确的结果
在这里插入图片描述

2.3 水位线特点

(1)水位线是插入到数据流中的一个标志
(2)水位线主要内容是一个时间戳,用来表示当前事件时间的进展
(3)水位线是基于数据的时间戳产生的
(4)水位线的时间戳必须是单调递增的,来保障,以确保任务的事件时间时钟向前推进
(5)水位线可以设置延迟,来保证正确处理乱序数据
(6)一个水位线 Watermark(t),表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据


3. 如何生成水位线

3.1 生成水位线的总体原则

  由于网络传输的延迟不确定,为了获取所有迟到数据,只能设置延迟时间。但等待的时间越长,处理的实时性越低。因此可以单独创建一个 Flink 作业来监控事件流,建立概率分布或者机器学习模型,学习事件的迟到规律。得到分布规律之后,就可以选择置信区间来确定延迟,作为水位线的生成策略了。如,数据的迟到时间服从μ=1,σ=1 的正态分布,那么设置水位线延迟为 3 秒,就可以保证至少 97.7%的数据可以正确处理

3.2 水位线生成策略(Watermark Strategies)

  在 Flink 的 DataStream API 中 , 有 一 个 单 独 用 于 生 成 水 位 线 的 方法:.assignTimestampsAndWatermarks(),它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间:

stream.assignTimestampsAndWatermarks()

在这里插入图片描述
  .assignTimestampsAndWatermarks()方法需要传入一个 WatermarkStrategy 作为参数,这就是 所 谓 的 水 位 线 生 成 策 略 WatermarkStrategy 中 包 含 了 一 个 时 间 戳 分 配器TimestampAssigner 和一个水位线生成器WatermarkGenerator

TimestampAssigner:主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。时间戳的分配是生成水位线的基础

WatermarkGenerator:主要负责按照既定的方式,基于时间戳生成水位线。在WatermarkGenerator 接口中,主要又有两个方法:onEvent()和 onPeriodicEmit()

在这里插入图片描述

onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,以及允许发出水位线的一个 WatermarkOutput,可以基于事件做各种操作

onPeriodicEmit:周期性调用的方法,可以由 WatermarkOutput 发出水位线。周期时间为处理时间,可以调用环境配置的.setAutoWatermarkInterval()方法来设置,默认为200ms

env.getConfig().setAutoWatermarkInterval(60 * 1000L);

3.3 Flink 内置水位线生成器

3.3.1 有序流

  调用WatermarkStrategy.forMonotonousTimestamps()方法就可以实现,直接拿当前最大的时间戳作为水位线

在这里插入图片描述

  调用.withTimestampAssigner()方法,将数据中的 timestamp 字段提取出来,作为时间戳分配给数据元素;然后用内置的有序流水位线生成器构造出了生成策略


3.3.2 乱序流

  由于乱序流中需要等待迟到数据到齐,所以必须设置一个固定量的延迟时间(Fixed Amount of Lateness)。这时生成水位线的时间戳,就是当前数据流中最大的时间戳减去延迟的结果,相当于把表调慢,当前时钟会滞后于数据的最大时间戳。调用 WatermarkStrategy. forBoundedOutOfOrderness()方法就可以实现。这个方法需要传入一个 maxOutOfOrderness 参数,表示最大乱序程度,它表示数据流中乱序数据时间戳的最大差值
在这里插入图片描述

注:
  事实上,有序流的水位线生成器本质上和乱序流是一样的,相当于延迟设为 0 的乱序流水位线生成器,两者完全等同:

WatermarkStrategy.forMonotonousTimestamps()
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0))

注:
  乱序流中生成的水位线真正的时间戳,其实是 当前最大时间戳 – 延迟时间 – 1,单位毫秒。为什么要减 1 毫秒呢?时间戳为 t 的水位线,表示时间戳≤t 的数据全部到齐,不会再来了。如果考虑有序流,也就是延迟时间为 0 的情况,那么时间戳为 7 秒的数据到来时,之后其实是还有可能继续来 7 秒的数据的;所以生成的水位线不是 7 秒,而是 6 秒 999 毫秒,7 秒的数据还可以继续来

BoundedOutOfOrdernessWatermarks源码:
在这里插入图片描述

3.4 自定义水位线策略

(1)周期性水位线生成器(Periodic Generator)
  周期性生成器一般是通过 onEvent()观察判断输入的事件,而在 onPeriodicEmit()里发出水
位线

// 自定义水位线的产生
public class CustomWatermarkTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
                StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);
        env
                .addSource(new ClickSource())
                .assignTimestampsAndWatermarks(new CustomWatermarkStrategy())
                .print();
        env.execute();
    }

    public static class CustomWatermarkStrategy implements WatermarkStrategy<Event> {
        @Override
        public TimestampAssigner<Event> createTimestampAssigner(TimestampAssignerSupplier.Context context) {
            return new SerializableTimestampAssigner<Event>() {
                @Override
                public long extractTimestamp(Event element, long recordTimestamp) {
                    return element.timestamp; // 告诉程序数据源里的时间戳是哪一个字段
                }
            };
        }

        @Override
        public WatermarkGenerator<Event> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context) {
            return new CustomPeriodicGenerator();
        }
    }

    public static class CustomPeriodicGenerator implements WatermarkGenerator<Event> {
        private Long delayTime = 5000L; // 延迟时间
        private Long maxTs = Long.MIN_VALUE + delayTime + 1L; // 观察到的最大时间戳

        @Override
        public void onEvent(Event 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));
        }
    }
}

(2)断点式水位线生成器(Punctuated Generator)
  断点式生成器会不停地检测 onEvent()中的事件,当发现带有水位线信息的特殊事件时,就立即发出水位线。一般来说,断点式生成器不会通过 onPeriodicEmit()发出水位线

    public class CustomPunctuatedGenerator implements WatermarkGenerator<Event> {
        @Override
        public void onEvent(Event r, long eventTimestamp, WatermarkOutput output) {
            // 只有在遇到特定的 itemId 时,才发出水位线
            if (r.user.equals("Mary")) {
                output.emitWatermark(new Watermark(r.timestamp - 1));
            }
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 不需要做任何事情,因为我们在 onEvent 方法中发射了水位线
        }
    }

4. 水位线的传递

  上游任务处理完水位线、时钟改变之后,要把当前的水位线再次发出,广播给所有的下游子任务。这样,后续任务就不需要依赖原始数据中的时间戳(经过转化处理后,数据可能已经改变了),也可以知道当前事件时间

  在重分区(redistributing)的传输模式下
在这里插入图片描述
  当前任务的上游,有四个并行子任务,所以会接收到来自四个分区的水位线;而下游有三
个并行子任务,所以会向三个分区发出水位线。
具体过程如下:
  (1)上游并行子任务发来不同的水位线,当前任务会为每一个分区设置一个分区水位线(Partition Watermark),这是一个分区时钟;而当前任务的时钟,就是所有分区时钟里最小的那个
  (2)当有一个新的水位线(第一分区的 4)从上游传来时,当前任务会首先更新对应的分区时钟;然后再判断所有分区时钟中的最小值,如果比之前大,说明事件时间有了进展,当前任务的时钟也就可以更新了。这里要注意,更新后的任务时钟,并不一定是新来的那个分区水位线,比如这里改变的是第一分区的时钟,但最小的分区时钟是第三分区的 3,于是当前任务时钟就推进到了 3。当时钟有进展时,当前任务就会将自己的时钟以水位线的形式,广播给下游所有子任务
  (3)再次收到新的水位线(第二分区的 7)后,执行同样的处理流程。首先将第二个分区时钟更新为 7,然后比较所有分区时钟;发现最小值没有变化,那么当前任务的时钟也不变,也不会向下游任务发出水位线。
  (4)同样道理,当又一次收到新的水位线(第三分区的 6)之后,第三个分区时钟更新为6,同时所有分区时钟最小值变成了第一分区的 4,所以当前任务的时钟推进到 4,并发出时间戳为 4 的水位线,广播到下游各个分区任务

总之,每个任务都以处理完之前所有数据为标准来确定自己的时钟,就可以保证窗口处理的结果总是正确的;
水位线的默认计算公式:水位线 = 观察到的最大事件时间 – 最大延迟时间 – 1 毫秒

注意:
  在数据流开始之前,Flink 会插入一个大小是负无穷大(在 Java 中是-Long.MAX_VALUE)的水位线,而在数据流结束时,Flink 会插入一个正无穷大(Long.MAX_VALUE)的水位线,保证所有的窗口闭合以及所有的定时器都被触发


  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
上百节课视频详细讲解,需要的小伙伴自行百度网盘下载,链接见附件,永久有效。 课程亮点: 1.知识体系完备,从小白到大神各阶段读者均能学有所获。 2.生动形象,化繁为简,讲解通俗易懂。 3.结合工作实践及分析应用,培养解决实际问题的能力。 4.每一块知识点, 都有配套案例, 学习不再迷茫。 课程内容: 1.Flink框架简介 2.Flink集群搭建运维 3.Flink Dataset开发 4.Flink 广播变量,分布式缓存,累加器 5.Flink Datastream开发 6.Flink Window操作 7.Flink watermark与侧道输出 8.Flink状态计算 9.Flink容错checkpoint与一致性语义 10.Flink进阶 异步IO,背压,内存管理 11.Flink Table API与SQL 课程目录介绍 第一章 Flink简介 01.Flink的引入 02.什么是Flink 03.Flink流处理特性 04.Flink基石 05.批处理与流处理 第二章 Flink架构体系 01.Flink中重要角色 02.无界数据流与有界数据流 03.Flink数据流编程模型 04.Libraries支持 第三章 Flink集群搭建 01.环境准备工作 02.local模式 03.Standalone集群模式 04.Standalone-HA集群模式 05.Flink On Yarn模式-介绍 06.Flink On Yarn模式-准备工作 07.Flink On Yarn模式-提交方式-Session会话模式 08.Flink On Yarn模式-提交方式-Job分离模式 09. Flink运行架构-Flink程序结构 10. Flink运行架构-Flink并行数据流 11. Flink运行架构-Task和Operator chain 12. Flink运行架构-任务调度与执行 13. Flink运行架构-任务槽与槽共享 第四章 Dataset开发 01.入门案例 02.入门案例-构建工程、log4j.properties 03.入门案例-代码运行yarn模式运行 04.DataSource-基于集合 05.DataSource-基于文件 06.Transformation开发 07.Datasink-基于集合 08.Datasink-基于文件 09.执行模式-本地执行 10.执行模式-集群执行 11.广播变量 12.累加器 13.分布式缓存 14.扩展并行度的设置 第五章 DataStream开发 01.入门案例-流处理流程 02.入门案例-示例、参考代码 03.流处理常见Datasource 04.Datasource基于集合 05.Datasource基于文件 06.Datasource基于网络套接字 07.Datasource-自定义source-SourceFunction 08.Datasource-自定义source-ParallelSourceFunction 09.Datasource-自定义source-RichParallelSourceFunction 10.Datasource-自定义source-MysqlSource 11.Datasource-自定义source-KafkaSource 12.DataStream-transformations 13.DataSink-输出数据到本地文件 14.DataSink-输出数据到本地集合 15.DataSink-输出数据到HDFS 16.DataSink-输出数据到mysql,kafka,Redis 第六章 Flink中Window 01.为什么需要window 02.什么是window 03.Flink支持的窗口划分方式 04.Time-window之tumbling-time-window 05.Time-window之sliding-time-window 06.Time-window之session-window 07.Count-window之tumbling-count-window 08.Count-window之sliding-count-window 09.window-Apply函数 第七章 Eventime-watermark 01.时间分类 02.watermark之数据延迟产生 03.watermark之解决数据延迟到达 04.watermark综合案例 05.watermark之数据丢失 06.watermark+侧道输出保证数据不丢失 等等共十一章节
在 Apache Flink 中,可以使用自定义的 WatermarkGenerator 来生成水位线水位线用于衡量事件时间进展,帮助确定何时触发窗口计算。 首先,你需要实现 WatermarkGenerator 接口,并覆盖它的两个方法:getCurrentWatermark 和 onEvent。 ```java import org.apache.flink.streaming.api.functions.AssignerWithPunctuatedWatermarks; import org.apache.flink.streaming.api.watermark.Watermark; public class CustomWatermarkGenerator implements AssignerWithPunctuatedWatermarks<Event> { @Override public long extractTimestamp(Event event, long previousTimestamp) { return event.getTimestamp(); } @Override public Watermark checkAndGetNextWatermark(Event lastElement, long extractedTimestamp) { // 在这里根据需要实现水位线生成逻辑 // 返回一个 Watermark 对象,表示当前的水位线 // 可以使用事件中的时间戳进行计算 return new Watermark(extractedTimestamp - 5000); // 示例:设置水位线为事件时间减去 5 秒 } } ``` 然后,将自定义的 WatermarkGenerator 应用到你的 Flink 程序中: ```java DataStream<Event> events = ...; // 输入事件流 // 应用水位线生成器 DataStream<Event> eventsWithWatermarks = events.assignTimestampsAndWatermarks(new CustomWatermarkGenerator()); ``` 通过调用 `assignTimestampsAndWatermarks` 方法,并传入自定义的 WatermarkGenerator,即可将水位线应用到事件流上。 请注意,在 `CustomWatermarkGenerator` 中,`extractTimestamp` 方法用于从事件中提取时间戳,用于生成水位线。`checkAndGetNextWatermark` 方法在每个事件到达时被调用,可以根据事件的时间戳计算出水位线。示例中的水位线设置为事件时间减去 5 秒,你可以根据实际需求来实现水位线生成逻辑。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

但行益事莫问前程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值