文章目录

  • 1. Flink 的时间语义
  • 2. Timestamp 和 Watermark
  • 2.1 Timestamp 分配和 Watermark 生成
  • 2.2 Watermark 的传播
  • 2.3 ProcessFuction
  • 2.4 Watermark 的处理逻辑
  • 3. Table API 中的时间
  • 3.1 Table 中指定时间列
  • 3.2 时间列和 Table 操作
  • 4. 对于时间的思考
  • 4.1 时间是数据 or 元数据?
  • 4.2 对比 Event Time 和 Precessing Time
  • 4.3 时间和 Watermark 的本质


1. Flink 的时间语义

时间属性是流处理中最重要的一个方面,是流处理系统的基石之一。

Flink 共有 3 种时间语义:Processing Time,Event Time,Injection Time。

Process Time 是通过直接去调用本地机器的时间,而 Event Time 则是根据每一条处理记录所携带的时间戳来判定。

flink yarn session挂了 flink event time_Processing


Injection Time 代表数据到达 Flink 的时间。

  • Processing Time,是本地节点的时间,每一次取到的 Processing Time 肯定是递增的,所以相当于拿到的是一个有序的数据流。
  • Event Time 时间是绑定在每一条的记录上的。由于网络延迟、程序内部逻辑、或者其他一些分布式系统的原因,数据的时间可能会存在一定程度的乱序。在 Event Time 场景下,每一个记录所包含的时间称作 Record Timestamp。

2. Timestamp 和 Watermark

在 Event Time 场景下,为了处理乱序数据,Flink 引入了 Watermark。
watermark 相当于在整个时间序列里插入一些类似于标志位的特殊的处理数据,watermark 代表而来这些数据的 timestamp 数值,同时表示以后到来的数据已经再也没有小于或等于这个时间的了。

2.1 Timestamp 分配和 Watermark 生成

Flink 支持两种 watermark 生成方式。

  • 在 SourceFunction 中产生,相当于把整个的 timestamp 分配和 watermark 生成的逻辑放在流处理应用的源头。可以在 SourceFunction 里面通过这两个方法产生 watermark:
// 方法一:第一个参数就是我们要发送的数据,第二个参数就是这个数据所对应的时间戳
collectWithTimestamp(T element, long timestamp);
// 方法二:产生一条 watermark,表示接下来不会再有时间戳小于等于这个数值记录。
emitWatermark(Watermark mark);
  • 在流中指定,DataStream 调用 assignTimestampsAndWatermarks 就能够接收不同的 timestamp 和 watermark 的生成器。
dataStream.assignTimestampsAndWatermarks (...);

传入的生成器有两类:第一类是定期生成器;第二类是根据数据进行处理生成器

后者每一次分配 Timestamp 之后都会调用用户实现的 watermark 生成方法,需要在生成方法中去实现 watermark 的生成逻辑。

flink yarn session挂了 flink event time_数据_02

2.2 Watermark 的传播

  • 广播传播
    Watermark 会以广播的形式在算子之间进行传播,上游的算子,它连接了三个下游的任务,它会把自己当前的收到的 watermark 以广播的形式传到下游。
  • Long.MAX_VALUE 表示不会再有数据
    如果收到了一个 Long.MAX_VALUE 数值的 watermark,就表示对应的那一条流的一个部分不会再有数据发过来了,相当于一个终止标志。
  • 单输入取其大,多输入取其小
    对于单流而言,这个策略比较好理解,而对于有多个输入的算子,watermark 的计算就有讲究了,一个原则是:单输入取其大,多输入取小,似于木桶效应。
    下图理解:单输入取其大,多输入取其小。

    watermark 在传播的时候有一个特点是,它的传播是幂等的。多次收到相同的 watermark,甚至收到之前的 watermark 都不会对最后的数值产生影响,因为对于单个输入永远是取最大的,而对于整个任务永远是取一个最小的。

Watermark 的局现性:没有区分是一条流多个 partition 还是不同的逻辑上的流的 JOIN。
对于同一个流的不同 partition,我们对他做这种强制的时钟同步是没有问题的,因为这条流上的不同 partition 是共享时钟的。
但是,如果是两条时钟相差很大的流 JOIN,那么快的流要等慢的流,可能要在状态中去缓存非常多的数据,这对于整个集群来说是一个很大的性能开销。

2.3 ProcessFuction

Watermark 在任务里的处理逻辑分为内部逻辑外部逻辑。外部逻辑其实就是通过 ProcessFunction 来体现的,如果你需要使用 Flink 提供的时间相关的 API 的话就只能写在 ProcessFunction 里。
ProcessFunction 中对时间进行处理主要由以下三个方面:

  • 获取正在处理这条数据的 Record Timestamp,或者当前的 Processing Time。
  • 获取当前算子的时间,也可以理解成当前的 watermark。
  • 注册 Timer(定时器)并提供一些回调逻辑。
    registerEventTimeTimer()
    registerProcessingTimeTimer()
    onTimer()
    在 onTimer 方法中需要实现回调逻辑,当条件满足时回调逻辑就会被触发。

通过 timer 去设定一个时间,指定某一些数据可能在将来的某一个时间点过期,从而把它从状态里删除掉。所有的这些和时间相关的逻辑在 Flink 内部都是由 Time Service(时间服务)完成的。

2.4 Watermark 的处理逻辑

flink yarn session挂了 flink event time_Time_03

  1. 第一步:一个算子的实例在收到 watermark 的时候,首先要更新当前的算子时间。
  2. 第二步:遍历计时器队列,计时器队列就是 Timer,用户可以同时注册很多 Timer,Flink 会把这些 Timer 按照触发时间放到一个优先队列中,接着会逐一触发用户的回调逻辑。
  3. 第三步:Flink 的某一个任务会将当前的 watermark 发送到下游的其他任务实例上,完成整个 watermark 的传播,从而形成一个闭环。

3. Table API 中的时间

为了让时间参与到 Table/SQL 这一层的运算中,需要提前把时间属性放到表的 schema 中,这样才能够在 SQL 语句或者 Table 的一些逻辑表达式里面去使用这些时间去完成需求。

3.1 Table 中指定时间列

Processing Time

要得到一个 Table 对象(或者注册一个 Table)有两种手段:

flink yarn session挂了 flink event time_数据_04

  1. 可以从一个 DataStream 转化成一个 Table。
    此时,只需要在已有的列中( f1 和 f2 ),在最后用“列名.proctime”这种写法就可以把最后的这一列注册为一个 Processing Time,再查询的时候就可以直接使用这一列。
  2. 直接通过 TableSource 去生成这么一个 Table。
    如果 Table 是通过 TableSource 生成的,可以通过实现这一个 DefinedRowtimeAttributes 接口,然后会自动根据自定义的逻辑生成对应的 Processing Time。

Event Time

要得到一个 Table 对象(或者注册一个 Table)有两种手段:

flink yarn session挂了 flink event time_Time_05

  1. 可以从一个 DataStream 转化成一个 Table。
    如果要从 DataStream 去转化得到一个 Table,必须要提前保证原始的 DataStream 里面已经存在了 Record Timestamp 和 watermark。
  2. 直接通过 TableSource 去生成这么一个 Table。
    如果想通过 TableSource 生成,要保证接入的一个数据里面存在一个类型为 long 或者 timestamp 的一个时间字段。

总的来说,
如果要从 DataStream 去注册一个表,和 proctime 类似,只需要加上“列名.rowtime”就可以。需要注意的是,如果要用 Processing Time,必须保证要新加的字段是整个 schema 中的最后一个字段,而 Event Time 的时候其实可以去替换某一个已有的列,然后 Flink 会自动的把这一列转化成需要的 rowtime 这个类型。
如果是通过 TableSource 生成的,只需要实现 DefinedRowtimeAttributes 接口就可以了。需要说明的一点是,在 DataStream API 这一侧其实不支持同时存在多个 Event Time(rowtime),但是在 Table 这一层理论上可以同时存在多个 rowtime。因为 DefinedRowtimeAttributes 接口的返回值是一个 rowtime 的 List,即可以同时存在多个 rowtime 列,在将来可能会进行一些其他的改进,或者基于去做一些相应的优化。

3.2 时间列和 Table 操作

flink yarn session挂了 flink event time_Time_06


指定完了时间列之后,当我们要真正去查询时就会涉及到一些具体的操作。

比如说“Over 窗口聚合”和“Group by 窗口聚合”这两种窗口聚合,在写 SQL 提供参数的时候只能允许在这个时间列上进行聚合。还有就是时间窗口聚合,在写条件的时候只支持对应的时间列。

最后就是排序,在一个无尽的数据流上对数据做排序几乎是不可能的事情,但因为这个数据本身到来的顺序已经是按照时间属性进行排序的,所以如果要对一个 DataStream 转化成 Table 进行排序的话,你只能是按照时间列进行排序,当然同时用户也可以指定一些其他的列,但是时间列这个是必须的,并且必须放在第一位。

为什么说这些操作只能在时间列上进行?因为我们有的时候可以把到来的数据流就看成是一张按照时间排列好的一张表,而我们任何对于表的操作,其实都是必须在对它进行一次顺序扫描的前提下完成的。因为大家都知道数据流的特性之一就是一过性,某一条数据处理过去之后,将来其实不太好去访问它。当然因为 Flink 中内部提供了一些状态机制,我们可以在一定程度上去弱化这个特性,但是最终还是不能超越的限制状态不能太大。所有这些操作为什么只能在时间列上进行,因为这个时间列能够保证我们内部产生的状态不会无限的增长下去,这是一个最终的前提。

4. 对于时间的思考

4.1 时间是数据 or 元数据?

  • DataStream 中,时间是元数据。
    DataStream 中拿到一条数据后,是无法直接从数据中拿到时间的,所以时间是作为元数据存在的。。
  • SQL / Table 中,时间是数据。
    在 SQL / Table 中,如果要使用时间需要将时间注册到表中的某一列上,所以时间是作为是数据本身来存在的。
  • 在 Upsert Stream 中不允许出现事件字段。

4.2 对比 Event Time 和 Precessing Time

flink yarn session挂了 flink event time_数据_07

4.3 时间和 Watermark 的本质

  • 流处理中时间本质上就是一个普通的递增字段,不一定是真的时间。比如,某一些流式数据的id是递增的,那么也可以把它当做“时间”来看待。
  • Watermark 只是应对乱序的办法之一,在设置 Watermark 时,大多是启发式的,凭经验和对业务的理解上来进行设置,在延迟和完整性之间抉择。