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