Dataflow 计算模型
Dataflow 的核心计算模型非常简单,它只有两个概念,一个叫做 ParDo,就是并行处理的意思;另一个叫做 GroupByKey,也就是按照 Key 进行分组。
ParDo
ParDo 用来进行通用的并行化处理。每个输入元素(这个元素本身有可能是一个有限的集合)都会使用一个 UDF 进行处理(在Dataflow中叫做DoFn),输出是0或多个输出元素。这个例子是把键的前缀进行展开,然后把值复制到展开后的键构成新的键值对并输出。
GroupByKey
GroupByKey 用来按 Key 把元素重新分组。
ParDo 操作因为是对每个输入的元素进行处理,因此很自然地就可以适用于无边界的数据。而 GroupByKey 操作,在把数据发送到下游进行汇总前,需要收集到指定的键对应的所有数据。如果输入源是无边界的,那么我们不知道何时才能收集到所有的数据。所以通常的解决方案是对数据使用窗口操作。
窗口
时间语义
窗口通常基于时间,时间对于窗口来说是必不可少的,在流式计算中,有 processing-time 和 event-time 两种时间语义,具体参考: 时间语义
窗口分类
-
固定窗口(Fixed Window)固定区间(互不重叠)的窗口,可以基于时间,也可以基于数量;将事件分配到不同区间的窗口中,在通过窗口边界后,窗口内的所有事件会发送给计算函数进行计算;
-
滑动窗口(Sliding Window)固定区间但可以重叠的窗口,需要指定窗口区间以及滑动步长,区间重叠意味着同一个事件会分配到不同窗口参与计算。 窗口区间决定何时触发计算,滑动步长决定何时创建一个新的窗口;
-
会话窗口(Session Window)会话窗口通常基于用户的会话,通过定义会话的超时时间,将事件分割到不同的会话中; 例如,有个客服聊天系统,如果用户超过 30 分钟没有互动,则认为一次会话结束,当客户下次进入,就是一个新的会话了。
窗口分配与合并
Dataflow 模型里,需要的不只是 GroupByKey,实际在统计数据的时候,往往需要的是 GroupByKeyAndWindow。统计一个不考虑任何时间窗口的数据,往往是没有意义的; Dataflow 模型提出:
- 从模型简化的角度上,把所有的窗口策略都当做非对齐窗口,而底层实现来负责把对齐窗口作为一个特例进行优化。
- 窗口操作可以被分隔为两个互相相关的操作:
set<Window> AssignWindows(T datum)
即窗口分配操作。这个操作把元素分配到 0 或多个窗口中去。set<window> MergeWindows(Set<Window> windows)
即窗口合并操作,这个操作在汇总时合并窗口。 而在实际的逻辑实现层面,Dataflow 最重要的两个函数,也就是 AssignWindows 函数和 MergeWindows 函数。
窗口分配
每一个原始的事件,在业务处理函数之前,其实都是(key, value, event_time)这样一个三元组。而 AssignWindows 要做的,就是把这个三元组,根据我们的处理逻辑,变成(key, value, event_time, window)这样的四元组。
需要注意的是,在窗口分配过程中,滑动窗口中存在区间重合的情况,那么在为事件分配窗口的过程中,按照上文四元组的定义,可能一个事件会变成多个事件;
窗口合并
Dataflow 里,通过 AssignWindows+MergeWindows
的组合,来进行相应的数据统计。我们还是以前面会话窗口中的案例:客户 30 分钟没有互动就算作超时。
因为要根据同一个用户的行为进行分析,所以 Key 是用户 ID 。那么对应的 Value 里,可以记录消息发送方,以及对应的消息内容。而 event_time
,则是实际消息发送的时间。对于每一个事件,我们进行 AssignWindows
的时候,都是把对应的时间窗口,设置成 [eventtime,eventtime+30)
。也就是事件发生之后的 30 分钟超时时间之内,都是这个事件对应会话的时间窗口。而在同一个 Key 的多个事件,我们可以把这些窗口合并。对于会话窗口,如果两个事件的窗口之间有重合部分,我们就可以把它们合并成一个更大的时间窗口。而如果不同事件之间的窗口没有重合,那么这两个事件就还是两个各自独立的时间窗口。
例如同一个用户下,有三个事件,发生的时间分别是 13:02
、13:14
、13:57
。那么分配窗口的时候,三个窗口会是 [13:02,13:32)
,[13:14,13:44)
以及 [13:57,14:27)
。前两个时间窗口是有重叠部分的,但是第三个时间窗口并没有重叠,对应的窗口会合并成 [13:02,13:44)
以及 [13:57,14:27)
这样两个时间窗口。
窗口的分配和合并功能,就使得 Dataflow 可以处理乱序数据。相同的数据以不同的顺序到达我们的计算节点,计算的结果仍然是相同的。并且在这个过程里,我们可以把上一次计算完的结果作为状态持久化下来,然后每一个新进入的事件,都按照 AssignWindows
和 MergeWindows
的方式不断对数据进行化简。
触发器和增量处理
基于 watermark
处理迟到数据必然会面临以下两个问题:
- 在
watermark
之后的事件如何处理? - 为了数据准确性而设置过长的
watermark
,会导致没有迟到数据也会等待过长时间,从而失去时效性。
在 Dataflow 中给出如下思路:
基于 lambda 架构的思想,尽快给出一个计算结果,然后在后续的事件处理过程中,去不断修正计算结果,在 Dataflow 中体现为 触发器(Trigger) 机制;
触发器除了定义数据何时计算,还可以定义触发之后的输出策略:
- 抛弃(Discarding)策略,也就是触发之后,对应窗口内的数据就被抛弃掉了。这意味着后续如果有窗口内的数据到达,也没法和上一次触发时候的结果进行合并计算。但这样做的好处是,每个计算节点的存储空间占用不会太大。一旦触发向下游输出计算结果了,现有的数据我们也就不需要了。比如,一个监控系统,根据本地时间去统计错误日志的数量并告警,使用这种策略就会比较合适。
- 累积(Accumulating)策略,也就是触发之后,对应窗口内的数据,仍然会持久化作为状态保存下来。当有新的日志过来,我们仍然会计算新的计算结果,并且我们可以再次触发,向下游发送新的计算结果,而下游也会用新的计算结果来覆盖掉老的计算结果。
- 累积并撤回(Accumulating & Retracting)策略,也就是我们除了“修正”计算结果之外,可能还要“撤回”计算结果。在进行累积语义的基础上,计算结果的一份复制也被保留到持久化状态中。当窗口将来再次触发时,上一次的结果值先下发做撤回处理,然后新的结果作为正常数据下发;
-
以前面的客服会话为例:原本先收到了三个事件,
13:02
、13:14
、13:57
,根据 30 分钟的会话窗口,经过合并后,窗口就变成了[13:02,13:44)
以及[13:57,14:27)
这样两个时间窗口。并且,这两个会话分别作为两条记录,向下游的不同计算节点下发了。这个时候,我们又接收到了一条姗姗来迟的新日志,日志的时间是13:40
,这个用户其实只有一个会话[13:02,14:27)
。所以,我们不仅要向下游发送一个新会话出去,还需要能够“撤回”之前已经发送的两个错误的会话。
-