uiautomation遍历所有窗口_流式计算系统系列(3):窗口

流式计算系统系列:总纲​zhuanlan.zhihu.com

在上一篇文章中我们提到流式计算系统当中聚合数据流以挖掘更多信息的例子,分别是【网站每隔一个小时的访问量】【每隔五分钟输出最近一个小时成交额最高的商品】和【实时显示用户的访问热点】。我们在上一篇文章中以此引出了流式计算系统当中的时间的概念,而这几个例子本身是基于时间的窗口的例子。

窗口(Window)暂存了上游输入的部分数据,以用于在给定的触发条件下对暂存的这部分数据进行聚合产生输出到下游的结果。可以看到,在这个定义下,窗口主要的属性包括:

  1. 窗口暂存数据的逻辑
  2. 窗口触发计算的逻辑

我们将从这两个属性入手,介绍窗口的分类及特定分类的语义和实现手法

窗口暂存数据的逻辑

这一部分的内容即确定一个窗口内暂存了哪些数据,这个问题包括了两个部分,即新到来的数据暂存到哪个窗口中,以及此前暂存的数据何时清理。

我们看到,在上面分拆问题的描述中,我们反转了行为的主体和受体,即不是由窗口来决定选择哪些数据,而是由数据来决定其归属的窗口。这样的逻辑更加符合流式计算中数据驱动的处理方式。在 Flink 的实现中,这个行为由 WindowAssigner 来负责,给定输入数据,由它判断将这个数据归属到若干个窗口当中。

我们看到 Flink 当中数据归属到窗口这个行为的分类。

GlobalWindow

全局所有数据都归属到同一个窗口。这是一个语义上正确的窗口,不过显而易见的是将所有数据都归属到同一个窗口的价值是有限的,同时面临着暂存区溢出的问题。通常这种窗口会结合定制的清理逻辑(Evictor)和触发逻辑(Trigger)来对窗口中的数据进行定制化的聚合和清理。

TimeWindow

数据按照附带的时间戳归属到不同的时间窗口当中。不同的数据几乎不会完全落到同一个窗口当中,因此时间窗口本身就具有将数据按照其时间戳初步划分批次的作用。我们看到具体的时间窗口的例子来介绍,这里的时间窗口按照其时间属性分为 Processing Time 的时间窗口或 Event Time 的时间窗口,关于不同时间属性的区别在前一篇文章中已经介绍,在此就不再做出区分。

TumblingTimeWindow

此窗口中文对应称为滚动窗口,我们首先看到一个滚动窗口的典型动图。

9f1860caa970216041b8bc5595ab4edc.gif

可以看到,滚动窗口根据划定的长度彼此紧邻而不交叉的出现,对于一个到来的数据,根据时间属性取得其时间戳,即可以算出它所对应的时间窗口。关于这一点,我们利用 Flink 的源码来做直观的解释。

// From TimeWindow.java
public static long getWindowStartWithOffset(long timestamp, long offset, long windowSize) {
    return timestamp - (timestamp - offset + windowSize) % windowSize;
}

这里 timestamp 即数据对应的时间戳,windowSize 即窗口的大小,计算滚动窗口的关键理解难点在于 offset 参数。其实说破了也不复杂,默认滚动时间窗口是整点对齐的,即初始时间可以理解为 0:00:00,下一个窗口的起始时间是 0:00:00 + windowSize,而 offset 能够调整起始时间。上面源码是一个比较内部的逻辑,时间都是化归到距离标准开始时间偏移的毫秒为单位的长整型,实际用户接口是更加可读的时间参数。

本文开始的三个例子当中的【网站每隔一个小时的访问量】即属于滚动时间窗口的例子。

SlidingTimeWindow

此窗口中文对应称为滑动窗口,英文中也有做 HoppingTimeWindow 的,也有 SlidingWindow 表示与此处表述的滑动窗口语义略有区别的时间窗口的,具体可参考这篇文章。我们同样先看到一个滑动窗口的典型动图。

d494e01a1218dc43fe35ee32d4c0a50f.gif

滑动窗口包括窗口长度和滑动步长两个属性,其具体语义由上图不难理解,上图中 s 对应窗口长度,h 对应滑动步长。对于一个数据来说,在滑动窗口的语义下,它有可能归属到若干个窗口当中。其计算方式与滚动窗口类似,只不过这一次首先算出其最后归属的窗口,然后按照滑动步长逐步退回到最先一个归属的窗口,在此期间遍历到的窗口将全部拥有这一个数据。

本文开始的三个例子当中的【每隔五分钟输出最近一个小时成交额最高的商品】即属于滑动时间窗口的例子。

SessionTimeWindow

此窗口中文对应称为会话窗口,与浏览器当中的会话类似,它有一个用户活跃的概念,抽象地说我们把一段连续的活跃时间内的数据划分到同一个窗口之中。我们同样先看到一个会话窗口的典型动图。

676787f9966d202057456f8c37af731e.gif

常见的会话窗口以数据之间超过一定的时间间隔来划分窗口,例如在上图中,s 代表了这个间隔,两个时间上相邻数据之间的间隔超过 0.5 个单位时间即认为是归属不同的会话窗口。

上图是从最终窗口的角度来展示会话窗口的划分过程。然而,我们在处理数据的时候,并不知道它应该归属到哪一个会话窗口中。尤其是考虑到数据无序到达的情况下,我们更不可能基于已有的数据直接在每个中间过程判断出相应的时间窗口归属。不同于前面两种时间窗口,会话窗口的产生是不可实现预知的,完全由输入数据决定。

Flink 对于会话窗口的实现与《Streaming System》或者说 Dataflow 论文介绍的方式一样,我们先看一张图。

c498e64ef3b4bf693df3600236dc97d4.png

再看一段源码。

// From EventTimeSessionWindows.java
public Collection<TimeWindow> assignWindows(Object element, long timestamp, WindowAssignerContext context) {
        return Collections.singletonList(new TimeWindow(timestamp, timestamp + sessionTimeout));
    }

也就是说,数据被归属到一个以其时间戳为起点,超时时间为长度的窗口中,重叠的此类窗口会合并形成更大的窗口,直到 Watermark 或其他触发逻辑表明该窗口可以固化时固化并发送。窗口的合并是一个复杂的过程,包括状态的管理和回调的清理等等,相关的代码细节可以查看 Flink 中 (Evicting)WindowOperator 类中 if (windowAssigner instanceof MergingWindowAssigner) 下的代码块,也可以参考 Flink PMC 伍翀老师的这篇文章。

本文开始的三个例子当中的【实时显示用户的访问热点】即属于会话时间窗口的例子。

User-Defined Window

上面介绍的是最常见的以及 Flink 和其他流式计算系统开箱即用的窗口,在《Streaming Systems》一书中还介绍了根据输入数据的键来错开窗口的非对齐的(Unaligned)滚动时间窗口等自定义窗口。Flink 支持自定义窗口,只要按照 Window/WindowAssigner 以及配套的序列化器等一整套进行定制化实现,就可以根据特殊的用户场景实现自己的功能。根据上面的讨论,从语义上我们也并不需要将窗口和时间绑定起来,窗口只是预划分并支持数据聚合计算的一个暂存区而已。

窗口暂存数据的清理

在上面的讨论中,我们介绍了数据归属到对应的时间窗口的逻辑,关于窗口暂存那些数据,还有一个事情要讨论,即我们如何清理窗口中的数据。

清理窗口数据或者说清理窗口状态的策略总的来说有三种大的类型。

  • 其之一是基于触发器(Trigger),在触发后清理所有数据,在 Flink 当中表现为回调触发器判断触发逻辑时返回 TriggerResult.FIRE_AND_PURGE,从而在触发窗口聚合计算后清理窗口状态。一个实例就是 Flink 当中的滚动计数窗口,它基于 GlobalWindow 实现,因此不会有基于事件的清理逻辑(下面提到的定时器),滚动窗口彼此互不相关,触发后将状态全部抛弃即可。
  • 其之二是基于定时器(Timer),例如时间窗口在超出其最晚时间之后,由此前注册的清理逻辑定时器触发清理状态的动作。在 Flink 中可以参考 WindowOperator#registerCleanupTimerWindowOperator#clearAllState 相关的逻辑。这个方式是符合清理时间窗口的自然逻辑的,不必多举例子。
  • 其之三是基于清除器(Evictor),从逻辑上说,它可以在某个时间根据当前窗口的数据状态来判断从窗口中清除掉那些元素,具有极高的清楚逻辑定制的自由度;从实现上,Flink 仅仅实现在窗口触发时处理函数执行前后在基于当时的状况判断清除掉哪些元素,在处理函数执行完成后重新计算窗口状态。一个实例就是 Flink 当中的滑动计数窗口,与上面提到的滚动计数窗口类似,问题在于滑动计数窗口的元素有重叠。由于没有像时间窗口一样将元素归类到不同的窗口中,而是从实现上在同一个 GlobalWindow 里,因此在触发计算时需要滑动地剔除不属于本计数窗口的元素。

窗口触发计算的逻辑

这一部分解决的是窗口暂存了部分数据之后何时触发聚合计算的问题。总的来说,这是触发器的职责。

关于基于时间的触发器在上一篇文章中已经详细介绍了 Watermark 的机制,也简单介绍了不基于时间的触发器的种类。在这里为了完整性以 Flink 的实现为例子介绍触发器的接口和语义。

// omit some modifiers
class Trigger<T, W extends Window> implements Serializable {
  TriggerResult onElement(T element, long timestamp, W window, TriggerContext ctx);
  TriggerResult onProcessingTime(long time, W window, TriggerContext ctx);
  TriggerResult onEventTime(long time, W window, TriggerContext ctx);
  void clear(W window, TriggerContext ctx);
  boolean canMerge();
  void onMerge(W window, OnMergeContext ctx);
}

后面两个跟前面提到的窗口合并有关,主要涉及内部定时器的清理和状态的合并,clear 方法也是类似的作用。

对于触发计算相关的内容,我们看到有 onElement/onProcessingTime/onEventTime 三个方法,也就是说 Flink 支持基于数据和基于时间的触发逻辑,它们分别在 WindowOperator 调用 processElementonProcessingTimeonEventTime 时被回调,并返回在当前数据或时间条件下是否触发当前窗口的计算。

关于这部分的详细展开,再次推荐参考本系列的前一篇文章关于时间和 Watermark 的介绍。

窗口状态管理

这一部分的内容要完全基于 Flink 的实现来讲了。因为前面两个部分讲的是从流式计算的理论上来说,我们是怎么理解窗口的语义的。Flink 使用其强大的可容错的状态管理机制赋能窗口计算的暂存区管理和容错能力。

根据不同的窗口处理函数,Flink 为 WindowOperator 准备的了 ListState, ReducingState, FoldingState, AggregatingState 等多种状态,以在使用明确的语义并优化状态的内存消耗。总的来说,这些状态都是 AppendingState,即支持有序追加元素的状态。

这个状态作为 WindowOperator 的算子状态,参与到本系列的第一篇文章中提到的容错管理当中,从而保证在容错场景下由检查点恢复出来的作业图在 WindowOperator 上的状态是正确的。具体地说,就是已经被处理并做检查点过的数据及其归属的窗口,这部分状态是能够容错的。因此从源数据回放输入是能够保证恰好一次的处理语义。

对于这个状态的特点,值得强调的有两点。

其之一,它是一个带有 Namespace 的状态,并且其 Namespace 就是窗口。Namespace 是状态除了天然支持的 KeyGroup 划分之外可以定制化的第二层划分的键,由于不同的窗口各自拥有一个命名空间,因此在时间窗口的场景下触发聚合计算的时候取暂存区的状态无需担心不同时间窗口的暂存区相互干扰的问题。

其之二,这个状态是一个 AppendingState,目前为了支持上面提到的一些增量聚合的窗口函数,会有特定的 ReducingState,FoldingState 和 AggregatingState 存在。这些状态实际上是跟处理逻辑相关的,并不是纯粹的数据结构的状态,例如 ListState,ValueState 和 MapState 等。这种设计使得 state backend 在提供不同实现的时候需要重复实现类似的代码去支持本该由上层处理统一的处理逻辑。另外,AppendingState 不支持 MapState,在某些增量操作例如 count distinct 时无法原生的表达,会导致性能瓶颈。性能瓶颈的另一个佐证是在引入 Evictor 时只能先全部出队,在剔除掉元素后再次入队构造状态,体现出了当前基于 AppendingState 设计的一些问题。

关于 Flink 代码,最后有一点需要提及的是,这部分代码的编写使用了大量的上下文状态的方式,即通过改变一个某种 Context 的对象可变的字段,来改变操作的目标,从而导致这部分代码非常难以阅读。建议阅读此部分代码的时候保持头脑清醒,对于上下文变量和类型参数可能的取值有一个清楚的清单。否则很容易就会陷入【我在哪?这段代码在干嘛?】的状况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值