今日,流式数据处理是大数据里的很重要一环。原因有不少,其中包括:
- 商业(竞争)极度渴望更快的数据,而转换成流计算则是一个好的方法来降低延迟。
- 海量的、无穷数据集在现在的商业环境里变的越来越常见,而用专门设计来处理这样数据的系统来应对这些数据则更为容易。
- 在数据到达时就对他们进行处理能够更加平均地把负载进行均衡,取得更好的一致性和更可预测的计算资源消耗。
尽管业务驱动带来了对流计算兴趣的猛增,但绝大部分现有的流计算系统相比于批处理还不够成熟,而后者已经产生了很多令人激动的、多产的应用。
作为从事海量大规模流计算系统的从业者(在谷歌工作超过五年,开发了MillWheel和Cloud Dataflow),我很高兴能看到对于流计算的时代热潮。考虑到批处理系统和流计算系统在语义上的不同,我也很愿意来帮助大家来理解流计算的方方面面,如它能做什么?怎么使用它最好?O’Reilly的编辑邀请我就我在2015 Strata+Hadoop World伦敦大会上的演讲《对批处理说再见》写一些文字的东西。这就是你所看到的这篇博文。因为需要覆盖的题目很多,我把他们分成了两篇来写:
- 流计算101:本文是第一篇,主要介绍一些基础背景知识,澄清了一些技术术语。随后会进入技术细节,关注时间域的内容,并对常见的数据处理方法(包括批处理和流计算)做一个高层次的总览。
- 数据流的模型:第二篇文章将会主要介绍Cloud Dataflow所使用的统一的批处理加流计算的模型,以及此模式应用于多种数据集场景下的一个具体案例。之后,我会就现有的批处理和流计算系统做一个简单的语义比较作为整篇的结论。
好的,下面会有很长的内容,让我们变成技术狂吧。
背景
开始我会介绍一些对我们理解后文的内容很重要的背景知识。我会分三个主题来讲:
- 技术术语:为了能精确地讲解复杂的题目,必须对相关术语做精确的定义。对于一些已经被滥用的术语,我也会很明确地说明我用它们时的意思。
- 能力:我会对一些反复感受到的流计算系统的缺点做一些评论。我也会提出我所认为的数据处理系统的建造者应采用的基本思路,基于这样思路构建的系统或可以应对现代数据消费者不断增长的需求。
- 时间域:我会介绍与数据处理相关的两个主要时间域概念,解释他们是如何相关的,并给出这两个域所带来的一些难题。
技术术语:什么是流计算
在继续前行前,让我们先解决一个重要问题:“什么是流计算?”。尽管文章到这里为止我也是在随意的用着这个名词。流计算这个词有很多不同的意思,这就导致了关于到底什么是流计算或者到底流计算系统能做什么的误解。正因如此,我愿意在这里先精确地定义它。
这个问题的难点在于很多术语本应该被描述成他们是什么(例如无穷数据处理和近似结果处理),但却被描述为他们过去是怎么被实现的(例如通过流计算执行引擎)。缺乏精确的定义模糊了流计算真正的意思,在某些场合下它还被贴上了它的能力仅限于“流”的那些特征(如近似结果、推测结果处理)的标签。鉴于良好设计的流计算系统能与现有的批处理引擎一样产生准确、一致和可再现的结果,我更愿意把流计算非常明确地定义为:一种被设计来处理无穷数据集的数据处理系统引擎。仅此而已。考虑到完整性,需要强调的是这个定义不仅包含了真正的流计算实现,也包括微批处理(micro-batch)的实现。
下面是与流计算相关的其他几个经常出现的术语,我也给出了更精确和清晰的解释。希望业界能够采纳和使用。
- 无穷数据(Unbounded data):一种持续生成,本质上是无穷尽的数据集。它经常会被称为“流数据”。然而,用流和批次来定义数据集的时候就有问题了,因为如前所述,这就意味着用处理数据的引擎的类型来定义数据的类型。现实中,这两类数据的本质区别在于是否有限,因此用能体现出这个区别的词汇来定性数据就更好一些。因此我更倾向于用无穷数据来指代无限流数据集,用有穷数据来指代有限的批次数据。
- 无穷数据处理(Unbounded data processing):一种发展中的数据处理模式,应用于前面所说的无穷数据类型。尽管我本人也喜欢使用流式计算来代表这种类型的数据处理方式,但是在本文这个环境里,这个说法是误导的。用批处理引擎循环运行来处理无穷数据这个方法在批处理系统刚开始构思的时候就出现了。相反的,设计完善的流计算系统则比批处理系统更能承担处理有穷数据的工作。因此,为了清晰明了,本文里我就只用无穷数据处理。
- 低延迟,近似和/或推测性结果(Low-latency,approximate,and/or speculative results):这些结果和流处理引擎经常关联在一起。批处理系统传统上不是设计来处理低延迟或推测性结果这个事实仅仅是一个历史产物,并无它意。当然,如果想,批处理引擎也完全能产生近似结果。因此就如其他的术语,最好是用这些术语是什么来描述这些结果,而不是用历史上它们是用什么东西(通过流计算引擎)产生的来描述。
此后,文里任何地方我使用术语“流计算”,我就是指为无穷数据集所设计的处理引擎,仅此而已。当我使用上述任何术语时,我就会明确说无穷数据、无穷数据处理,或低延迟,近似和/或推测性结果。这些也是我在Cloud Dataflow里使用的术语,我也建议业界去使用。
流计算的最夸张的限制
下面让我们看看流计算系统能和不能做什么,重点是能做什么。在这个博文里我非常想让读者了解的一件事便是一个设计合理的流计算系统能做什么。长久以来,流计算系统被认为是专为提供低延迟、不精确/推测性结果的某些特定市场而设计,并配合一个更强大的批处理系统来提供最终准确的结果,如Lambda架构(Lambda Architecture)。
对于不熟悉Lambda架构的读者,它的基本思想就是与批处理系统一起运行流计算系统,同时进行几乎一样的计算。流计算系统提供低延迟、不准确的结果(或是因为使用了近似算法,或是因为流计算系统本身没能提供足够准确的结果),而一段时间之后当批处理计算完成,再给出正确的结果。这个架构最初是由推特的内森•马兹(Natan Marz,Storm的发明人)提出的,结果在当时非常成功。因为在当时这是一个非常好的主意:流计算引擎在正确性方面还令人失望,而批处理引擎则是固有的缓慢和笨重,所以Lambda就给出了一套现成的解决方案。不幸的是,维护Lambda系统是一个麻烦:需要搭建、部署、维护两套独立的数据流管道系统,并将两个系统产生的结果在最后进行某种程度的合并。
作为曾多年从事强一致流计算引擎的从业者,我认为Lambda架构的基本原理是有问题的。不出意外,我是杰伊•克雷普(Jay Krep)的博文《质问Lambda架构》的超级粉丝。很高兴的,下面是反对双模式运行必要性的很好的陈述之一。克雷普通过使用可重放的系统(如Kafka)作为流计算交汇点来解决重复性的问题,并更进一步的提出“Kafka架构”。此架构的基本思路就是使用单套合理设计的引擎作为数据流管道来处理Lambda关注的任务。虽然我并不认同这个概念需要一个名字,但是我完全支持这个观点里的基本原理。
实话实说,我愿意更进一步。我认为设计良好的流计算系统的能力是批处理系统的功能的超集(包含关系)。或许排除增量的效益,未来将不再需要如今日的批处理系统1。Flink基于这个想法开发了一套完全流计算模式的系统(同时也支持批处理模式)的做法是值得称赞的。我喜欢他们的工作!
上述思路的必然结果就是,结合了鲁棒的框架、并不断成熟的流计算系统可以充分应对无穷数据,也终将会把Lambda架构送进博物馆。我认为这个时刻已经到来。因为如果想用流计算在批处理擅长的领域打败它,你只需要能实现两件事:
1.正确性:这保证流计算能和批处理平起平坐。
本质上,准确性取决于存储的一致性。流计算系统需要一些类似于checkpoint的方法来保证长时间的持久化状态。克雷普斯(Kreps)在他的博文《为什么本地状态化是流计算系统的一个基础》讨论了这个问题。同时流计算系统还必须针对系统宕机后还能保证数据一致性进行精心的设计。几年前,当Spark刚刚出现在大数据领域的时候,它几乎就是照亮了流计算黑暗面的灯塔(译者注:因为Spark支持强一致)。在这之后,情况越来越好。但是还是有不少流计算系统被设计和开发成尽量不去支持强一致性。我实在是不能明白为什么“最多处理一次(at-most-once processing)”这样的方式仍然存在。
再次强调一遍重点:强一致性必须是“只处理一次(exactly-once processing)”,这样才能保证正确性。只有这样的系统才能追平并最终超越批处理系统。除非你对计算的结果是否正确并不介意,否则我还是请你放弃任何不能保证强一致性的流计算系统。现有的批处理系统都保证强一致性,不会让你在使用前去检查计算结果是否正确。所以也不要浪费你的时间在那些达不到这样标准的流计算系统上。
如果你很想了解如何才能在一个流计算系统里提供强一致性,我建议你去读一读MillWheel和Spark Streaming这两个链接里的文章。两篇文章都有相当的篇幅来介绍一致性。同时这个题目也有大量的文献可供参考,所以这里就不再详细讨论了。
2. 时间推理的工具:这一点让流计算超越批处理。
在处理无穷的、无序的、事件—时间分布不均衡的数据时,好的时间推理工具对于流计算系统是极其重要的。现在越来越多的数据已经呈现出上面的这些特征,而现有的批处理系统(也包括几乎所有的流计算系统)都缺少必要的工具来应对这些特性带来的难题。我会在这篇文章的余下部分和下一篇博文的大部分内容里来关注于这个题目。
首先,我会介绍时间域里的一些重要概念。随后我会深入介绍上面所说的无穷性、无序性和事件—时间分布不均衡这几个特性。在本文剩下的部分里面,我会介绍常见的处理无穷和有穷数据的方法,包括批处理和流计算两种系统。
事件时间和处理时间
为了能更好的说明无穷数据处理,就需要很非常清楚的理解时间域的内容。任何一个数据处理系统里,都包含两种典型的时间:
- 事件时间(Event time):是指事件发生的时间。
- 处理时间(Processing time):系统观察到事件发生的时间。
不是所有的应用场景都关心事件时间(如果你的场景不用,你的日子就好过多了),但大部分都关注。随便举几个例子,比如一段时间里的用户行为刻画、计费应用和很多的异常检测应用。
理想化的情况下,事件时间和处理时间应该总是相同的,即事件在它发生的同时就被处理了。但现实是残酷的,处理时间和事件时间之间的偏移不仅是非零的,还经常是由多种因素(如输入源、处理引擎和硬件)的特性所共同组合成的一个可变方程。可以影响这个偏移的因素包括:
- 共享的资源使用情况:比如网络拥塞、网络分区或共享环境里的CPU使用情况。
- 软件因素:如步分布系统逻辑、资源争夺等。
- 数据自身的特征:包括键分布、吞吐量变化、失序导致的变化(比如乘坐飞机的旅客在飞机落地后把手机从飞行模型调整到正常模式,然后某些事件才发生)。
因此,如果把在实际系统里的事件时间和处理时间的关系画出来,你很可能会得到类似图1这样的一些图。
图1:时间域对应的例子。图里X轴代表系统里的事件时间,即事件发生的时间在某一点之前的所有事件,Y轴代表事件被处理的时间,即处理某事件数据时系统的时间。泰勒•阿克道制作
图中,黑色的虚线的斜率是1,代表了理想的情况,即事件时间和处理时间是一样的。红色的线代表现实的情况。在这个例子里,系统在处理时间开始阶段有一些延迟,随后趋于理想状况的同步,最后又产生了一些延迟。在理想情况和实际情况之间的水平距离则代表了处理时间和事件时间之间的偏移。本质上,偏移就是由处理管道产生的延迟。
可见事件时间和处理时间之间的偏移并不是静态的,这就意味着如果你关注的是事件时间(比如事件确切发生的时间点),在你处理数据数据时不能只看数据被观察的时间(处理时间)。不幸的是,现在很多的流计算系统却是按照处理时间设计来处理无穷数据的。为了应对无穷数据集的无限的特性,这些系统一般都会提供一些把输入数据按时间分片的机制。下面会仔细的讨论分片机制,但其本质都是按时间把数据切割成有限的块。
如果你真正关心的是正确性并希望分析的是事件时间,你就不能用处理时间来定义数据的时间边界(比如,用处理时间来分片),虽然现有的很多流计算系统是这么做的。鉴于事件时间和系统时间之间没有一个一致的关联,某些数据可能会被错误的分到按处理时间分片的数据片里,尽管它们的事件时间并不属于这个片。这可能是由于分布式系统内在的延迟,或是由于很多数据源的在线/离线特性所造成的。但后果就是准确性就无法得到保证。下面(包括下一篇博文)我会用一些案例来更详细地讨论这个问题。
糟糕的是,即便是用事件时间来分片,情况也不那么美好。对于无穷数据,失序和偏移的变化给分片带来了另外一个问题:完整性,即既然无法预测事件时间和处理时间之间的偏移,你怎么能确定你获得了分片时间内的所有数据?对很多的真实数据,这个问题的答案是无法确定。现有的大部分数据处理系统都依赖某种完整性的想法,对于无穷数据而言这可能会带来严重的困难。
我建议与其试图去把无穷的数据梳理成有限的信息片,我们应该设计这样的工具(系统),他们可以让我生活在这些复杂数据造成的不确定性中。当新的数据到来时,我们可以抽取或者更新旧数据。任何系统都应该能应对这些不确定性,去方便的优化完整性的概念,而不只是一个口头上的必须。
在介绍我们是如何在Cloud Dataflow里面使用Dataflow模型去构建这样一个系统前,让我们再讲一些有用的背景知识:常见的数据处理模式。
数据处理模式
到目前为止,我们已经获得了足够的背景知识来开始研究处理无穷数据和有穷数据的常见的核心模型。下面我会在批处理和流计算两种引擎的环境下分别对两种处理模式进行介绍。这里我把微批处理和流计算归为一种,因为在这个层面上,他们没有什么特别大的区别。
有穷数据处理
处理有穷数据是很简单直接的,相信大家都比较的熟悉了。如下图(图2)所示,我们会先对左边非结构化的据进行操作。使用某种分析引擎(通常是批处理类型的,但一个设计良好的流计算引擎也能做的一样好),比如MapReduce,对这些数据做运算。最后得到图右边所示的有规则的结构化数据,并获得其内在的价值。
图2:用经典的批处理引擎来处理有穷数据。左边有限的非结构化数据经过一个数据处理引擎的处理,转变成了右侧的相应的结构化数据。泰勒•阿克道制作
尽管上述过程可能有无数多种变形的版本,但他们总体的模式是很简单的。更有趣的是如何处理无穷数据集,下面就让我们来看一看各种处理无穷数据的典型方法。我们从使用传统的批处理引擎开始,最后以使用专门为无穷数据集而设计的系统(例如大部分流计算或微批处理系统)来结束。
批处理引擎尽管不是设计来处理无穷数据的,但从它们诞生开始就已经被用来处理无穷数据集了。可以想像的是,这个方法一般都涉及到把无穷数据分片成一系列有穷数据集,再用批处理引擎来处理。
固定的时间窗口
批处理引擎最常见的处理无穷数据集的方法就是重复性地把输入数据按固定时间窗口分片,然后再把每个片当作一个独立有穷数据源进行处理。特别是像日志这样的数据源,事件被记录进有层级的文件系统,而日志文件的名字就对应于它们相应的时间窗口。第一感就会用这个(固定窗口)方法。因为本质上,在数据创建之前就已经进行了基于事件时间的排列来把数据写入适当的时间窗口了。
然而在实际场景中,很多系统依然需要处理完整性的问题。例如,要是由于网络原因某些事件写入日志被延迟了,怎么办?要是你的所有日志都要被传送到一个通用的存储区后才能被处理,怎么办?要是事件是从移动设备上发送来的,怎么办?这些场景都会需要对原先的处理方法进行一定的修改(例如,延迟处理知道确保所有的时间片内的事件都收集齐;或如果有数据晚到了,就对整个时间窗口内的数据再处理一次)。
图3:通过临时的固定窗口,用经典的批处理引擎来处理无穷数据。无穷数据集先通过固定的时间窗口被采集整理成有穷数据,然后再通过重复运行批处理引擎来处理。泰勒•阿克道制作
会话单元
更复杂的时间窗口策略可以是会话单元。这个方法把无穷数据进行了更细的划分,以方便批处理引擎来处理无穷数据。会话一般是被定义为活动(如某个特定用户)的时间周期,以一段时间的不活跃来判定结束。使用批处理引擎来计算会话单元时,也经常会碰到同一个会话被分到了两个单元里,就如图4里的红色块所示。这种情况是可以通过增加批次的大小来减少,但相应的延迟也就会增加。另外一个选择是增加额外的逻辑来把分到前一次运行里的会话补进本次运算,但这会带来额外的复杂度。
图4:通过临时的固定窗口并结合会话单元,用经典的批处理引擎来处理无穷数据。无穷数据集先通过固定的时间窗口被采集整理成有穷数据,并再进一步划分成不同的会话单元,然后再通过重复运行批处理引擎来处理。泰勒•阿克道制作
使用传统的批处理引擎来计算会话单元还不是最理想的方法。一个更好的方法则是用流的方式来构建会话。下面我们就来讨论。
无穷数据—流计算
与基于批次的无穷数据处理方法的临时特性相反的是,流计算系统天生就是为无穷数据开发的。如前文所说的,对于很多现实世界里的分布式数据源,你不仅要应对数据的无穷性,还要处理下面两个特性:
- 对应于事件时间的高度无序性。这意味着你需要某种程度上对事件做时间排序,如果你想按事件时间来做分析。
- 时间漂移的变化性。这意味着在一个固定的Y时间的增量里你不能假定你可以看到大部分发生在对应的X时间增量范围内的事件。
有多种可以处理有这样特性的数据集的方法。我大致把它们分成四类:
- 时间不可知(Time-agnostic)
- 近似算法(Approximation algorithms)
- 按处理时间做时间窗口分片
- 按事件时间做时间窗口分片
下面就分别看一看这几种方法。
时间不可知
时间不可知处理方法的使用场景是当时间本质上无关,所有的逻辑仅关心数据本身而非时间。因为这种场景下只关心数据的到达,所以并不需要流计算引擎来做特殊的支持,只要保证数据的传递就可以了。因此,本质上现有的所有流计算系统都支持时间不可知场景(当然对于准确性有要求的用户,还需要排除那些对强一致性的保证不支持的系统)。批处理系统也能很好的支持时间不可知的无穷数据的应用场景,只要简单地把数据分成特定的有穷数据块序列,再依次处理即可。下面会介绍一些实际的例子,但考虑到这种场景的处理比较的容易理解,我不会用过多的篇幅。
过滤(Filtering)
非常基础的一个场景就是过滤。比如你要处理网站流量日志,想过滤掉不是来自某个特定域名的所有流量。那么就只要在数据到达的时候,检查一下是不是来自那个特定的域,如果不是就丢弃掉。这种场景只依赖于数据元素本身,因此跟数据源是否是无穷的、失序的或是延迟有变化的就没有关系了。
图5:过滤无穷数据。不同类型的数据从图左向右流进,被过滤后形成了只包含一种类型数据的统一数据集。泰勒•阿克道制作。
内连接(Inner-Join)
另外一种时间不可知的应用场景就是内连接(也叫哈希连接, Hash-Join)。当连接两个无穷数据源的时候,如果你只想得到在两个数据源里都有的元素,那么这里的逻辑就跟时间无关了。在得到一个新的值后,只要简单地把它持久的缓存起来,再一直等到另外一个源里也送来这个值,然后输出即可。当然有可能这里会需要一些垃圾回收机制来把那些从来没出现的连接的元素给清理掉,这时候就跟时间有关了。但是对于那些不会出现不完全连接的场景,这个就没什么了。
图6:对两个无穷数据源做内连接。当来自两个数据源中都出现了相同的观察值后,就进行连接操作。泰勒•阿克道制作。
如果问题转变成了外连接(Outer-Join),这就会出现之前讨论的完整性的问题,即当你看到连接的一边,你怎么能知道另外一边是否会出现?事实是,你不知道!这种情况下,你就必须采用某种超时机制,而这就又涉及到了时间。这里的时间本质上就又是一种时间窗口分片,后面会仔细分析。
近似算法(Approximation algorithms)
图7:对无穷数据源进行近似计算。数据进入后通过了一个复杂的近似算法的运算,得到差不多你想要的结果。泰勒•阿克道制作。
第二类方法是近似算法,比如近似Top-N、流K-means聚类等。他们都以无穷数据为输入,并计算出差不多你想要的结果。这些近似算法的好处是它们一般开销小,而且就是设计来处理无穷数据的。缺点是这类方法数量有限,且实现都比较复杂,更新也难。近似的特性又使得它们不能广泛应用。
值得注意的是,这些算法一般都有一些时间域的特性(例如,某种衰退机制)。同时也因为这些方法一般都是在数据到达后就处理,所以它们基本用的都是处理时间。对于有些可以提供证明的错误范围的算法,这一点很重要。因为如果算法能够利用数据到达的顺序来预测错误范围,那么即便是事件—时间漂移有变化,对于无穷数据,这些错误都可以忽略不计了。请记住这一点。
近似算法本身是很有趣的话题,但它们本质上也是时间不可知方法的一种(如果不考虑它们自身带有的一些时间域特性)。而且这些算法也很直白容易理解和使用,这里就不再详细地介绍了。
时间窗口分片
另外两个无穷数据处理的方法都是时间窗口分片法的变形。在继续前,我会花一些篇幅来讲清楚时间窗口分片的具体含义是什么。分片就只是对应于一个输入数据源(无穷或有穷),按时间区间把数据分成有限个片,再来处理。图8里面给出了三种分片的方式。
图8:不同的时间窗口分片机制。每个例子都包括三个输入键对应的数据,并按不同的分片方式进行了划分,如窗口对齐的(对所有的键都适用)和窗口不对齐的(只对应于某些键的)。泰勒•阿克道制作。
- 固定窗口(Fixed windows):固定时间窗口按固定长度的时间来分片。如图8所示,固定时间窗口典型地会对所有的数据集进行划分,也叫对齐的窗口。在某些情形下,可能会希望对不同的数据子集应用不同的相位偏移,从而能让分片的完整度更加的平均。这时就不再是对齐的窗口,而是非对齐的。
- 滑动窗口(Sliding windows):滑动窗口是固定窗口的一个更一般化的形式。一般会定义两个量,即窗口大小(时间长短)和滑动时间。如果滑动时间比窗口要小,则窗口会重叠;如果相等,这就是固定窗口;如果滑动时间比窗口大,就产生了一种特殊的数据采样,也就是按时间只看数据集里的一部分子集的数据。类似于固定窗口,滑动窗口一般也是对齐的。出于性能考虑也会在某些情况下是非对齐的。需要注意的是,图8里为了能表明滑动的性质而没有把每个窗口对应到所有的键。实际情况里是都要对应到的。
- 会话单元(Sessions):是动态窗口的一种。一个会话是在不活跃时间段之间的一连串事件。这个不活跃时间一般是设定的比超时的时间要长。会话单元一般用来做用户行为分析,即观察在一个会话单元里用户的一系列事件。会话单元的长度一般都没法提前确定,完全取决于实际数据的情况。会话单元也是非对齐窗口的一个经典案例,因为实际情况下,不同子集数据的会话单元长度几乎不可能一致地对齐。
上面讨论的处理时间和事件时间是我们最关心的两个概念2。在两种情况下,时间窗口分片都可以使用。所以下面我们会详细的来看看他们的区别。由于按处理时间做窗口分片是最常见的,我们就想讲它吧。
按处理时间做时间窗口分片
图9:按处理时间对数据做固定窗口的分片。数据按照它们到达处理管道的时间(处理时间)顺序地被分成固定窗口的片。泰勒•阿克道制作。
这种方式下,系统本质上是把进来的数据进行缓存,达到一定的处理时间窗口再对缓存的数据进行处理。例如,在一个5分钟的固定窗口里,系统会按自己的系统时间缓存5分钟内的数据,然后把这5分钟内的数据视为一片,交由流程的下一步做处理。
用处理时间做窗口分片有一下几个好的特性:
- 简单。实现起来非常简单明了,不用担心数据失序和重排序。只要把数据缓存后按时交给下游就好了。
- 判断完整性很容易。因为系统能很清楚地知道某窗口里的数据是否已经全部到到,所以数据的完整性很容易保证。这就意味着系统不用操心去处理那些“晚到的”数据了。
- 如果你关心的是事件被观察到后的信息,那么按处理时间做时间窗口分片就是你所需要的方法。很多监控应用场景都可以归到这一类。比如你想获得某大型网站的每秒访问量,再通过监控这个数量来判断网站是否有服务中断,这时候用处理时间做时间窗口分片就是绝佳的选择。
尽管有这些好处,这个方法也有一个非常大的缺陷,即如果要处理的数据包含事件时间,而时间窗口需要反映的是数据的事件时间,那么就需要数据严格地按照事件时间来到达。不幸的是,在现实中这种按事件时间排好序到达的数据几乎是没有的。
举一个简单的例子,手机里的App收集上传用户的使用数据用于后期分析。当手机离网一段时间后(比如无网络连接、飞行模式等),这期间记录的数据就需要等到手机接入网络后才能上传。这意味着处理时间和事件时间就会出现从分钟到几周不等的偏移。这时候用处理时间来做时间窗口分片就没法对这样的数据做出有效的处理并产生有用的信息。
另外一个例子是有些分布式的数据源在系统正常情况下可以提供按事件时间排序好(甚至非常好)的数据。但是当系统的健康状况得不到保证的时候,就很难保证有序性了。比如某全球业务需要处理采集自多个大洲的数据。而洲际间的网络带宽一般会受限(不幸的是,这很常见),这时就会出现突然间一部分数据会比通常情况下晚到。再用按处理时间做分片,就不再能有效地反映数据实际发生时的情景了。这时窗口内的数据就已经是新旧混合的数据了。
这两个例子里,我们真正想用的都是事件的发生时间,因为这样才能保证数据到达的有序性。这就需要按事件时间进行时间窗口分片。
按事件时间做时间窗口分片
当你需要的是把事件按照发生时的时间分进有限的块内,你所需要的就是按事件时间做时间窗口分片。这是时间窗口分片的黄金标准。很不幸,目前绝大多数系统都不支持这样的方法。尽管那些支持强一致的系统(比如Hadoop和Spark)经过一些修改都可以支持这种方法。
下图就给出了一个用一小时的固定窗口对无穷数据做按事件时间分片的演示。
图10:按照事件时间用固定窗口分片。数据按照他们发生的时间收集。白色箭头指出把那些事件时间属于同一个分片的数据放到同一个窗口中去。泰勒•阿克道制作。
图里的白色箭头线对应于两个特别的数据。这两个数据先后达到处理管道的时间和他们的事件时间并不一致。如果是按照处理时间来分片处理,但实际我们关心的是事件发生时的信息,那么计算出的结果就会不正确了。如此,用事件时间分片来保证事件时间计算的正确性就很完美了。
这个方法来处理无穷数据的另外一个好处就是你可以使用动态大小窗口,比如会话单元,而不用出现前面用批处理引擎来处理会话时会出现的会话被分到两个窗口里(见图4)。
图11:按照事件时间做会话单元的窗口分片。数据按照他们发生的时间以及活动性被分到了不同的会话单元里。白色箭头指出把那些事件时间属于同一个分片的数据放到同一个窗口中去并按事件时间排序。泰勒•阿克道制作。
当然,天下没有免费的午餐,按事件时间做时间窗口分片也不例外。由于窗口必须要比窗口的长度存在更长的时间(处理时间),所以它有两个很大的缺点。
- 缓存:由于窗口的存在时间要长,所以就需要缓存更多的数据。比较好的是,现在持久化已经是整个数据处理系统资源里最便宜的部分(其他的是CPU、带宽和内存)。所以在一个设计良好的数据处理系统里,用强一致的持久化机制加上好的内存缓存机制后,这个问题可能并没想像的那么严重。另外不少的聚合运算(比如求和、算平均)都不需要把所有的数据都缓存起来,只要把很小的中间结果缓存下来,并逐步累积就可以了。
- 完整性:考虑到我们通常没有好的方法来确定已经收集到了一个窗口片里的所有数据,我们怎么知道什么时候可以把窗口里的数据交给下游去处理?事实是,我们确实不知道。对很多输入类型,系统可以给出一个相对合理准确的完整性估计,比如在MillWheel系统里使用的水印(在第二篇博文里会有更多的介绍)。但是对于绝对的准确要求极度高的场景(比如计费),唯一的选择就是提供一个方法让引擎来决定什么时候交出数据,同时能让系统不断地修正结果。应对窗口内数据的完整性是一个非常有趣的题目,但最好是能在一个具体的例子里来讨论说明,以后我会再介绍。
结论
哇噢!很多内容是不是?如果你坚持读到这里,你应该得到表扬。到这里我基本讲了我想说的一半内容。所以让我们适当地的停一下,在继续第二篇文章前,先回顾一下都讲了什么。令人高兴的是,第一篇的内容比较的沉闷,而第二篇的内容会相对有趣很多。
回顾
在上文中,我们已经:
- 澄清了一些术语,特别是把流计算的内涵局限到了执行引擎。同时把流计算这一大概念下的一些术语都用了描述性的词汇来做概念上的区别,如无穷数据、近似结果/推测结果等。
- 评估了设计良好的批处理和流计算系统的相对能力,并提出流计算系统的能力是批处理系统的超集。而类似于Lambda架构这样的主张(认为流计算比批处理要差一些)终会被成熟的流计算系统所取代。
- 提出了对于流计算系统而言两个重要的概念,实现这两个概念就可以帮助流计算系统追赶并最终超越批处理系统。他们分别是正确性和时间推理工具。
- 介绍了事件时间和处理时间的重要区别,指出了在分析数据何时发生的情况下,这些区别所带来的困难。并提出了处理方法需要从主张完整性转变到适应数据随时间变化的特性。
- 分析了目前批处理系统和流计算系统对于有穷和无穷数据的主要的数据处理方法。并大致把处理无穷数据的方法分为四类:时间不可知型、近似算法、按处理时间做时间窗口分片和按事件时间做时间窗口分片。
下一篇
本篇为第二篇博文所要的具体的案例提供了必须的上下文。在第二篇博文里,我会介绍如下的主要内容。
- 从概念上来介绍我们是如何在Dataflow模型里把数据处理的过程按四个维度进行划分:什么(what)、哪里(where)、什么时候(when)以及如何做(how)。
- 用一个简单具体的数据集来详细的介绍多个场景下的处理方法。重点强调使用Dataflow模型和各种API能处理的案例。通过这些例子我们可以更好的理解上面所说的事件时间和处理时间。同时也会介绍一个新的概念—水印(watermark)。
- 针对博文里所介绍的重点特征来比较现有的数据处理系统,从而能帮助大家更好的做选择。同时也鼓励大家对系统里有缺陷的地方进行改进,从而能最终实现我的目标,即在大数据产业中完善现有的数据处理系统,特别是流计算系统。
好,就到这里,下一篇见!
1我这里提出效益增量并不是流计算系统内在的缺陷,而只是很多的流计算系统在设计时的决策的产物。批处理系统相对于流计算系统的效益增量更多的是因为更好的打包机制和更有效的排序传输机制。现在的批处理系统开发了很多非常好的优化机制,从而能用非常便宜的硬件资源来处理海量的吞吐。没理由认为我们不能把批处理系统里这些好的机制运用到为无穷数据设计的系统里。这样用户就可以在高延迟、高效率的批处理系统与低延迟、低效率的流计算系统间做出灵活的选择了。我们在Cloud Dataflow里面就是这么做的,即使用统一的模型同时提供批处理和流计算的两个运行环境。我们使用两个运行环境主要是因为我们恰好有两套独立的系统分别为他们特殊的应用做了很好的优化。从工程的角度长远来看,我喜欢把这两个系统里的精华融合成单一的系统,同时还能保留不同的效率层次以便让用户来灵活选择。不过现在我们还没有做。实话实说由于使用了统一的Dataflow模型,可能也没有必要这么做。所以这种情况可能不会发生了。
2 如果你查看足够多的学术论文或基于SQL的流计算系统,你可能还会发现第三种时间窗口分片的机制:基于记录的窗口分片,例如窗口的大小取决于记录里元素的多少。然而,基于记录的窗口分片本质上也是一种基于处理时间的时间窗口分片。就是元素按照他们到达系统的时间来递增的赋予时间戳。有鉴于此,我就不会在这里讨论这种方式了(在第二篇文章里会有一个关于这种方式的例子)。