流式计算中的Window机制 |青训营笔记

118 阅读13分钟

这是我参与[第四届青训营]笔记创作的第4天。

一、概述

1、流式计算VS流式计算

数据价值:实行性越高,数据价值越高。

例如:测试一个程序,我们希望他跑完就出结果,而不是几个月之后给结果。

屏幕截图 2022-07-29 213742.jpg

2、批处理

批处理模型典型的数仓架构为T+1架构,即数据计算是天级别的,当天只能看到前一天的计算结果。(+1天,要想看到结果,得等到第二天。)通常使用的计算引擎为Hive或者Spark等。计算的时候,数据完全是ready的,输出和输入都是确定性的。

屏幕截图 2022-07-29 214708.jpg

  • 小时级批处理--按小时切分成更小的部分

屏幕截图 2022-07-29 214927.jpg

3、时间处理窗口

为了做到更实时,我们加入了窗口。

  • 实时计算:处理时间窗口。
  • 数据实时流动,实时计算,窗口结束直接发送结果,不需要周期调度任务。

屏幕截图 2022-07-29 215406.jpg

4、处理时间VS事件时间

  • 处理时间:数据在流式计算系统中真正处理时所在机器的当前时间。
  • 事件时间:数据产生的时间,比如客户端、传感器、后端代码等上报数据的时间。

事件时间需要watermark配合来处理乱序

屏幕截图 2022-07-29 215751.jpg

有的数据几分钟到几小时不等,所以有时候1h划分不了,所以有了流动窗口,它是根据事件时间的真实时间来计算窗口的。

5、事件时间窗口

  • 实时计算:事件时间窗口。
  • 数据实时进入真实事件发生的窗口中进行计算,可以有效的处理数据延迟和乱序。

屏幕截图 2022-07-29 220358.jpg 在图中我们可以看到数据延迟和乱序,为了可以更好的控制窗口结束时数据延迟,引用了watermark机制。

6、watermark

屏幕截图 2022-07-29 220829.jpg w(11)认为是开始数,如果在规定时间里有比11小的数,那么我们认为该数是延迟数据,不参与我们计算,

二、watermark

  • watermark表示系统认为的当前真实的事件时间。

1、如何产生watermark

屏幕截图 2022-07-29 221303.jpg 一般说从数据的事件事件来产生,产生的策略可以灵活多样,最常见的包括使用当前事件时间的时间减去一个固定的delay,来表示可以容忍多长时间的乱序。 可以通过Wm Generator来生成,API总共有三个我们这里提供了两个,还有一个是PYFlink用python写的--参见第二节。

2、如何传递Watermark

屏幕截图 2022-07-29 221618.jpg 传递就类似于Checkpoint的barrier,上下游task之间有数据传输关系的,上游就会将wm传递给下游;下游收到后,默认会取其中的最小值来作为自身的wm,同时也会将自己的wm传递给他的下游。 取上游所有subtask的最小值,像执行计划枚举一样找最小--参见第一节。

3、如何通过Flink UI观察watermark

屏幕截图 2022-07-29 222153.jpg 一般通过Flink Web UI上的信息来观察当前任务的wm的情况,看是否正常。这个问题是生产实践中最容易遇到的问题,大家在开发事件时间的窗口任务的时候,经常会忘记设置wm,或者数据太少wm没有及时更新,导致窗口一直不能触发。

4、典型问题

(1)Per-partition VS per-subtask watermark

  • 后者早期版本都是这种机制。典型的问题是如果一个source subtask 消费多个partition,那么多个partition之间的数据读取可能会加剧乱序程度。
  • 前者新版本引入了基于每个partition单独的watermark生成机制,这种机制可以有效避免上面的问题。

(2)部分partition/subtask断流

数据断流是很常见的问题,有时候业务数据本身就有这种特点,比如白天有数据,晚上没有数据。在这种情况下。wm默认是不会更新的。根据上图可知在watermask传递机制中,下游的subtask会将上游所有subtask的watermark值的最小值作为自身的watermask的值。如果上游的subtask的wm不更新的,则下游的也不更新了。

解决方案:idle source

当某个subtask断流超过配置的idle超过时间时,将当前subtask置为idle,并下发一个idle的状态给下游。下游在计算自身的wm时,可以忽略掉当前是idle的那些submask。

(3)迟到数据处理

因为wm表示当前事件发生的真实时间,那晚于wm的时间到来时,系统会认为这种数据是迟到的数据。处理时间不能产生迟到数据。

怎么定义迟到:一条数据到来后,会用WindowAssigner给他划分一个window,一般时间窗口是一个时间区间,比如【10:00,11:00),如果划分出来的Window end比当前的watermask值还小,说明这个窗口已经触发了计算,这条数据会被认为是迟到数据。注意不是数据的时间晚于wm就算迟到,而是它所属的窗口已经被触发了,才算迟到。

解决方案:算子自身来决定如何处理迟到数据

  • window聚合,默认会丢弃迟到数据。
  • 双流join,如果是outer join,则可以认为它不能join到任何数据。
  • CEP,默认丢弃。

迟到数据处理:

  • Allow lateness:这种方式需要设置一个允许迟到的时间。设置之后,窗口正常计算结束后,不会马上清理状态,而是会多保留AllowLateness这么长时间,在这段时间内如果还有数据到来,则继续之前的状态进行计算。修正过程即Retract--参见第三节。 适用于:DataStream、SQL
  • SideOutPut(侧输出流):这个方式需要对迟到数据打个tag,然后在DataStream上根据这个tag获取到迟到数据流,然后业务层面自行选择进行处理。把迟到的数据变成一个单独的流,再由用户自己来决定如何处理这部分数据。 适用于:目前只有在DateStream的窗口中才可以用,在SQL中目前没有这种语义。

屏幕截图 2022-07-29 230640.jpg

三、Window

1、基本功能

window有很多,我们来看典型的。

  • Window的使用

屏幕截图 2022-07-29 224411.jpg

(1)滚动窗口TUMBLE Window

这是最常见的窗口类型,就是根据数据的时间(可以是处理时间,也可以是事件事件)划分到它所属的窗口中。

  • 窗口划分:每个Key单独划分和每条数据只会属于一个窗口。
  • 窗口触发:window结束时间到达的时候一次性触发。

窗口结束才输出结果

屏幕截图 2022-07-29 224840.jpg

(2)滑动窗口HOP Window

上面的滚动窗口是每条数据只会落在一个窗口中,在滑动窗口中,每条数据可能会属于多个窗口的,具体属于多少,取决于窗口定义的大小和滑动。

  • 窗口划分:每个Key单独划分和每条数据可能会属于多个窗口。
  • 窗口触发:window结束时间到达的时候一次性触发。是时间大于等于window end的时候,触发对应的window的输出。计算有可能提前就增量计算好了,目前的实现是给每个window都注册一个timer,通过处理时间或者事件时间的timber来触发window的输出。

屏幕截图 2022-07-29 225033.jpg (3)会话窗口

上面两种窗口,都是根据当前数据的时间就可以直接确定它所属的窗口。会话窗口的话,是一个动态merge的过程。一般设置一个会话的最大的gap,比如10min。

  • 窗口划分:每个Key单独划分和每条数据会单独划分为一个窗口,如果window之间有交集,则会对窗口进行merge。
  • 窗口触发:window结束时间到达的时候一次性触发。

那某个key下面来第一条数据的时候,它的window就是【event_time,event_time+gap),当这个key后面来了另一条数据的时候,它会立即产生一个窗口,如果这个窗口跟之前的窗口有overlap的话,则会将两个窗口进行一个merge,变成一个更大的窗口,此时需要将之前定义的timer取消,在注册一个新的timer。 动态过程,可能结果合并,例如在十点半到十点四十接受数据,有个数据十点三十五来的,则时间改为十点半到十点四十五。另外不能确定片段所属窗口是哪个,只能确定我这条数据属于哪个。

(4)增量 VS 全量计算

  • 增量计算:数据到来,直接计算,window只存储计算结果。比如计算sum,状态中只需要存储sum的结果,不需要保存每条数据。在DataStream里要用增量计算的话,需要reduce、aggregate等函数方法就可以用到增量计算,如果用process接口,就属于全量计算。SQL中的聚合只有增量计算。

屏幕截图 2022-07-29 231506.jpg

  • 全量计算:数据到来,存储到window的state中,等到window触发计算的时候,将所有数据拿出来一起算。典型的process函数就是全量计算。

屏幕截图 2022-07-29 231609.jpg

(5)EMIT触发

通常来讲,window都是在结束的时候才能输出结果,比如1h的tumble window,只有在1h结束后才能统一输出结果。如果窗口比较大,比如一天,甚至更大的话,那计算结果输出的延迟性就比较高,失去了实时计算的意义。

  • EMIT输出指的是,在window没有结果的时候,提前把window计算的部分结果输出出来。
  • 怎么实现:在DataStream里面可以通过自定义Trigger来实现。Trigger的结果可以是continue、fire(触发计算,但是不清理)、purge、fire_and_purge。SQL也可以使用,通过配置:
table.exec.emit.early-fire.enabled=ture
table.exec.emit.early-fire-delay={time}

2、高级优化

(1)Mini-batch优化-解决频繁访问状态的问题

一般来讲,Flink的状态比较大一些都推荐使用rocksdb statebacked,这种情况下,每次的状态访问就都需要做一次序列化和反序列化,这种开销还是挺大的,为了降低这种开销,我们可以通过降低状态访问频率的方式来解决。这就是mini-batch最主要解决的问题,即攒一小批数据在进行计算,这批数据的每个key的state访问只有一次。

这个优化主要适用于没有窗口的聚合场景,字节内部也拓展了window来支持mini-batch,可以节省CPU开销。

屏幕截图 2022-07-29 232830.jpg 相似于retract机制--参见第三节

mini-batch看似简单,实际上设计非常巧妙。假设用最简单的方式实现,就是每个算子内部自己进行攒一个小的batch。这样的话,如果上下游串连的算子比较多,任务整体的延迟就不是很容易控制。

所以真正的mini-batch实现,是复用了底层的wm传递机制,通过watermark事件来作为mini划分的依据。这样整个任务中不管串联了多少个算子,整个任务的延迟都是一样的,就是用户配置的得类时间。 屏幕截图 2022-07-29 232927.jpg 频繁的序列化以及反序列化使CPU开销增大。解决方法:让中间结果偏多、状态访问频繁。

(2)倾斜优化-local-global-降低数据shuffle的量,解决倾斜的问题

所谓的local——global,就是将原本的聚合划分成两阶段,第一阶段先做一个local的聚合,这个阶段不需要数据的shuffle,是直接跟在上游算子之后进行处理的。第2个阶段,要对第1个阶段结果做一个merge。如果存在没有实现merge的聚合函数,那么这个优化就不会生效。

屏幕截图 2022-07-29 233316.jpg 常见优化

(3)Distinct计算状态复用-降低状态量

一般批里面的引擎都是通过把它优化成aggregate的方式来处理,但是在流式window中,我们不能直接这样进行优化,要不然算子就会变成下发retract的数据了。所以在流式中,对于count distinct这种情况,我们是需要保存所以数据是否出现过这样子的一个映射。

屏幕截图 2022-07-29 233453.jpg 在SQL中,我们会对同一个字段用不同的filter来进行count distinct的计算。如果每个指标都单独用一个map来记录每条数据是否出现过,那状态量是很大的。我们可以把相同字段的distinct计算用一个map的key来存储在map的value中,用一个bit vector来实现就可以把各个状态复用在一起了。MYSQL无

屏幕截图 2022-07-29 233553.jpg

比如一个bigint有64位,可以表示同一个字段的64个filter,这样整体状态量就可以节省很多了。

(4)pane优化-降低滑动窗口的状态存储量

滑动窗口如上面所述,一条数据可能会属于多个window,所以这种情况下同一个key下的window数量可能会比较多。比如3h的窗口1h滑动的话,每条数据到来会直接对这三个窗口进行计算和更新,这样对于状态访问频率是比较高的,而且计算量也会增加很多。

屏幕截图 2022-07-29 233645.jpg 优化方法是,将窗口的状态划分为更小粒度的pane。比如上面3h的窗口,1h的滑动情况。可以把pane设置位1h,这样每来一条数据,我们就只更新这条数据对应的pane的结果就可以了,当窗口需要输出结果的时候,只需要将这个窗口对应的pane的结果merge起来就可以了。注意:这里也是需要所有聚合函数都有merge来实现的。

屏幕截图 2022-07-29 233722.jpg 进一步细化时间

四、案例分析

(1)使用Flink SQL计算抖音日活跃人数曲线

滚动窗口+mini优化 结果提前输出

屏幕截图 2022-07-29 234157.jpg 红色代码是字节后加的。通过两阶段聚合来把数据打散,完成第一轮聚合,第二轮聚合只需要对各个分桶的结果求和即可。两阶段聚合解决倾斜问题。

(2)使用Flink SQL计算大数据任务的资源使用

  • 问题描述:大数据任务(离线任务)运行时通常会有多个container启动并且运行,每个container在运行结束后,YARN会负责将它的资源使用情况(CPU、内存)上报。一般大数据任务运行时间从几分钟到几小时不等。
  • 需求:根据YARN上报的各个container的信息,在任务结束时,尽快计算出一个任务运行所消耗的总的资源。假设前后两个container的结束时间差不超过10min。
select
    application_id
    sum(cpu_usage) as cpu_total
    sum(memory_usage) as memory_total,
for resource_usage
group by
    application_id,
    session(event_time,interval '10' minute)

典型的可以通过会话窗口来将数据划分到一个window中,然后再将结果求和即可。