Flink 时间语义和Watermark

时间语义

Flink有三种时间语义,即

  • Event Time(事件时间):事件创建的时间
  • Ingestion Time(摄入时间): 数据进入Flink集群的时间
  • Window Processing Time(处理时间):执行操作算子时的本地系统时间,与机器相关。

理想情况下事件时间和处理时间是没有延迟的,即事件产生即被处理,如同下图虚线所示,但是现实中因为网络波动,设备故障等会出现延迟,就如同下图的红线一样。

在这里插入图片描述

不同的时间语义有不同的应用场合,但是有波动的处理时间用起来肯定很麻烦,所以我们最关心的是事件时间,即事件什么时候发生的。

Event Time

  1. 在代码里设置事件时间
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 设置时间特性为事件时间
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
  1. 乱序数据的影响

在这里插入图片描述

因为网络、分布式等原因,会导致乱序数据的产生,当Flink以EventTime模式处理数据流时,他会根据数据里的时间戳来处理基于时间的算子。

比如每1s发送一条数据,假设开窗是5S,理想情况下一个窗口会处理5条数据,但是实际上窗口处理了3条数据,这样就会导致窗口的计算不准确。

水位线(Watermark)

为了避免乱序数据带来计算不正确,哪怕一个时间戳达到了窗口关闭的时间,也应该再等待一段时间,等迟到的数据来了再关闭窗口。

假如我们设置10s的时间窗口(window),那么0-10s,10-20s都是一个窗口,以0-10s为例,0位start-time,10为end-time。假如有4个数据的event-time分别是8(A),12.5(B),9(C ),13.5(D),我们设置Watermarks为当前所有到达数据event-time的最大值减去end-time,即3.5秒,也就是说对于迟到的数据,我们只等你3.5秒。

【这里你可能有个问题,如果超过3.5秒该怎么办,这时候就需要我们对生产环境有一个整体的认识和把握,数据是否有延迟,延迟大概是多长时间,这样达到数据不丢失。当然还有另外的方法来处理延迟,我们这里只讲水位线。】

当A到达的时候,Watermarks为max{8}-3.5=8-3.5 = 4.5 < 10,不会触发计算
当B到达的时候,Watermarks为max(12.5,8)-3.5=12.5-3.5 = 9 < 10,不会触发计算
当C到达的时候,Watermarks为max(12.5,8,9)-3.5=12.5-3.5 = 9 < 10,不会触发计算
当D到达的时候,Watermarks为max(13.5,12.5,8,9)-3.5=13.5-3.5 = 10 = 10,触发计算
触发计算的时候,会将ABC(因为他们都小于10)都计算进去

通过上面这种方式,我们就将迟到的C计算进去了

这里的延迟3.5s是我们假设一个数据到达的时候,比他早3.5s的数据肯定也都到达了,这个是需要根据经验推算的,假如D到达以后有到达了一个Eevent-time=6(E),但是由于0~10的时间窗口已经开始计算了,所以E就丢了。

从这里上面E的丢失说明,水位线也不是万能的,但是如果根据我们自己的生产经验+侧道输出等方案,可以做到数据不丢失。

水位线就是一种衡量事件时间进展的机制,可以设定延迟触发。

  • Watermark 是用于处理乱序事件的,而正确的处理乱序事件,通常用Watermark 机制结合 window 来实现;
  • 数据流中的 Watermark 用于表示 timestamp 小于 Watermark 的数据,都已经到达了,因此,window 的执行也是由 Watermark 触发的。
  • Watermark 用来让程序自己平衡延迟和结果正确性
  • Watermark 其实是一条特殊的数据记录
  • Watermark 必须单调递增,以确保任务的事件时间在向前推进
  • Watermark 与数据的时间戳相关

水位线的传递

当算子收到一个WaterMark时,算子会根据这个WaterMark的时间戳更新内部的Event Time Clock,当前记录的时间与WaterMark进行对比,如果WaterMark大于记录的时间,则更新该记录为当前的WaterMark值。

在实际应用中,主要是应对并行多流的场景:
在这里插入图片描述

假设一个Task算子,上游有四个分区,下游有三个分区。上游四个分区的水位线分别为2,3,4,5,则这个算子的Event Time Clock就会更新为四个分区中最小的水位线2,同时将他作为下游三个分区的水位线统一广播出去。

之后第一个分区的水位线更新为4,此时最小的水位线为3,更新到Event Time Clock,此时下游分区的水位线就是3。

水位线的设定

WaterMark设定方法有两种:

1.Punctuated Watermark : 数据流中每一个递增的EventTime都会产生一个Watermark

在实际的生产中Punctuated方式在TPS很高的场景下会产生大量的Watermark在一定程度上对下游算子造成压力,所以只有在实时性要求非常高的场景才会选择Punctuated的方式进行Watermark的生成

2.Periodic Watermark : 周期性的(允许一定时间间隔或者达到一定的记录条数)产生一个Watermark

在实际的生产中Periodic的方式必须结合时间和积累条数两个维度继续周期性产生Watermark,否则在极端情况下会有很大的延时

水位线实例

我们先看一下在有序数据环境下水位线和时间戳的变化。

package com.ts.watermark;

import org.apache.flink.api.common.functions.FoldFunction;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.streaming.api.TimeCharacteristic;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.time.Time;
import scala.tools.jline_embedded.internal.Nullable;

public class PunctuatedTest {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment();
        
        // 事件时间
        env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
        
        // socket接受数据
        DataStream<String> dataStream = env
                .socketTextStream("localhost", 9999).assignTimestampsAndWatermarks(new AssignerWithPeriodicWatermarks<String>() {
                    // 当前时间戳
                    long currentTimeStamp = 0l;
                    // 最大允许延迟时间
                    long maxDelayAllowed = 0l;
                    // 当前水位线
                    long currentWaterMark;

                    @Nullable
                    @Override
                    // 再调用
                    public Watermark getCurrentWatermark() {
                        // 当前水位线 = 当前时间戳-最大允许延迟时间
                        currentWaterMark = currentTimeStamp - maxDelayAllowed;
                        System.out.println("当前水位线:" + currentWaterMark);
                        return new Watermark(currentWaterMark);
                    }

                    @Override
                    // 先调用
                    public long extractTimestamp(String s, long l) {
                        String[] arr = s.split(",");
                        long timeStamp = Long.parseLong(arr[1]);
                        currentTimeStamp = Math.max(timeStamp, currentTimeStamp);
                        System.out.println("Key:" + arr[0] + ",EventTime:" + timeStamp + ",前一条数据水位线:" + currentWaterMark);
                        return timeStamp;
                    }
                });

        dataStream.map(new MapFunction<String, Tuple2<String, String>>() {
            @Override
            public Tuple2<String, String> map(String s) throws Exception {
                return new Tuple2<String, String>(s.split(",")[0], s.split(",")[1]);
            }
        }).keyBy(0)
                // 5s的滚动窗口
                .window(TumblingEventTimeWindows.of(Time.seconds(5)))
                // 打印进入窗口内的数据的时间戳变化
                .fold("Start:", new FoldFunction<Tuple2<String, String>, String>() {
                    @Override
                    public String fold(String s, Tuple2<String, String> o) throws Exception {
                        return s + " - " + o.f1;
                    }
                }).print();

        env.execute("WaterMark Test Demo");
    }
}

输入数据:

test,1553503185000
test,1553503186000
test,1553503187000
test,1553503188000
test,1553503189000
test,1553503190000

输出结果:

当前水位线:0
Key:test,EventTime:1553503185000,前一条数据水位线:0
当前水位线:1553503185000
Key:test,EventTime:1553503186000,前一条数据水位线:1553503185000
当前水位线:1553503186000
Key:test,EventTime:1553503187000,前一条数据水位线:1553503186000
当前水位线:1553503187000
Key:test,EventTime:1553503188000,前一条数据水位线:1553503187000
当前水位线:1553503188000
Key:test,EventTime:1553503189000,前一条数据水位线:1553503188000
当前水位线:1553503189000
Key:test,EventTime:1553503190000,前一条数据水位线:1553503189000
当前水位线:1553503190000
5> Start: - 1553503185000 - 1553503186000 - 1553503187000 - 1553503188000 - 1553503189000
当前水位线:1553503190000

一条一条输入的话,可以很明显看到水位线的变化,如果一次性输入多条,就会出现并行多流的情况。

当前水位线:0
Key:test,EventTime:1553503185000,前一条数据水位线:0
Key:test,EventTime:1553503186000,前一条数据水位线:0
Key:test,EventTime:1553503187000,前一条数据水位线:0
Key:test,EventTime:1553503188000,前一条数据水位线:0
Key:test,EventTime:1553503189000,前一条数据水位线:0
当前水位线:1553503189000
5> Start: - 1553503189000 - 1553503186000 - 1553503188000 - 1553503187000 - 1553503185000

如果在最后再输入几条数据:

test,1553503183000
test,1553503184000
test,1553503190000
test,1553503191000
test,1553503192000
test,1553503193000
test,1553503194000
test,1553503195000
test,1553503192000
test,1553503193000

输出结果:

Key:test,EventTime:1553503185000,前一条数据水位线:0
Key:test,EventTime:1553503186000,前一条数据水位线:0
Key:test,EventTime:1553503187000,前一条数据水位线:0
Key:test,EventTime:1553503188000,前一条数据水位线:0
Key:test,EventTime:1553503189000,前一条数据水位线:0
当前水位线:1553503189000
5> Start: - 1553503189000 - 1553503186000 - 1553503188000 - 1553503187000 - 1553503185000

Key:test,EventTime:1553503183000,前一条数据水位线:1553503189000
Key:test,EventTime:1553503184000,前一条数据水位线:1553503189000
Key:test,EventTime:1553503190000,前一条数据水位线:1553503189000
Key:test,EventTime:1553503191000,前一条数据水位线:1553503189000
Key:test,EventTime:1553503192000,前一条数据水位线:1553503189000
Key:test,EventTime:1553503193000,前一条数据水位线:1553503189000
Key:test,EventTime:1553503194000,前一条数据水位线:1553503189000
当前水位线:1553503194000
    ----------
5> Start: - 1553503184000 - 1553503183000
    ----------
5> Start: - 1553503189000 - 1553503185000 - 1553503187000 - 1553503186000 - 1553503188000
当前水位线:1553503194000
    ----------
5> Start: - 1553503190000 - 1553503191000 - 1553503193000 - 1553503192000 - 1553503194000
当前水位线:1553503194000
    ----------
Key:test,EventTime:1553503195000,前一条数据水位线:1553503194000
当前水位线:1553503195000
    ----------  
Key:test,EventTime:1553503192000,前一条数据水位线:1553503195000
Key:test,EventTime:1553503193000,前一条数据水位线:1553503195000
5> Start: - 1553503192000 - 1553503193000

可以看到迟到的数据(下面两组)并没有被处理。

test,1553503183000
test,1553503184000
----
test,1553503192000
test,1553503193000

为了解决乱序数据的问题,我们允许Flink处理延迟以5秒内的迟到数据。

long maxDelayAllowed = 5000l;

输入数据

test,1553503185000
test,1553503186000
test,1553503187000
test,1553503188000
test,1553503189000
test,1553503190000
test,1553503187000
test,1553503186000
test,1553503191000
test,1553503192000
test,1553503193000
test,1553503194000
test,1553503195000

输出结果

Key:test,EventTime:1553503185000,前一条数据的水位线:-5000
当前水位线:1553503180000

Key:test,EventTime:1553503186000,前一条数据的水位线:1553503180000
当前水位线:1553503181000

Key:test,EventTime:1553503187000,前一条数据的水位线:1553503181000
当前水位线:1553503182000

Key:test,EventTime:1553503188000,前一条数据的水位线:1553503182000
当前水位线:1553503183000

Key:test,EventTime:1553503189000,前一条数据的水位线:1553503183000
当前水位线:1553503184000

Key:test,EventTime:1553503190000,前一条数据的水位线:1553503184000
当前水位线:1553503185000

Key:test,EventTime:1553503187000,前一条数据的水位线:1553503185000
当前水位线:1553503185000

Key:test,EventTime:1553503186000,前一条数据的水位线:1553503185000
当前水位线:1553503185000

Key:test,EventTime:1553503191000,前一条数据的水位线:1553503185000
当前水位线:1553503186000

Key:test,EventTime:1553503192000,前一条数据的水位线:1553503186000
当前水位线:1553503187000

Key:test,EventTime:1553503193000,前一条数据的水位线:1553503187000
当前水位线:1553503188000

Key:test,EventTime:1553503194000,前一条数据的水位线:1553503188000
当前水位线:1553503189000

Key:test,EventTime:1553503195000,前一条数据的水位线:1553503189000
当前水位线:1553503190000
2> Start: - 1553503185000 - 1553503186000 - 1553503187000 - 1553503188000 - 1553503189000 - 1553503187000 - 1553503186000

设置了最大允许乱序时间后,WaterMark要比原来低5秒,可以对延迟5秒内的数据进行处理,窗口的触发条件也同样会往后延迟。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

寒 暄

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

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

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

打赏作者

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

抵扣说明:

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

余额充值