【Flink】Flink 中的时间语义和水位线

目录

一、Flink里的时间

二、水位线(Watermark)

1、水位线的概念

        2、有序流中的水位线

3、乱序流中的水位线  

 4、水位线的特性

5、如何生成水位线

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

(2)Flink 内置水位线生成器

(3)自定义水位线策略

(4)在自定义数据源中发送水位线

6、水位线的传递

7、水位线总结


一、Flink里的时间

因为网络传输会有延迟,而且这延迟是不确定的,所以 JobManager 发出的同步信号无法同时到达所有节点;想要拥有一个全局统一的时钟,在分布式系统里是做不到的。
我们所定义的窗口操作,到底是以那种时间作为衡量标准,就是所谓的“时间语义
1. 处理时间 Processing Time
        数据真正被处理的时刻;一般用在对实时性要求极高、而对计算准确性要求不太高的场景。
2. 事件时间(Event Time):是指每个事件在对应的设备上发生的时间,也就是数据生成的时间。在事件时间语义下, 水位线成为了时钟,可以统一控制时间的进度。这就保证了我们总可以将数据划分到正确的窗口中。这种时间语义可以保证比较好的正确性,同时又不会引入太大的延迟。它的具体行为跟事件时间非常像,可以当作特殊的事件时间来处理。
在实际应用中,事件时间语义会更为常见。一般情况下,业务日志数据中都会记录数据生成的时间戳(timestamp ),它就可以作为事件时间的判断基础。

二、水位线(Watermark)

水位线,就是基于事件时间提出的概念。
在事件时间语义下的窗口,其实是基于数据的时间戳,自定义了一个“逻辑时钟”。这个时钟的时间不会自动流逝;它的时间进展,就是靠着新到数据的时间戳来推动的。这样的好处在于,计算的过程可以完全不依赖处理时间(系统时间),不论什么时候进行统计处理,得到的结果都是正确的。

1、水位线的概念

在事件时间语义下,我们不依赖系统时间,而是基于数据自带的时间戳去定义了一个时钟, 用来表示当前时间的进展。于是每个并行子任务都会有一个自己的逻辑时钟,它的前进是靠数据的时间戳来驱动的。我们应该把时钟也以数据的形式传递出去,告诉下游任务当前时间的进展;而且这个
时钟的传递不会因为窗口聚合之类的运算而停滞。
Flink 中,这种用来衡量事件时间(Event Time )进展的标记,就被称作“水位线
水位线可以看作一条特殊的数据记录,它是插入到数据流中的一个标记点,主要内容就是一个时间戳,用来指示当前的事件时间。而它插入流中的位置,就应该是在某个数据到来之后;这样就可以从这个数据中提取时间戳,作为当前水位线的时间戳了。

2、有序流中的水位线

 为了提高效率,一般会每隔一段时间生成一个水位线,这个水位线的时间戳,就是当前最新数据的时间戳。对于水位线的周期性生成,周期时间是指处理时间(系统时间),而不是事件时间。

3、乱序流中的水位线  

在分布式系统中,数据在节点间传输,会因为网络传输延迟的不确定性,导致顺序发生改变,这就是所谓的“乱序数据”。
我们插入新的水位线时,要先判断一下时间戳是否比之前的大,否则就不再生成新的水位线 。也就是说,只有数据的时间戳比当前时钟大,才能推动时钟前进,这时才插入水位线
如果考虑到大量数据同时到来的处理效率,我们同样可以周期性地生成水位线。这时只需要保存一下之前所有数据中的最大时间戳,需要插入水位线时,就直接以它作为时间戳生成新的水位线:

 但事实上,由于数据是乱序的,还可能有时间戳为 7 秒、8 秒的数据在 9 秒的数据之后才到来,这就是“迟到数据”(late data)。为了让窗口能够正确收集到迟到的数据,我们也可以等上 2 秒;也就是用当前已有数据的最大时间戳减去 2 秒,就是要插入的水位线的时间戳:

 由于水位线是周期性生成的,所以插入的位置不一定是在时间戳最大的数据后面:

 4、水位线的特性

  • 水位线是插入到数据流中的一个标记,可以认为是一个特殊的数据
  • 水位线主要的内容是一个时间戳,用来表示当前事件时间的进展
  • 水位线是基于数据的时间戳生成的
  • 水位线的时间戳必须单调递增,以确保任务的事件时间时钟一直向前推进
  • 水位线可以通过设置延迟,来保证正确处理乱序数据
  • 一个水位线 Watermark(t),表示在当前流中事件时间已经达到了时间戳 t, 这代表 t 之前的所有数据都到齐了,之后流中不会出现时间戳 t’ ≤ t 的数据
  • 水位线是 Flink 流处理中保证结果正确性的核心机制,它往往会跟窗口一起配合,完成对乱序数据的正确处理。

5、如何生成水位线

Flink 中的水位线,其实是流处理中对低延迟和结果正确性的一个权衡机制,而且把控制的权力交给了程序员,我们可以在代码中定义水位线的生成策略。如何确定延迟时间?
  • 依靠经验值
  • 可以单独创建一个 Flink 作业来监控事件流,建立概率分布或者机器学习模型,学习事件的迟到规律。得到分布规律之后,就可以选择置信区间来确定延迟,作为水位线的生成策略了。

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

原始的时间戳只是写入日志数据的一个字段,如果不提取出来并明确把它分配给数据, Flink 是无法知道数据真正产生的时间的。当然,有些时候数据源本身就提供了时间戳信息, 比如读取 Kafka 时,我们就可以从 Kafka 数据中直接获取时间戳,而不需要单独提取字段分配了。
用于生成水位线的方法:.assignTimestampsAndWatermarks() ,它主要用来为流中的数据分配时间戳,并生成水位线来指示事件时间:
DataStream<Event> withTimestampsAndWatermarks = 
stream.assignTimestampsAndWatermarks(<watermark strategy>);
.assignTimestampsAndWatermarks() 方法需要传入一个 WatermarkStrategy 作为参数,这就
是 所 谓 的 水 位 线 生 成 策 略 WatermarkStrategy 中 包 含 了 一 个 时 间 戳 分 配
”TimestampAssigner 和一个 水位线生成器”WatermarkGenerator
  • TimestampAssigner:主要负责从流中数据元素的某个字段中提取时间戳,并分配给元素。
  • WatermarkGenerator:主要负责按照既定的方式,基于时间戳生成水位线。主要又有两个方法:onEvent()onPeriodicEmit()
  • onEvent:每个事件(数据)到来都会调用的方法,它的参数有当前事件、时间戳,以及允许发出水位线的一个 WatermarkOutput,可以基于事件做各种操作
  • onPeriodicEmit:周期性调用的方法,可以由 WatermarkOutput 发出水位线。周期时间为处理时间,可以调用环境配置的.setAutoWatermarkInterval()方法来设置,默认为200ms

(2)Flink 内置水位线生成器

  • 有序流
有序流的主要特点就是时间戳单调增长 Monotonously Increasing Timestamps),所以永远不会出现迟到数据的问题。这是周期性生成水位线的最简单的场景,直接调用 WatermarkStrategy.forMonotonousTimestamps()方法就可以实现。简单来说,就是直接拿当前最大的时间戳作为水位线就可以了。需要注意的是,时间戳和水位线的单位必须都是毫秒
stream.assignTimestampsAndWatermarks(
 WatermarkStrategy.<Event>forMonotonousTimestamps()
     .withTimestampAssigner(new SerializableTimestampAssigner<Event>() 
        {
         @Override
         public long extractTimestamp(Event element, long recordTimestamp) 
            {
                 return element.timestamp;
             }
         })
);
  • 乱序流
由于乱序流中需要等待迟到数据到齐,所以必须设置一个固定量的延迟时间。这时生成水位线的时间戳,就是当前数据流中最大的时间戳减去延迟的结果,相当于把表调慢,当前时钟会滞后于数据的最大时间戳。调用 WatermarkStrategy. forBoundedOutOfOrderness()方法就可以实现。这个方法需要传入一个 maxOutOfOrderness 参数,表示“ 最大乱序程度 ,它表示数据流中乱序数据时间戳的最大差值。
env.addSource(new ClickSource())
 // 插入水位线的逻辑
 .assignTimestampsAndWatermarks(
     // 针对乱序流插入水位线,延迟时间设置为 5s
     WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
         .withTimestampAssigner(new SerializableTimestampAssigner<Event>() {
             // 抽取时间戳的逻辑
             @Override
             public long extractTimestamp(Event element, long recordTimestamp) {
                 return element.timestamp;
             }
          })
  ).print();
有序流的水位线生成器本质上和乱序流是一样的,相当于延迟设为 0 的乱序流水位线生成器,两者完全等同:
WatermarkStrategy.forMonotonousTimestamps()
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(0))
乱序流中生成的水位线真正的时间戳:
其实是 当前最大时间戳 延迟时间 – 1 ,这里的单位是毫秒。为什么要减 1 毫秒呢?我们可以回想一下水位线的特点:时间戳为 t 的水位线,表示时间戳≤t 的数据全部到齐,不会再来了。如果考虑有序流,也就是延迟时间为 0 的情况,那么时间戳为 7 秒的数据到来时,之后其实是还有可能继续来 7 秒的数据的;所以生成的水位线不是 7 秒,而是 6 999 毫秒, 7 秒的数据还可以继续来。(BoundedOutOfOrdernessWatermarks 的源码里有写)

(3)自定义水位线策略

WatermarkStrategy 中,时间戳分配器 TimestampAssigner 都是大同小异的,指定字段提取时间戳就可以了;而不同策略的关键就在于 WatermarkGenerator 的实现。整体说来, Flink有两种生成水位线的方式:一种是周期性( Periodic ,另一种是断点式( Punctuated
onEvent() onPeriodicEmit() ,前者是在每个事件到来时调用,而后者由框架周期性调用。周期性调用的方法中发出水位线,自然就是周期性生成水位线;而在事件触发的方法中发出水位线,自然就是断点式生成了。
  • 周期性水位线生成器Periodic Generator
周期性生成器一般是通过 onEvent() 观察判断输入的事件,而在 onPeriodicEmit() 里发出水 位线。
  • 断点式水位线生成器Punctuated Generator
断点式生成器会不停地检测 onEvent() 中的事件,当发现带有水位线信息的特殊事件时,就立即发出水位线。一般来说,断点式生成器不会通过 onPeriodicEmit() 发出水位线。
    // 主函数
    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 l, WatermarkOutput watermarkOutput) {
            // 每来一条数据就调用一次
            maxTs = Math.max(event.timestamp, maxTs); // 更新最大时间戳
        }

        @Override
        public void onPeriodicEmit(WatermarkOutput output) {
            // 发射水位线,默认200ms调用一次
            output.emitWatermark(new Watermark(maxTs - delayTime - 1L));
        }
    }

    // 断点式水位线生成器
    public static class CustomPunctuatedGenerator implements WatermarkGenerator<Event>{

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

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

(4)在自定义数据源中发送水位线

我们也可以在自定义的数据源中抽取事件时间,然后发送水位线。在 自 定 义 数 据 源 中 生 成 水 位 线 和 在 程 序 中 使 用 assignTimestampsAndWatermarks 方法生成水位线二者只能取其一

public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        env.setParallelism(1);

        env.addSource(new ClickSourceWithWatermark()).print();

        env.execute();
}

    // 泛型是数据源中的类型
    public static class ClickSourceWithWatermark implements SourceFunction<Event> {
        private boolean running = true;
        @Override
        public void run(SourceContext<Event> sourceContext) throws Exception {
            Random random = new Random();
            String[] userArr = {"Mary", "Bob", "Alice"};
            String[] urlArr  = {"./home", "./cart", "./prod?id=1"};
            while (running) {
                long currTs = Calendar.getInstance().getTimeInMillis(); // 毫秒时间戳
                String username = userArr[random.nextInt(userArr.length)]; // 随机选取名字
                String url      = urlArr[random.nextInt(urlArr.length)];  // 随机选取url
                Event event = new Event(username, url, currTs);  // 封装成Event
                // 使用collectWithTimestamp方法将数据发送出去,并指明数据中的时间戳的字段
                sourceContext.collectWithTimestamp(event, event.timestamp);
                // 发送水位线
                sourceContext.emitWatermark(new Watermark(event.timestamp - 1L));
                Thread.sleep(1000L);
            }
        }

        @Override
        public void cancel() {
            running = false;
        }
    }

6、水位线的传递

实际应用中往往上下游都有多个并行子任务,为了统一推进事件时间的进展,我们要求上游任务处理完水位线、时钟改变之后,要把当前的水位线再次发出,广播给所有的下游子任务。这样,后续任务就不需要依赖原始数据中的时间戳(经过转化处理后,数据可能已经改变了)。
如果一个任务收到了来自上游并行任务的不同的水位线,说明上游各个分区处理得有快有慢,进度各不相同比如上游有两个并行子任务都发来了水位线,一个是 5 秒,一个是 7 秒。 如果以最小的水位线 5 秒作为当前时钟就不会有这个问题了,因为确实所有上游分区都已经处理完,不会再发 5 秒前的数据了。这让我们想到“木桶原理”:所有的上游并行任务就像围成木桶的一块块木板,它们中最短的那一块,决定了我们桶中的水位。
水位线在上下游任务之间的传递,非常巧妙地避免了分布式系统中没有统一时钟的问题,每个任务都以“处理完之前所有数据”为标准来确定自己的时钟,就可以保证窗口处理的结果总是正确的。

7、水位线总结

(1)水位线是一种特殊的事件,由程序员通过编程插入的数据流里面,然后跟随数据流向下游流动。
(2) 水位线的默认计算公式:水位线 = 观察到的最大事件时间 最大延迟时间 – 1 毫秒。
(3)在编写 Flink 程序时,一定要谨慎的编写每一个算子的计算逻辑,尽量避免大量计算或者是大量的 IO 操作,这样才不会阻塞水位线的向下传递。
(4)在数据流开始之前,Flink 会插入一个大小是负无穷大(在 Java 中是-Long.MAX_VALUE)
的水位线,而在数据流结束时, Flink 会插入一个正无穷大 (Long.MAX_VALUE) 的水位线,保证所有的窗口闭合以及所有的定时器都被触发。
        Flink 对于离线数据集,只会插入两次水位线,也就是在最开始处插入负无穷大的水
位线,在结束位置插入一个正无穷大的水位线。因为只需要插入两次水位线,就可以保证计算
的正确,无需在数据流的中间插入水位线了。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值