Apache Beam核心—触发器规约

来源:互联网 发布:node http 客户端ip 编辑:程序博客网 时间:2024/06/18 10:04

概述

本文公式化的描述了Apache Beam中触发器的语义,然后推导出在实现触发器时的限制。 目标是为Beam Runner开发者和高级的用户提供参考。

 

动机

大数据中批处理的输出结果是最终的结果,处理时间是在计算过程中的临时使用的。相比之下,流处理更关注在最终结果出来之前的中间结果。可能的方式是对输入数据进行窗口化切分,当窗口数据被认定是已经全部到达的时候,对每一个窗口产生输出,计算出一个推测性的结果,推测性结果收敛于最终结果。举例来说,当数据无限、无序和必定会存在延迟时,一般来说,获取所有的数据,计算最终的准确结果是不可行的。因此,在文档的剩余部分,所有的说明都是基于流处理,其实也一样适用于批处理。

所以如果我们不等待最终结果,我们什么时候应该产生输出?批处理是一个极端(所有数据都达到,不会再变了之后进行计算),另一个极端"每当有新信息时计算输出",换句话说,在每个输入元素之后立即计算并输出。这样做成本很高,收益不见得大,所以一般不建议这么做。不同的场景需要不同的策略。

触发器是让用户在完整性(当前窗口的是否所有数据到达),延迟(等待输出)和计算成本之间的取得平衡的一种机制。触发器以用户指定的方式调节计算步骤之间的数据流。本文并没有深入到这些动机中,而是重新回顾一下,以便给出触发器规范的上下文。本文档的其余部分以语言无关和执行引擎无关的方式,使用公式的形式定义触发器的细节。

 

触发器是什么?

此处先我们描述的触发器是什么,触发器并不限于Apache Beam中现有的触发器。在本文中使用触发器R (t 表示时间, T 表示类型, 所以使用R)表示1个满足单调性的断言PR,对于一组元素和时间t,断言PR满足条件,可以进行计算并输出结果。断言隐式的是对应于key+window组。

单调性定义如下:

PR({e1, ..., en},t) ⇒ PR({e1, ..., en, ..., en+k},t+Δ)

... 对于任何k(k个任意的新元素)和Δ(任意的一段事件时间)

断言触发产生输出的规约如下:

只要P(<该触发器上次触发以来收到的数据>,<当前Watermark>)成立,则输出结果并移动到下一个(next函数定义)触发器。

 

触发器用来管理什么时刻系统可以计算并输出结果,关于输出的细节参考Apache Beam中的延迟和窗格设计.

 

触发器语法

下边的触发器语法的定义,其中包含了大部分已经在Apache Beam中实现的触发器,并在语法中扩展了新的语义(Done).

Trigger

::=

OnceTrigger

一次性触发器

 

|

AfterEach(Trigger+)

当所有的子触发器Ready之后进入ready状态,当一个子触发器进入Done之后按照顺序移动到下一个触发器。

 

|

Repeat(Trigger)

当子触发器是Ready之后,输出结果并重置触发器。

 

|

Trigger.orFinally(OnceTrigger)

无论任何子触发器为Ready 状态,"finally"分支会一直等待直到该分支中的OnceTrigger为Ready,当该分支中的OnceTrigger进入Ready之后,进入完成状态。

    

OnceTrigger

::=

LeafTrigger

  

|

Done(LeafTrigger)

1个叶子触发器完成(Done状态)。

 

|

AfterFirst(OnceTrigger+)

当任何一个子触发器Ready的时候进入Ready状态,然后进入Done状态

 

|

AfterAll(OnceTrigger+)

当所有的子触发器为Ready状态的时候,进入Ready状态,然后进入Done状态。

    

LeafTrigger

::=

ElemCount(k)

当有k个元素到达的时候进入Ready状态,然后进入Done状态。

 

|

EndOfWindow

当时间超过窗口的末尾是进入Ready状态,然后进入Done状态。

 

|

SinceEarliestElem(Δ)

当前时间与最早的元素之间的时间超过Δ之后进入Ready状态,然后进入Done状态。

 

完成

表示1个叶子触发器完成,封装到Done(...)墓碑中。对于复合触发器,完成状态的判断是个递归判断的问题。下边的isDone()函数比较清楚的说明了状态判断的规则。

isDone(Done(...))

=

True

isDone(LeafTrigger)

=

False

isDone(AfterFirst(R1, R2, ...))

=

isDone(R1)isDone(R2) ∨ ...

isDone(AfterAll(R1, R2,...))

=

isDone(R1) ∧isDone(R2) ∧ ...

isDone(Repeat(R))

=

False (Repeat确保isDone(R)永远是false)

isDone(AfterEach(R1, R2, …))

=

isDone(R1)∧ isDone(R2) ∧ ...

isDone(R1.orFinally(R2))

=

isDone(R1)isDone(R2)

(在Apache Beam的Java SDK中,使用深度优先搜索的索引方式,在BitSet跟踪触发器的完成状态。)

 

断言

每一个触发器表示一个如下递归定义的断言,并且隐式隶属于为1个key+window特别注意AfterEachorFinally是目前为止最复杂的触发器。

PDone(R)({e1, ..., en},t)

=

True,由于rewind(下边有定义)的存在,一个触发器进入Done状态了,仍然会让复合触发器处于Ready状态。

PElemCount(k)({e1, ..., en},t)

=

n >= k

PEndOfWindow({e1, ..., en},t)

=

t >= 窗口末尾

PSinceEarliestElem(Δ)({e1, ..., en},t)

=

t - Δ >= {e1, ..., en}中的最小时间

PAfterFirst(R1,R2, …)({e1, …, en}, t)

=

PR1({e1, …, en},t) ∨ PR2({e1, …, en},t) ∨ …

PAfterAll(R1,R2, …)({e1, ..., en},t)

=

PR1({e1, …, en},t) ∧ PR2({e1, …, en},t) ∧ …

PAfterEach(R1,R2, …)({e1, ..., en},t)

=

PR({e1, ..., en},t),R是第一个未进入Done状态子触发器

PRepeat(R)({e1, ..., en},t)

=

PR({e1, ..., en},t)

PR1.orFinally(R2)({e1, …, en},t)

=

PR1({e1, …, ek},t) ∨ PR2({e1, …, en},t)

R2触发时{e1, ..., en}必须包含所有的数据元素,R1触发时{ek, ..., en} (k < n)。这意味着需要保存额外的状态,但是在触发之间仍然是单调的。

   

很明显元素个数不是一个特别的属性-只是元数据的一个例子。

(在Apache Beam的Java SDK中实现为shouldFire()方法。)

 

触发进度

PR 成立的时候,可以计算行发出结果。在这个时刻,触发器可以生成一个新的触发器来计算下一个输出。下边给出了next 的定义,来判断触发器的断言条件是否满足和触发器尚未进入Done状态。

next:

  

next(R)

=

Done(R) 如果R是叶子触发器。

next(AfterFirst(R1, R2,))

=

AfterFirst(nextIfReady(R1),nextIfReady(R2), ...)对于所有的R PR

成立。

next(AfterAll(R1, R2,))

=

AfterAll(next(R1),next(R2),)

next(AfterEach(R1, R2, …))

=

AfterEach(,next(R),)R是第一个未进入Done状态的。

next(Repeat(R))

=

Repeat(restart(R)) 如果isDone(next(R))为true, 否则Repeat(next(R))

next(R1.orFinally(R2))

=

nextIfReady(R1).orFinally(nextIfReady(R2))

nextIfReady(R)

=

如果PR 成立 ,返回next(R),否则返回R

 

重启restart:

  

restart(Done(R))

=

仍然是R ,当R是叶子触发器时

restart(R)

=

重启R 的所有子触发器

注意:

  • 如果触发器R从未发生过状态迁跃,restart(R) =R
  • 注意墓碑机制的使用。在实际中可以跳跃状态(和向后变换来重启),但是这样在数学上比较别扭。
  • next 隐式的依赖于上次输出以来的watermark和元素,因为对断言的引用。
  • next(AfterFirst(...))变换所有Ready状态的子触发器。
  • next(AfterEach(...))直观的说就是,将当前的活动触发器变换状态。
  • orFinally 的next函数,隐藏了一些复杂性:当执行nextIfReady的时候,主触发器的断言和"finally"触发器的断言应用的是不同的数据元素集合。主触发器和"finally"触发器实际上是被看做是同等地位的角色。

     

    (在Apache Beam的java SDK中实现为onFire()方法)。

     

    实现

    描述在Beam Java中如何实现触发器。

    在实现中,触发器R将通过检查来自<elements>,<event time>的相关信息的紧凑摘要状态来响应PR (<elements>,<event time>)的查询,我们以代码readyR(state)。State通过onElement(),onTimer(), onMerge()进行更新为了简单起见,假设这借个方法返回新的State

     

    合并

    当合并窗口的时候,窗口的当前触发器也需要被合并。触发器合并需要满足交换律和结合律。

    假设窗口w1w2 要被合并成一个新的窗口w1w2。现在需要将窗口w1 and w2的触发器R1R2 进行合并,并赋予窗口w1w2,R1R2的区别在于他们的完成状态/触发进度,新的触发器被称为R1R2这个Pcollection的初始Trigger被称之为RR= restart(R1)&restart(R2)。

    如果R从开头我们想合理的估计R的触发进度,规约如下:

    合并之后R1R2 触发的输出应该跟R从一开始就w1w2输出是一致的

    之前的输出取决于将输入切分成小块的数据,所以某种意义上说是任意的。但是我们希望随着R1R2 触发的进度,将关联在w1w2的数据元素进行1:1切分,然后作为一对数据输出。

    更详细的说,假设我们一个Combine.perKey().

  • 有两个输出元素<k, v1v2v3...> and <k, v4v5v6…>两个元素(丢弃模式下) 表示对总体结果的渐进聚合,然后得出总体结果<k, v1v2v3v4v5v6...>.

     

  • 最理想的的情况是不管聚合是提前发生还是延后发生,最终我们能得到正确的结果<k, v1v2v3v4v5v6...>

     

    对于触发器R(在单调性上)只有一种情况是例外的:超过了窗口末尾(EOW)。扩展窗口的末尾值为w1 的EOW 和w2的EOW (例如在会话窗口中)无法保持单调性。如果w1 andw2都没有到达窗口末尾,那么合并后的窗口也没有到达末尾。如果触发器R从合并窗口的起始位置开始执行,在需要EOW的地方,就会卡住

    • 如果两个窗口的末尾都到达了(可能是因为刚刚收到延迟的数据),则不用考虑下边的逻辑,因为所有的输出都是允许的。

       

    • 如果两个窗口的末尾都没到达,接下来的逻辑就是不做操作。

     

    所以接下来的部分适用于只有1个窗口到达末尾的情况。

    在窗口的末尾之前R1R2会在什么位置卡住?由rewind函数决定:

    rewind(Done(EndOfWindow))

    =

    EndOfWindow

    rewind(Done(R))

    =

    Done(R)如果是叶子触发器,而不是EndOfWindow

    rewind(ElemCount(k))

    =

    ElemCount(k)

    rewind(EndOfWindow)

    =

    EndOfWindow

    rewind(AfterFirst(R1, R2,))

    =

    AfterFirst(rewind(R1),rewind(R2), ...)

    rewind(AfterAll(R1, R2,))

    =

    AfterAll(rewind(R1),rewind(R2),)

    rewind(AfterEach(R1, R2, …))

    =

    AfterEach(rewind(R1),, rewind(Rk),restart(Rk+1), …)

    Rk 是第一个未处于Done状态的子触发器

    rewind(Repeat(R))

    =

    Repeat(rewind(R))

    rewind(R1.orFinally(R2))

    =

    rewind(R1).orFinally(rewind(R2))

    restart(Done(R))

    =

    R如果是叶子触发器

    restart(R)

    =

    Restart所有子触发器

    注意:

    • 如果一个窗口并没有到达窗口末尾并且触发了,那么 rewind(R) =R

       

    所以我们使用rewind(R1) 和rewind(R2)的中最靠后(further)的触发作为合并后的触发器的定义。

    R1R2

    =

    further(rewind(R1),rewind(R2))

    further(ElemCount(k),DoneElemCount(k))

    =

    DoneElemCount(k) (and etc. commutative)

    further(R1,R2)

    =

    使用further合并所有的子触发器

     

    (在Apache Beam的Java SDK中实现为onMerge() 方法.)

    由于rewind的存在如果允许延迟数据,在合并的WindowFn中数据消费者,的仍然要注意上游的复合触发的输出。我们不想推迟所有输出,以防有延迟数据到达。数据消费者需要做好以下准备:

    • 多个ON_TIME 输出但是只有1个是给合并了的窗口的。
    • ON_TIME 的输出可能会是EARLY 的在合并的窗口中。
    • LATE(延迟) 可能是ON_TIME(及时) 甚至是EARLY(提前) 在合并的窗口中

       

    数据元素和时间

    P的单调性可以由如下的规范定义:

    readyR(state) ⇒ (readyR(onElement(state)) & readyR(onTimer(state)))

    readyR(state) ⇒ (readyR(onElement(state)) and readyR(onTimer(state)))

     

    状态类型

    State本质上是对上次触发以来收到的数据元素的摘要,时间可以在任何时间由系统提供,但是数据可能会聚合之后被丢弃。

    StateT(Done(R))

    =

    StateT(ElemCount(k))

    =

    StateT(EndOfWindow)

    =

    StateT(SinceEarliestElem(Δ))

    =

    timestamp

    StateT(Repeat(R))

    =

    StateT(R)

    StateT(AfterEach(R1,…))

    =

    所有子触发器的State

    StateT(AfterFirst( ...))

    =

    所有子触发器的State

    StateT(AfterAll(...))

    =

    所有子触发器的State

    StateT(R1.orFinally(R2))

    =

    所有子触发器的State

    注意

    • Repeat(...)的子触发器完成之后,State会被清空。
    • AfterEach(...) 维护所有子触发器的状态,不考虑当前哪个子触发器是活动状态。

       

    状态更新

    很多状态的更新都是将新的State转给子触发器,所以在此将拿几个距离说明。顺便说明一下,只允许更新未处于Done状态的触发器。

    onElementElemCount(k)(n)

    =

    n+1(n始于 0)

    onElementAfterEach(…)(state1, ...)

    =

    调用每一个子触发器的onElement 方法

    注意

    • 尽管将所有的时间传递给触发器是默认行为,但是要特殊说明一下AfterEach,因为AfterEach是违反直觉的。这是为了支持further函数,触发器的状态在合并的时候,可能会向前推进。

       

    下一个状态

    当触发器调用next进入下一个触发器的时候,旧的处于Ready状态的触发器的State会被丢弃掉,因为此时State代表了已经发送的数据元素。特别注意orFinally触发器的"finally"的分支在"main"分支触发的时候,依然会保持自己的State

     

    跟踪完成状态

    在实际中,将触发器路径映射到Tree是一种简单的实现方式,用Tree来表示每个触发器是否是Done状态。对于叶子触发器,映射关系真实的反应了触发器的Done(...)状态。对于复合触发器,isDone 函数来判断状态。

     

    合并状态

    当合并窗口w1 andw2 的时候需要合并R1R2state1state2 代表尚未触发的元素{x1, …, xn} and {y1, …, ym},当前的已知事件时间是t。我们需要产生mergeR1R2(state1,state2)如下

    readyR1⊕R2(state1state2)

    PR1R2({x1, …, xn, y1, …, ym}, t)

    触发如果要合并,两个触发器必须匹配。注意,最上层的触发器是非Done状态。对于大部分触发器来说,只要简单的合并子触发器的State就行,如下几个:

    mergeElemCount(k)(m, n)

    =

    m + n

    mergeAfterEach(R1, ...)(state1, ...)

    =

    合并所有子触发器的State

    合并AfterEach的状态需要通过一个例子来说明:合并需要rewind并且通过further函数向前推进。

    AfterEach(R1, R2, R3)AfterEach(<done>,<done>, R3)

    =

    AfterEach(<done>, R2', R3)

    所以等待状态应该已经被R2'观察到。 任何一个子触发器都可能成为当前活动的触发器,所以我们需要子触发器可以理解的State。

     

    本文作者专栏:http://blog.csdn.net/ffjl1985