通过Time、Window与Trigger比较Google Cloud DataFlow与Apache Flink的区别

来源:互联网 发布:网吧网络是多少兆宽带 编辑:程序博客网 时间:2024/04/30 15:27

本文主要通过Google Cloud DataFlow Paper的内容与Apache Flink DataStream中的实现进行了基于Time、Window和Trigger的比较。

众所周知,Apache Flink最早来源于[Stratosphere]项目,DataFlow则来源于MillWheel项目,且DataFlow的实现是基于FlumeJava和MillWheel。Flink的实现也是借鉴自Millwheel,因此,在流处理的实现上,两者的实现引擎可以说都来自MillWheel,进而有很多相似之处,尽管Apache Flink在概念上有些也是基于DataFlow的论文。

1、流处理简介

流处理在分布式系统中面临的难题包括:大量的数据、无界、乱序、以及全局性(基于分布式)的处理。

何为正确性?完全的正确性在流处理中不可能实现,只有在批处理中才会有completely正确性。因此,DataFlow的设计准则的第一条就是“从来不依赖任何的完整性的概念”。

下图展示DataFlow的设计准则:

这里写图片描述

Flink的设计目标包括对于大量数据处理提供低延迟、能够处理乱序、有状态(全局性状态)的无界的数据流。

2、如何在正确性、延迟以及成本之间进行平衡?

DataFlow和Flink中都有Time、watermark、Window和Trigger的概念。

(1)Time的2种方式:

1、Event Time2、processing Time

除此之外,Flink中还实现了一种新的时间概念:Ingestion Time。即event进入Flink时打上的时间戳,也就是其Ingestion time可以作为其watermark的时间。这对于event本身没有提供时间字段的例子很有帮助。

关于watermark,之前的文章中也有提到。理想情况下,事件的event time等于processing time,然后现实不可能是这样的。因此,理想水位线与现实水位线之间的差值,DataFlow中给出了这个值的定义:Skew。
这里写图片描述

(2)window内部实现的2种方式:

1、Set<Window> AssignWindows(T datum)2、Set<Window> MergeWindows(Set<Window> windows)

即根据数据,确定其所属的window;同时,对于session window而言,也存在着window merge到一起的情况。

Flink与DataFlow中都有相同的Window的概念。

我们通过一个例子说明window的assign和merge。

(1)Window Assignment
这里写图片描述

例如,有2条数据(k,v1,12:00)以及(k,v2,12:01),通过operator:“AssignWindows(
Sliding(2m,1m))”将每条数据划分到相应的窗口中,由于是sliding窗口,因此每条数据被划分到2个不同的窗口中。

(2)Window Merging
这里写图片描述

在DataFlow中,ession window的处理是window merge机制产生的动机,因此,这里使用了session window的例子,关于session window,可以参考我之前的文章。

这个例子中,设置的session gap是30分钟,且用到了几个operator:

1、AssignWindows(Sessions(30m)): 根据每条数据的timestamp,划分其窗口范围2、DropTimestamps: 将每条数据的timestamp列去掉,只剩下(k,v,window range)3、GroupByKey: 根据key,聚合数据成为一个(value,window)组4、MergeWindows(Sessions(30m)): 根据指定的窗口策略(sessions(30m)),将同一个key中有重合的窗口进行merge。例如k1中,v1和v4的窗口有重合,因此,扩展v1和v4所在的窗口范围到[13:02,13:50),即v1和v4所在窗口范围的并集。5、GroupAlsoByWindow: 对于同一个key,按照窗口分组,将相同窗口的value聚合到一起。6、ExpandToElements: 扩展元素,这里最终输出(key,value List,window_end_time,window),即把窗口结束的时间作为每条数据的时间戳。

从最后的结果看,DataFlow中的session window和Flink中的session window基本一致。

(3)Trigger触发器

在流处理中,实现基于Event Time、提供非对齐(窗口大小不固定,典型的就是session window)的窗口,本身就是一种提升。

我们先看看DataFlow中如何处理Trigger。

到目前为止,我们提到了Event Time与Window的机制与实现,那么我们还面临2个问题:

1、对于tuple以及基于processing time的窗口的支持2、何时触发窗口?

对于问题1,一会我们会在具体的例子中展示;问题2,对于现实世界中基于event time处理可能遇到的乱序的情况,我们需要一种信号来告诉我们什么时候开始窗口的计算。

说到这里,可能大家已经想到,乱序问题的处理的就依赖watermark就好了。然而, watermark本身也存在2个短板:

1、太快2、太慢

这怎么解释呢?

太快就是watermark产生(emit)后,仍然存在一些late的数据到来。因为在分布式数据处理中,我们永远不知道late element何时到达。因此也就无法100%保证数据的正确性。

太慢的意思是说watermark本身是一种全局性的概念,虽然我们可以通过设置一个更小的让时间放慢的基准来减小event time skew的大小,但是基准线仍然可能会带来几分钟的延迟,这通常取决于数据源。因此,仅仅通过watermark来作为窗口触发的机制,可能回带来更大的延迟。

基于这些原因,我们也可以总结出:任何流处理都不能处理完整性问题,取而代之的观点是我们设定一个低延迟的目标值,然后在此基础上,对结果的一致性和正确性提供保障。

由此,trigger触发器应运而生,它提供了一种机制,当收到某个内部或者外部信号时,刺激触发窗口的计算。触发器是窗口机制的一种补充。对于window而言,它决定将包含event time字段的数据聚合在哪里;而Trigger则决定着何时(本质上也是一种processing time)将窗口触发。

这里稍微解释下Trigger为什么是processing time时间轴语义的定义,我们想象一种watermark trigger,即根据数据自身的event time,应用某种函数,生成watermark,然后根据watermark的时间来作为触发的条件,实现时本质上就是processing time。

DataFlow模型中提供了几种预定义的触发器:

1、watermark timer trigger:(例如百分位水位线,当输入数据到达一定的百分比时,可以快速的触发计算)2、processing timer trigger: processing time的触发器3、data arriving trigger: 基于count,bytes或者某些特殊的数据标记或模式匹配等的触发器4、composing trigger: 一些与、或、基于环或者序列的触发器5、自定义触发器:例如某些外部信号或者RPC回调的完成等的触发器。

而且,当trigger触发后,也有几种不同的更加精细的控制模式:

1、丢弃式(当窗口触发后,又有之前的窗口的数据进来时,此种方式直接抛弃这些记录,跟之前触发的窗口没有关系)2、累加式(当窗口触发后,窗口内的数据被完整的缓存起来,当又有之前的窗口的数据进来时,这些记录会再次追加到            之前的窗口中,进行一种累加式的计算。这种方式不太适合立刻就输出结果的用例)3、累加并撤销式(当窗口触发后,不但窗口内的数据被缓存起来,而且将窗口计算的结果也缓存起来,当又有之前窗口的数据进来时,这些记录会再次追加进去计算,而且再次输出。此时,第一次输出的内容是之前窗口的值,第二次则是修正之后的值。由于需要输出2次或者多次,因此对于那种低延迟、又需要最终一致性的需求,是非常适合的。)

(4)DataFlow的例子

下面我们通过一些例子来总结一下DataFlow模型中Time、Watermark、Window以及Trigger的概念。

例子中假设只对输入的数字进行sum统计,假设有10个相同key的数据(当然现实中肯定是多个key的并行数据流,这里为方便画图,简单起见)。其中,X轴代表Event Time,即数据发生的时间;Y轴代表系统的处理时间,即Processing Time。而且很多例子中都依赖watermark,这里画出理想的watermark直线与实际的watermark曲线(存在skew)。SDK表示如下:
这里写图片描述

这里写图片描述

上图中,数字9发生的时间在【12:01,12:02】之间,但是到达系统的时间在【12:08,12:09】之间。理想的watermark是是没有skew的,即一条直线(Y轴开始的时间是12:05)。而实际的watermark则是一条曲线。

(a):GlobalWindow的批处理

假如我们以传统的批处理的方式计算,那么我们就得等到所有的数据都到达后,才开始触发计算,且只计算一次。此时的实际watermark是在12:09左右,如下图:
这里写图片描述

(b):GlobalWindow的流处理

假设这些数据以流的形式进入系统,默认的触发机制是watermark超过窗口的结束时间,但此时是个global window(Sum.integersPerKey()没有任何窗口,即默认的Global window),即没有结束时间,所以如果只以watermark作为触发机制,则永远也不会有输出。

(b.1)

此时,我们换一种SDK表达,每隔一分钟进行一次重复的统计,后边统计的结果要在前边的结果上进行累加操作:
这里写图片描述

此时,通过触发器机制,我们可以获得多次输出,每次输出都是对上一次结果的累加,看起来是这样的:

这里写图片描述

(b.2)

此时,我们再换一种SDK表达,仍然是每分钟统计一次,但是此时使用discard方式,即不做累加处理:
这里写图片描述

此时的结果如下:

这里写图片描述

我们看到,这种方式的窗口是Global Window,触发机制是以processing time每隔1分钟触发一次,每次的范围是当前分钟内数据,不做累加的值。概括起来就是GlobalWindow.trigger的方式来实现。

以上2种都是Processing time语义的窗口机制,根本没有event time的事。如果只是单纯的以processing time作为触发机制,那么有一个最好的办法:使用Ingestion Time。即数据注入到系统后,打上一个时间戳作为event time,此时的watermark是完美的,高效的且低成本的,没有任何的late element产生。

(b.3)

这里,我们再次换一种SDK表达,使用的是基于count的触发方式,即每收到2个数据,触发一次:

这里写图片描述

输出的结果如下:

这里写图片描述

这里也是GlobalWindow.trigger,只不过不以Processing Time作为触发机制,而是以count作为触发机制。

(b.4)

现在,我们不再使用GlobalWindow作为window,取而代之的是FixedWindow。我们以2分钟的固定窗口来进行累加统计,SDK如下:

这里写图片描述

这里使用的是预定义的窗口,FixedWindows。我们注意到此时并没有指定trigger,此时使用默认的trigger机制:watermark trigger。因此,上边的SDK也等同于下边的表达:

这里写图片描述

当watermark通过窗口结束时,触发计算。上边的几个例子中,repeat调用的目的是用来处理late数据的,一旦有小于watermark的数据到达系统后,会立刻再次触发窗口的计算。

(c):FixedWindow的批处理

我们继续来看2分钟的固定窗口的例子。如果我们使用batch的方式,那我们需要等到所有数据到达才触发,此时的结果如下:

这里写图片描述

此时,由于是watermark trigger,因此窗口的结束时间就是所有数据都到达的时间,也就是窗口触发的时间。这个例子是个2分钟的固定窗口,因此同时触发了4个窗口的计算(event time)。分别是[12:00,12:02),[12:02,12:04),[12:04,12:06),[12:06,12:08)。

(d):FixedWindow的微批处理

我们还是以2分钟的固定窗口举例。这里假设每分钟一个批次,每次都是触发都是统计用event time作为时间的2分钟窗口,最终的输出是这样的:

这里写图片描述

这里解释一下输出:

1、在12:06时产生一批,即触发窗口,此时的窗口有2个,分别是[12:00,12:02),[12:02,12:04),输出的结果是5和7。2、在12:07时又产生一批,此时窗口有2个,分别是[12:02,12:04)和[12:04,12:06),输出的结果分别是14和3.虽然[12:02,12:04)之前被触发过,但是选择的方式是累加模式,且这个批次又有属于这个窗口的数据到达,因此再次触发这个窗口的计算。3、在12:08时产生一批,被触发的窗口是[12:02,12:04),[12:06,12:08)。4、在12:09时产生一批,被触发的窗口是[12:00,12:02)和[12:06,12:08),结果是累加后的14和12。

(e):FixedWindow的流处理

对于流处理,我们需要等到有数据的event time大于窗口的结束时间时,才开始触发计算。那么我们看看执行的结果:

这里写图片描述

我们简单分析一下输出:

1、当数字值为7的值到达时,触发了[12:00,12:02)的窗口,此时的物理时间大概在12:06;此时的watermark时间大概在12:022、当数字3(第二个数字3,即大概在12:04:20秒左右产生的数据)到达时,触发了[12:02,12:04)的窗口,此时物理时间大概在12:07:30秒;此时的watermark时间到了12:043、当数字3(第三个数字3,即大概在12:07分左右产生的数据)到达时,触发了[12:04,12:06)的窗口,此时物理时间大概在12:07:40秒;此时的watermark时间超过了12:064、当数字9到达后,由于数字9event time还是12:01:30秒,远远小于当前的watermark时间(12:06后),因此立刻、再次触发窗口[12:00,12:02)的计算,累加后的结果是145、当有数据的水位线超过12:08时(图中没有标示后边的数字),触发了[12:06,12:08)的窗口,此时的物理时间大概在12:10左右,watermark时间超过了12:08

以上就是流处理时,以watermark trigger作为触发器的例子,这也是默认的触发器机制。但是,我们可以看到上边的例子的最大的问题就是延迟比较高(虽然现实世界数据总是源源不断的进来,不会出现这么高的延迟)。

(f):FixedWindow的精细化的流处理

由上边的例子我们可以看出,某些特殊情况下,流处理的延迟甚至高于微批处理的延迟。那么,我们换一种更加细化的流处理方式,自定义组合式的trigger,替换原有的watermark trigger,SDK如下:

这里写图片描述

上边的trigger的含义是:以2分钟的固定窗口,以processing time每隔1分钟触发一次窗口的计算(窗口内需要有新数据),直到新数据的watermark时间超过了窗口的结束时间。这句话简单理解就是:当窗口内有数据时,如果数据的watermark时间没有超过窗口的结束时间,但是距离上一次触发超过1分钟(物理时间)了,则也要触发;如果新数据的watermark时间超过了窗口的结束时间,但是距离上一次触发不够1分钟(物理时间),此时也要触发。

这里写图片描述

我们看一下具体的过程:

1、数字7到达时,其watermark时间大于12:02,因此[12:00,12:02)的窗口被触发;此时的物理时间大概在12:062、当processing time到达12:07时,此时窗口[12:02,12:04)内有2个新数据,分别是3,4,虽然此时的watermark时间还没有超过12:04(大概在12:03:50秒左右),但是这个窗口距离上次触发已经到了1分钟,因此也要触发计算,此时累加值是143、同样的道理,在物理时间12:07时,窗口[12:04,12:06)内有1个新数据3,因此也和[12:02,12:04)的窗口一起触发,结果为44、当数字3(event time12:07左右)到达时,此时processing time还不到12:08,但是其watermark时间一下子升到了12:07,因此,超过了窗口[12:02,12:04)的结束时间1204,且在此窗口内又有1个新的元素8到达,因此也要触发[12:02,12:04)的窗口计算,累加值是225、之后processing time来到了12:08分,又到了物理时间触发的时间间隔了,此时[12:06,12:08)内有1个新的数据3,因此触发计算;6、数字9的event time12:01:30秒,但是到达系统的时间已经是12:08:20秒了,此时的watermark时间已经到了12:07了(由上一步的3产生的),所以9属于late elvement,立刻触发窗口计算,[12:00,12:02)的累加值是147、此时processing time已经来到了12:09分,又到了物理时间超过1分钟的时间间隔了,此时在窗口[12:06,12:08)内有2个新的数据81,因此触发窗口[12:06,12:08)的计算,累加值是12

(g):SessionWindow的流处理

最后,我们看一下SessionWindow的流处理,我们设定1分钟的gap以及1分钟的processing time间隔:
这里写图片描述
SessionGap是针对数据的event time而言的,而AtPeriod(1,MINUTE)是针对processing time而言的。

输出的结果如下:

这里写图片描述

这里做个简单的说明:

1、processing time12:06时,57已经到达系统,且2者的event time gap时间超过了1分钟,因此属于不同的窗口,分别是572、processing time12:07时,3,4,3又到达了系统,他们最早的时间3是在12:03:40左右发生的,超过了数字7所在的窗口的结束时间(大概在12:03:30),因此这3个数据和7所在的窗口也不是同一个窗口,也发生了gap,因此统计结果是10;这时的watermark时间已经到了12:04:30秒了;3、数据8到来时,其自身的event time大概在12:03:10秒,小于此时的watermark时间,所以属于late元素,所以立刻触发其所在窗口[12:03:10,12:04:10)的计算,但是此时它的窗口范围和数字7以及(3,4,3)所在的窗口发生了重合,因此这3个窗口发生了merge,结果就只有一个session window,范围是[12:02:30,12:05:10),结果是254、数字9来到时,和数字8是一样的情况,late元素,窗口与之前和之后的都重合,因此merge成一个新的session window,范围是[12:00:30,12:05:10),输出结果是395、数字381先是为3在processing time12:08时触发,之后1分钟的gap后又撤回并计算了一次,结果是12

5、DataFlow与Flink的对比

通过论文中对DataFlow模型的理解以及对Flink的了解,我这里做个简单的比较:

1Time模型

两者在Time模型上都支持基于processing time与event time的处理;对于event time,两者也都提供一种特殊的event time:Ingestion Time。这对于不关心数据本身的时间戳的情况,而是在进入系统后打上的时间戳,会有很高的效率提升。

2、Window模型

两者都支持基于时间的窗口,默认也都是Global Window,同时各自也有些预定义的窗口:固定窗口和滑动窗口。这个概念基本一致;而且两者也都支持基于count的窗口和基于session的窗口。

3Trigger模型

两者都支持基于processing time的触发器以及watermark的触发器和基于count的触发器,默认的触发器机制都是watermark trigger。但是相对于Flink而言,DataFlow的触发器机制可以说丰富的多,具体表现在:

支持组合式触发器,例如对于watermark以及processing的组合;支持多次触发窗口,同时支持丢弃、累加、累加并撤回的触发器机制;支持以数据作为驱动的触发器,例如RPC调用完成、某些特殊数据的到达等。

虽然Flink没有提供这些预定义的trigger的实现,但是有些trigger我们也可以通过自定义trigger接口来实现,或者通过自定义的watermark来间接实现;

但是对于支持累加、丢弃、组合式等复杂的trigger,实现起来恐怕会非常复杂。

未来的Flink版本中会有对trigger的进一步改进。

6、总结

总体而言,Google Cloud DataFlow模型和Apache Flink都是统一了批处理与流处理,而且流处理的实现分别基于基于MillWheel和Stratosphere,故有很多相似之处。

虽然Google可能认为Flink的实现没有其DataFlow的全面,但是相比于其他的流处理框架,Flink在流处理上的容错、支持exactly-once处理、能够处理乱序、有状态的、支持session window、以及低延迟和高吞吐等特点看,仍然由比较大的优势。

这里写图片描述

这里写图片描述

1 0
原创粉丝点击