此文选自Google大神Tyler Akidau的另外一篇文章:Streaming 102: The world beyond batch算法
欢迎回来!若是您错过了我之前的帖子,Streaming-大数据的将来,强烈建议您先花时间阅读那篇文章。apache
简要回顾一下,上一篇咱们介绍了Streaming,批量与流式计算,正确性与推理时间的工具,数据处理模式,事件事件与处理时间,窗口化。windows
在这篇文章中,我想进一步关注上次的数据处理模式,但更详细。网络
这里会用到一些Google Cloud Dataflow的代码片断,这是谷歌的一个框架,相似于Spark Streaming或Stormsession
。app
这里还有再说三个概念:框架
Watermarks:水印是关于事件时间的输入完整性的概念。若是到某一个时间的水印,应该是已经获取到了小于该时间的全部数据。在处理无界数据时,水印就做为处理进度的标准。机器学习
Triggers: 触发器是一种机制,用于声明窗口什么时候应该输出,触发器可灵活选择什么时候应发出输出。咱们能够随着时间的推移不断改进结果,也能够处理那些比水印晚到达的数据,改进结果。分布式
Accumulation: 累积模式指定在同一窗口中观察到的多个结果之间的关系。这些结果多是彻底脱节的,即随着时间的推移表示独立的增量,或者它们之间可能存在重叠。工具
四个新的问题: what? where? when? How?
计算什么? 但愿经过数据计算的结果,和批处理相似,构建直方图,计算总和,训练机器学习等等。
在哪里计算? 事件时间窗口能够回答这个问题,好比以前提到的(固定,滑动,会话),固然这个时间也多是处理时间。
何时处理产生结果?经过水印和触发器来回答。可能有无限的变化,常见的模式是使用水印描述给定窗口的输入是否完整,触发器指定早期和后期结果。
结果如何相关? 经过累计模式来回答,丢弃不一样的,累积产生的结果。
详细介绍Streaming 101的一些概念,并提供一些例子。
计算的结果是什么?熟悉批处理的应该很熟悉这个。
举一个例子,计算由10个值组成的简单数据集的整数和。您能够想象为求一组人的分数和,或者是计费,监控等场景。
若是您了解Spark Streaming或Flink之类的东西,那么您应该相对容易地了解Dataflow代码正在作什么。
Dataflow Java SDK 模型:
PCollections,表示能够执行并行转换的数据集(多是大量的数据集)。
PTransforms,将PCollections建立成新的PCollections。PTransforms能够执行逐元素变换,它们能够将多个元素聚合在一块儿,或者它们能够是多个PTransforms的组合。
图二 转换类型
咱们从IO源中获取消息,以KV的形式转换,最后求出分数和。示例代码以下:
PCollection<String> raw = IO.read(...); PCollection<KV<String, Integer>> input = raw.apply(ParDo.of(new ParseFn()); PCollection<KV<String, Integer>> scores = input .apply(Sum.integersPerKey());
这个过程能够是在多个机器分布式执行的,分布的将不一样时间状况的数据进行累加,输出获得最终的结果,咱们不用关心分布式的问题,只要把全部的结果集转换累加便可。
图三 x为事件时间 y为处理时间
这里咱们计算的是全部事件时间,没有进行窗口转换,所以输出矩形覆盖整个X轴,可是咱们处理无界数据时,这就不够了,咱们不能等到结束了再处理,由于永远不会结束。全部咱们须要考虑在哪里计算呢?这就须要窗口。
还记得咱们以前提过的三种窗口,固定,滑动,会话。
图四 三种窗口
咱们用刚才的例子,将其固定为两分钟的窗口。
PCollection<KV<String, Integer>> scores = input .apply(Window.into(FixedWindows.of(Duration.standardMinutes(2)))) .apply(Sum.integersPerKey());
Dataflow提供了一个统一的模型,能够在批处理和流式处理中同时工做,由于批处理实际上只是流的一个子集。
图五 窗口处理
和之前同样,输入的数据在累积,直到它们被彻底处理,而后产生输出。在这种状况下,咱们获得四个输出而不是一个输出:四个基于这个两分钟事件时间窗口中的单个输出。
如今咱们能够经过更具体的水印,触发器和累计来解决更多的问题了。
刚才的处理仍是通用的批处理方式,延迟很大,但咱们已经成功把每一个窗口的输入都计算了,咱们目前缺少一种对无限数据处理方法,还要能保证其完整性。
水印是何时处理产生结果?其实也就是咱们以前研究事件时间和处理时间的那张图。
上文图 事件时间 处理时间 水印
这条红色曲线就是水印,它随着处理时间的推移不断的去捕获事件时间。从概念上讲,咱们将其视为从处理时间到事件时间的映射。水印能够有两种类型:
完美水印:这要求咱们对的输入数据所有了解。也就没有了后期数据,全部的数据准时到达。
启发式水印:对于大部分分布式输入源,完整的了解输入数据是不可能的,这就须要启发式水印。启发式水印经过分区,分区排序等提供尽量准确的估计。因此是有可能错误的,这就须要触发器在后期解决,这个一会会讲。
下面是两个使用了不一样水印的流处理引擎:
图六 左完美 右启发
在这两种状况下,当水印经过窗口的末端时,窗口被实现。两次执行之间的主要区别在于右侧水印计算中使用的启发式算法未考虑9的值,这极大地改变了水印的形状。这些例子突出了水印的两个缺点:
太慢:若是由于网络等缘由致使有数据未处理时,只能延迟输出结果。左图比较明显,迟到的9影响了总体的进度,这对于第二个窗口[12:02,12:04]尤其明显,从窗口中的第一个值开始到咱们看到窗口的任何结果为止须要将近7分钟。而启发式水印要好一点只用了两分钟。
太快:当启发式水印错误地提早超过应有的水平时,水印以前的事件时间数据可能会在一段时间后到达,从而产生延迟数据。这就是右边示例中发生的状况:在观察到该窗口的全部输入数据以前,水印超过了第一个窗口的末尾,致使输出值不正确,正确的应该是14。这个缺点严格来讲是启发式水印的问题, 他们的启发性意味着他们有时会出错。所以,若是您关心正确性,单靠它们来肯定什么时候实现输出是不够的。
这时候咱们就须要触发器。
触发器用于声明窗口什么时候应该输出。
触发的信号包括:水印进度,处理时间进度,计数,数据触发,重复,逻辑与AND,逻辑或OR,序列。
仍是用上面的例子,咱们增长一个触发器:
PCollection<KV<String, Integer>> scores = input .apply(Window.into(FixedWindows.of(Duration.standardMinutes(2))) .triggering(AtWatermark())) .apply(Sum.integersPerKey());
这里规定了触发的状况,咱们能够考虑水印太快和太慢的状况。
太慢时,咱们假设任何给定窗口都存在稳定的传入,咱们能够周期性的触发。
太快时,能够在后期数据到达后去修正结果。若是后期数据不频繁,并不会影响性能。
最后咱们能够综合考虑,协调早期,准时,晚期的状况:
PCollection<KV<String, Integer>> scores = input .apply(Window.into(FixedWindows.of(Duration.standardMinutes(2))) .triggering( AtWatermark() .withEarlyFirings(AtPeriod(Duration.standardMinutes(1))) .withLateFirings(AtCount(1)))) .apply(Sum.integersPerKey());
生成结果以下,这个版本有了明显的改进:
图七 增长早期晚期
对于[12:02,12:04]窗口太慢的状况,每分钟定时更新。延迟时间从七分钟减小到三分半。
对于[12:00,12:02]窗口太快的状况,当值9显示较晚时,咱们当即将其合并到一个值为14的新的已更正窗格中。
可是这里有一个问题,窗口要保持多长时间呢?这里咱们须要垃圾收集机制。
在[启发式水印示例中,每一个窗口的持久状态在示例的整个生命周期,这是必要的,这样咱们才可以在他们到达时适当处理迟到的数据。可是,虽然可以保持全部持久状态直到时间结束是很棒的,但实际上,在处理无限数据源时,保持给定窗口的状态一般是不切实际的。无限, 咱们最终会耗尽磁盘空间。
所以,任何真实的无序处理系统都须要提供一些方法来限制它正在处理的窗口的生命周期。
咱们能够定义一个范围,当超出这个范围后,咱们就丢弃无用的数据。
PCollection<KV<String, Integer>> scores = input .apply(Window.into(FixedWindows.of(Duration.standardMinutes(2))) .triggering( AtWatermark() .withEarlyFirings(AtPeriod(Duration.standardMinutes(1))) .withLateFirings(AtCount(1))) .withAllowedLateness(Duration.standardMinutes(1))) .apply(Sum.integersPerKey());
一旦水印经过窗口的延迟范围,该窗口就会关闭,这意味着窗口的全部状态都将被丢弃。
图八 垃圾收集
这里的6在容许迟到的范围内,能够被收集,而9不在这个范围,就被丢弃了。
有两点要注意:
若是您正在使用可得到完美水印的数据源的数据,就不须要处理延迟数据。
即便在使用启发式水印时,若是是将有限数量聚合,并且能保证一直可控,也不用考虑窗口的寿命问题。
如今时间的问题解决了,下面咱们讨论如何累积数据。
有三种不一样的累积模式:
丢弃:当下游的消费者进行累积计算时,直接相加所要的,就能够获得最终结果。
累积:好比将来的能够覆盖以前的,一直要保持最新状态,例如Hbase这种键值对的存储。
累积和撤回:和累积相似,但更复杂。好比从新分组的状况,可能不仅是覆盖那么简单,须要先删掉以前的,再加入最新的;还有动态窗口的状况,新窗口会替换旧窗口,但数据要放在不一样的位置。
好比上图中事件时间范围[12:02,12:04],下表显示了三种累积模式:
丢弃 | 累积 | 累积和收回 | |
---|---|---|---|
窗格1:[7] | 7 | 7 | 7 |
第2页:[3,4] | 7 | 14 | 14,-7 |
第3页:[8] | 8 | 22 | 22,-14 |
观察到最后的价值 | 8 | 22 | 22 |
总和 | 22 | 51 | 22 |
**丢弃:**每一个窗格仅包含在该特定窗格期间到达的值。所以,观察到的最终值并未彻底捕获总和。可是,若是您要本身对全部独立窗格求和,那么您将获得22的正确答案。
**累积:**每一个窗格结合了特定窗格期间到达的值,加上从先前的窗格中的全部值。所以,正确观察到的最终值能够捕获22的总和。
**累积和撤回:**每一个窗格都包含新的累积模式值以及前一个窗格值的缩进。所以,观察到的最后一个(非回缩)值以及全部物化窗格的总和(包括撤回)都为您提供了22的正确答案。这就是撤回如此强大的缘由。
图九 三种累积模式
随着丢弃,累积,累积和撤回的顺序,存储和计算成本在提升,所以累积模式的选择要在正确性,延迟和成本中作出选择。
咱们已经解决了全部四个问题,What,Where,When,How。但咱们都是再事件时间的固定窗口。
因此咱们还要讨论一下处理时间中的固定窗口和事件时间中的会话窗口。
先讨论处理时间中的固定窗口,处理时间窗口很重要,缘由有两个:
有两种方法可用于实现处理时窗口:
**触发器:**忽略事件时间(即,使用跨越全部事件时间的全局窗口)并使用触发器在处理时间轴上提供该窗口的快照。
入口时间:将入口时间指定为数据到达时的事件时间,并使用正常的事件时间窗口。这基本上就像Spark Streaming目前所作的那样。
处理时间窗口的一个重大缺点是,当输入的观察顺序发生变化时,窗口的内容会发生变化。为了以更具体的方式展现,咱们将看看这三个用例:
这里咱们将两种事件时间相同而处理时间不一样的状况比较。
事件时间窗口
图10 事件时间窗口
四个窗口最终结果依然相同。
经过触发器处理时间窗口
使用全局事件时间窗口,在处理时间域按期触发,使用丢弃模式进行
图11 触发器处理时间窗口
经过入口时间处理时间窗口
当元素到达时,它们的事件时间须要在入口时被覆盖。返回使用标准的固定事件时间窗口。因为入口时间提供了计算完美水印的能力,咱们可使用默认触发器,在这种状况下,当水印经过窗口末端时,它会隐式触发一次。因为每一个窗口只有一个输出,所以累积模式可有可无。
图12 入口时间处理时间窗口
若是您关心事件实际发生的时间,您必须使用事件时间窗口,不然您的结果将毫无心义。
动态的,数据驱动的窗口,称为会话。
会话是一种特殊类型的窗口,它捕获数据中的一段活动,它们在数据分析中特别有用。
图13 会话
咱们来构建一个会话:
PCollection<KV<String, Integer>> scores = input .apply(Window.into(Sessions.withGapDuration(Duration.standardMinutes(1))) .triggering( AtWatermark() .withEarlyFirings(AtPeriod(Duration.standardMinutes(1))) .withLateFirings(AtCount(1))) .accumulatingAndRetractingFiredPanes()) .apply(Sum.integersPerKey());
咱们获得结果以下:
图14 会话窗口
当遇到值为5的第一个记录时,它被放置在一个原始会话窗口中。
到达的第二个记录是7,它一样被放入它本身的原始会话窗口,由于它不与5的窗口重叠。
同时,水印已通过了第一个窗口的末尾,因此5的值在12:06以前被实现为准时结果。此后不久,第二个窗口也被实现为具备值7的推测结果,正如处理时间达到12:06那样。
咱们接下来观察一系列记录,3,4和3,原始会话都重叠。结果,它们所有合并在一块儿,而且在12:07触发的早期触发时,发出值为10的单个窗口。
当8在此后不久到达时,它与具备值7的原始会话和具备值10的会话重叠。所以全部三个被合并在一块儿,造成具备值25的新组合会话。
当9到达时,将值为5的原始会话和值为25的会话加入到值为39的单个较大会话中。
这个很是强大的功能,Spark Streaming已经作了实现。
简单回顾一下,咱们讨论了事件时间与处理时间,窗口化,水印,触发器,累积。探索了What,When,Where,How四个问题。而最终,咱们将平衡正确性,延迟和成本问题,获得最适合本身的实时流式处理方案。
更多实时计算,Kafka等相关技术博文,欢迎关注实时流式计算