streaming101
awesomepaper
translate
字数10388 2021-03-02

原文:https://www.oreilly.com/radar/the-world-beyond-batch-streaming-101/

流数据处理是当今大数据世界的一大难题,理由如下:

尽管业务驱动导致流计算的兴趣日益增加,但是和批处理相比,现有的大多数流式系统仍然不够成熟,这也导致了最近该领域的很多有意思的发展。

作为一个最近5年多一直在谷歌的大规模流式系统(MillWheel, Cloud Dataflow)工作的我来说,很乐于看到这种源源不断的流计算的思潮。对于如何保证人们理解并应用流式系统,特别是在现存的批处理和流处理系统存在一定的语义鸿沟的情况下,我很感兴趣。因而接下来的内容将分为两个部分:

  1. Streaming 101:第一篇文章会介绍基本的背景情况以及一些术语,然后再深入到时域,以及对批处理和流处理的一般方法做高度的概括。
  2. Dataflow 模型:第二篇文章主要介绍 Cloud Dataflow 所使用的批流统一的模型,并以一个例子应用不同的场景来辅助说明。最后对现有的批处理和流处理系统做一个简要的语义上的比较。

闲话少说,下面进入主题。

背景

开始前,我会介绍一些重要的背景信息, 以帮助构建接下来我将要讨论的主题框架。下面我会分为三个部分:

术语: 流(streaming)是什么?

在接下来的内容之前,首先我们要解决的是:流到底是什么?“流”可以表示各种不同的意思,这可能导致我们误解流到底是什么以及流式系统究竟能做什么。鉴于此,我会尽可能地准确定义这些术语。

当前问题的关键在于许多事情应该由它们是什么(例如无限数据处理、近似结果等)来定义,然而事实上经常由它们在历史上是如何(例如通过流计算引擎)实现的来进行介绍。术语上的这种不准确导致了流的真正含义变得模糊,有时候甚至把流式系统的功能局限于流的特性,如近似和推测结果。由于精心设计的流式系统也能够和批处理系统一样产生正确、一致、可重复的结果。所以这里我倾向给“流”下一个更加精确的定义:一种针对无限数据集而设计的数据处理引擎。仅此而已。(为了防止有失偏颇,有必要说明一下这个定义同时包括了真正的流以及微批的实现方式。)

作为“流”的常见场景,下面是我常听到的,每个都被精确定义,我也建议我们的社区应该采用:

  1. 无限数据:一种不断增长的,本质上无限的数据集。这些通常被称为“流数据”(streaming data)。然而,当应用于数据集时,流或批的术语是有问题的,因为这意味着使用特定的执行引擎来处理这些数据集。两种类型的数据集之间的关键区别在于现实中它们的有限性,因此最好用这种能够区分它们特征的术语。因此,我倾向于将无限的“流”数据集称为无限数据,有限的“批”数据集叫做有限数据。
  2. 无限数据处理:一种持续的数据处理模式,适用于上述的无限数据。虽然我个人很想使用“流”这个术语来描述数据处理的类型,但是这种情况下的使用再次暗示使用了流计算引擎,这根本就是误导;而自从设计了批处理系统以来,我们就一直重复运行批处理引擎来处理无限数据(相反地,精心设计的流式系统能够比批处理系统更会处理有限数据)。因此,为了清楚定义,我将简单地称之为无限数据处理
  3. 低延迟、近似、推测结果:结果的类型往往和流计算引擎相关。事实上,批处理系统从一开始就没有被设计为低延迟或者推测结果,这是历史事实,仅此而已。当然,如果有必要的话批处理引擎完全有能力产生近似结果。因此,对于上述的术语,应该按照它们本来的样子(低延迟、近似、推测结果)来描述,这胜过说它们历史上如何表现的(通过流计算引擎)。

从现在开始,任何时候使用术语“流”,意思都是设计用于无限数据集的执行引擎,仅此而已。当我谈到上述任何其他术语时,我会直接说无限数据,无限数据处理或低延迟、近似、推测结果。这些是我们在 Cloud Dataflow 中采用的术语,同时我鼓励其他人也如此使用。

被过分夸大的流处理的局限

接下来,谈一谈流式处理系统能做什么和不能做什么,重点在能做什么;其中我最想做的事情就是讨论一个精心设计的流处理究竟可以做到什么。流式系统长期以来一直被放在提供低延迟,不准确/推测结果的场景里,通常结合批处理系统来提供最终正确的结果,即 Lambda 架构。

对于不熟悉 Lambda 架构的你来说,只需要知道它的基本思想就是在批处理系统旁边运行一个流处理系统,并且执行基本相同的计算。流式处理系统提供低延迟、不准确的结果(由于使用近似算法,或者因为流系统本身不提供准确性的保证),所以每过一段时间,批处理系统便持续滚动处理并计算出正确的结果。该思想最初是由 Twitter 的 Nathan Marz(Apache Storm 的创始人)提出的,最终是相当成功的因为在当时这是个很棒的想法;流计算引擎在正确性方面让人失望,而批处理天生笨拙,所以 Lambda 为您提供了一种方法让您鱼和熊掌兼得。然而维护 Lambda 系统是一件麻烦:需要构建和维护两个版本的管道,并且要将两者的结果合并。

和一些常年从事于强一致性流计算引擎的人一样,我对 Lambda 架构的整个概念感到有点讨厌。不出所料,当 Jay Kreps 的文章 Lambda 架构质疑 一出,我变成了他的铁粉。这是反对双模式执行的必要性的首次有力陈述。Kreps 使用 Kafka 这样的可重放系统作为流计算架构的内部连接,从而解决了可重复的问题,甚至进一步提出了 Kappa 架构,这基本意味着可以使用一个精心设计的系统来运行单一的管道。我不确定这个概念需要个名字,但是我完全支持这个概念。

老实说,我会走得更远。我会讨论一个精心设计的流式系统事实上提供了一个批处理功能之上的严格超集。感谢 Flink 的开发者将这一思想牢记于心,并构建了一个一直完全流式的系统,甚至包含批的模式。

所有这一切的必然结果是广泛成熟的流式系统,加上用于无限数据处理的健壮框架,最终 Lambda 架构将回到它所属的大数据的历史洪荒中去。我相信这将成为现实。因为在这场比赛中打败批处理,你今需要两个概念:

  1. 正确性——这让你与批处理平起平坐 首先,正确性归结为一致性存储。流式系统需要伴随时间存在的检查点来持久化状态信息(有些内容 Kreps 在他的 为什么本地状态是是流处理的基本原语 有提及),而且必须设计得足够好,能够在机器出现故障时保持一致性。当 Spark Streaming 几年前首次出现在公开的大数据场景下时,那简直是幽暗的流计算世界里一致性的灯塔。庆幸的是自那以后情况有所好转,但是仍然有不少流式系统运行在没有强一致性的情况下;我真的不敢相信至多一次处理仍然是个问题,但事实就是。 重申一下,因为这点很重要:仅仅一次的处理要求很强的一致性,这对于正确性是必要条件,而且这对于希望达到甚至超过批处理系统的能力的任何系统而言都是必需的。除非你真的不在乎结果,否则我恳求你不要使用那些不能提供强一致性状态的流式系统。批处理系统不需要你事先验证它们是否能够产生正确的结果;因而不要把时间浪费在那些不能实现相同目标的流式系统上。 如果想了解更多关于流式系统中如何实现强一致性,可以参考 MillWheel 和 Spark Streaming 论文。两篇论文花了大量时间讨论一致性。由于这些论文对此给出了高质量的介绍,本文将不会赘述。
  2. 关于时间的推理工具——这让你超越批处理 对于乱序无限数据流,数据产生的时间和数据真正被处理的时间之间的的偏差很大,用于推理时间的工具至关重要。越来越多的现代数据集体现了这个特点,现有的批处理系统(以及大多数流处理系统)缺乏必要的工具来应对这个问题。接下来以及下一篇文章,我们都将聚焦于此。 首先,我们将对时域概念有一个基本的了解,之后我们将更深入了解我所说的无限的、乱序的、不同的事件时间倾斜是什么意思。剩下的时间我们再了解使用批处理和流式系统处理有限和无限的数据的常用方法。

事件时间 vs. 处理时间

要阐述无限数据的处理方式,需要清楚地了解时间所涉及的领域。在任何数据处理系统中,通常有两种时间值得关注:

并不是所有情况下都需要关心事件的时间(如果你的不需要,万岁啊——你的生活因此更加简单),但大多数情况下需要,例如根据时间刻画用户行为,大多数计费应用程序、不同类型的异常检测。

在一个理想的世界中,事件在发生时即被处理,因而事件时间和处理时间总是相等的。然而,现实并非如此,事件时间和处理时间之间总会存在偏差,而且通常严重受到数据底层输入源,执行引擎甚至硬件的影响。可能影响的因素包括:

因此,如果要将实际系统中事件时间和处理时间的进度关系画出来的话,您得到的应该是类似于图1中红线的结果。

img

斜率为1的黑色虚线表示理想状态下,处理时间和事件时间一致;红线表示现实情况。在这个例子中,系统在处理时间的前段落后一点,在中间偏向理想状态,然后再次落后直到最后。黑色虚线与红线之间的水平距离是处理时间和事件时间之间的偏差。这种偏差本质上就是处理管道所带来的延迟。

由于事件时间和处理时间之间的关系不是静态的,如果关注数据的事件时间,那么就不能仅仅基于在你的数据管道中所观察到的时间来分析数据。不幸的是,现有的大多数系统都是为处理时间而设计的。为了处理无限数据集的无穷的特性,这些系统通常对于传入的数据提供了窗口概念。下面将深入讨论窗口,但它本质上其实就是将无限数据集沿着时间的边界切分成有限数据集。

如果您关注正确性并且希望基于事件时间分析数据,那么就不能像那些现有的系统一样来使用处理时间(即处理时间窗口)来定确定那些边界;由于处理时间和事件时间不具备一致的相关性,使用处理时间会导致一些数据划分到错误的窗口中(由于分布式系统的固有滞后,各种类型输入源的在线/离线特性等等),导致不正确的结果。我会在下面的例子以及接下来一篇文章中详细介绍这个问题。

不幸的是,按照事件时间进行窗口操作也不是那么乐观。对于事件时间窗口来说,基于无限的数据,乱序和可变的时间延迟会引入一致性的问题:处理时间和事件时间之间的关系是不可预测的,那么给定一个事件时间 X,你如何确定所有数据都到达了?对于多数真实的数据源,你恐怕无法简简单单就做到。目前使用的绝大多数数据处理系统都依赖于一些完整性的概念,这使得它们不太容易处理无限的数据集。

所以我建议与其试图将无限数据变成最终一致的有限批次数据,还不如设计一些工具让我们生活在这种不确定的世界中,从而应对这些复杂的数据集。新数据将要到达,旧数据可能会被撤回或更新,我们构建的任何系统都应该能够应对这些事实,这里使用完整性的概念是方便阐述,而不是必要的术语。

在深入研究我们是如何使用 Cloud Dataflow 中用到的 Dataflow 模型来构建这样一个系统之前,让我们先学习一些背景知识:一般的数据处理模式。

数据处理模式

至此,我们已经掌握了足够的背景知识,现在开始研究有限及无限数据处理中常见的几种处理模式。我们针对这两种计算引擎,研究它们的处理类型以及相关之处(这里指的是批处理和流式处理,我把微批处理和流处理放在了一起,因为这二者在这级别上的差异并不是很大)。

有限数据

处理有限数据是非常简单的,我们都很熟悉,例如Hadoop,在最开始都是为了处理有限数据集而出现的。在下图中,从左边开始,一个混乱无序的数据集,经过数据处理引擎(通常是批处理引擎,精心设计的流处理引擎也可以),如 MapReduce,最后在右侧产生一个更具价值的新的结构化数据集:

img

尽管作为这个方案的一部分,您实际上可以计算出无数种可能性,但整个模型是非常简单的。而更有意思的的是处理无限数据集。现在来看看处理无限数据的几种方式,从传统批处理引擎使用的方法开始,最后看看设计用于无限数据的系统使用的方法,诸如大多数流处理或微批引擎。

无限数据——批处理

批处理引擎虽然不是为无限数据集处理而设计,但是自批处理系统诞生以来都在用于处理无限数据集。而且很容想到,这种处理方式就是将无限数据集分解成适合于批处理的有限数据集。

固定窗口

处理无限数据集的最常见方法就是将输入数据分配到固定大小的窗口中,将这些窗口中作为单独、有限的数据集进行处理,然后不断运行。例如像日志这种输入源,将事件写入文件目录,名称对应所属的窗口,只要基于时间执行 shuffle,让数据分配到合适的事件时间窗口中即可。

实际上,大多数系统仍然有一个完整性的问题需要处理:如果一些事件由于网络分区而延迟该怎么办?如果需要从全球各地收集事件,必须在处理之前传输到一个公共的位置?如果事件来自移动设备又该怎么办?这意味着必须要有某种方式来解决此类问题(例如延迟处理直到确定已收集所有事件,或者在数据迟到时,为指定窗口重新处理整个批次)。

img

会话

当您面对更加复杂的窗口策略(如会话)时,还希望使用批处理引擎来处理无限数据,这样几乎是徒劳的。会话窗口通常被定义为一个活动周期,超过一定时间不再活动就认为会话窗口中止。当使用批处理引擎计算会话窗口时,通常会遇到会话的数据拆分在2个或多个批次中的情况,如下图中的红色标记所示。可以通过增加每批次数据量的大小来减少拆分数量,但是代价是增加了延迟。另一个选择是增加额外的逻辑,从之前的运行中合并会话,但带来了更高的复杂度。

img

不管怎样,使用传统的批处理引擎来计算会话都不够理想。更好的方式是以流的方式来构建会话,接下来我们一起看看吧。

无限数据——流式处理

与大多数基于批的无限数据处理方法的特殊性相反,流式系统是专为无限数据构建的。如前所述,对于现实世界的很多分布式输入数据源,不仅要处理无限的数据,而且要处理这样的数据:

处理具备这些特征的数据,一般有几种方法,我通常将它们分为以下四类:

下面我们来依次分析。

时间无关

时间无关的处理一般用于与时间无关的场景,即所有相关逻辑都是数据驱动的。由于这种情况下持续的数据输入最为重要,流处理引擎除了满足数据传输的特性外,不需要其他的。因此,基本上所有流处理系统都支持时间无关的使用场景(系统到系统的一致性保证,对关心正确性的人有用)。批处理系统非常适用于时间无关的无限数据集处理,它会通过简单地将无限数据切分成任意的有限数据集然后单独地处理它们。我们将在本节中看几个具体的例子,由于时间无关的处理简单直观,所以并不会花很多时间在上面。

过滤

一种最基本的时间无关的处理就是过滤。假设正在处理 Web 流量日志,并且要过滤掉非特定域名的所有流量。每条记录到达时,检查是否来自某个来源,如果不是则丢弃。由于这种处理只针对单个元素,而事实上数据源是无限的、乱序的以及多样的事件时间差这几个因素是无关紧要的。

img

内连接

另一个时间无关的例子是内连接(或者叫哈希连接)。当 join 两个无限数据源时,如果只关心连接的结果,则不需要关注时间。从一个数据源接收1个值,你可以直接缓存为持久化状态;一旦来自另一个源的第2个值到达时,仅需要发出连接上的记录。(在实际中,有一些记录可能没有合适的关联数据进行连接,此时可能需要基于时间进行清理掉旧数据,但是对于很少或没有未完成连接的情况,这问题不大。)

img

至于外连接则存在我们之前讨论过的数据完整性问题:一方数据到了,你怎么知道另一方数据是否会到达? 在实际中,很难确定,所以必须使用超时的概念,这也因此要引入时间元素。而时间元素在本质上就是一种窗口形式,稍后会更详细地介绍。

近似算法

img

第二类方法是近似算法,例如近似Top-N、流的K均值等。它们采用无限输入数据源,并提供输出数据。近似算法的优点在于它们的开销比较小,专为无限数据集而设计。缺点是算法本身往往是复杂的(这使得难以引出新的算法),并且这种近似的特性限制了它的用武之地。

值得注意的是:这些算法通常在其设计中考虑了时间因素(例如某种内置的衰减)。而且一般采用到达时处理元素的策略,所以通常是基于处理时间的。这一点对于可以证明近似算法的误差范围特别重要。如果这些误差范围在数据顺序到达的情况下可以证明,那么在应对不同事件时间的差距时它们什么也不是,这些算法也将无用。

近似算法是一个很有趣的主题,由于它们本质上是与时间无关的处理,因此它们非常简单易用,此处便不再赘述。

窗口

无限数据剩下的两种处理,都是基于窗口的变体。 在深入不同窗口类型的差异之前,需要明确一下窗口的含义。 窗口其实就是对数据源(不论是无限还是有限的)在时间上进行切分,得到有限的数据块。 下图显示了三种不同的窗口模式:

img

其中的处理时间和事件时间时我们主要关注的。窗口在两种时间类型中都是有意义的,接下来我们看看二者的区别。 因为处理时间在现有系统中应用最广,那么首先从处理时间开始。

基于处理时间的窗口

img

当基于处理时间划分窗口时,系统将进入的数据缓冲到窗口中,一直到窗口末尾的时间。 例如,在5分钟固定窗口的情况下,系统将缓冲数据,处理时间为5分钟,之后将其在5分钟内收到所有数据视为1个窗口,并将其发送到下游进行处理。

处理时间有以下几个优点:

此外,处理时间窗口有一个很大的缺点:如果数据中带有事件时间,而处理时间窗口需要反映出这些事件真实发生的时间的话,那么则要求这些事件以事件时间的顺序到达。可惜的是,事件时间有序的数据在大多数现实的分布式场景中并不常见。

考虑一个简单的案例,有一个移动 App 收集使用统计信息以供后续处理。如果给定的移动设备离线(短时间内连接失败,调为飞行模式等),则在该设备上线之前不会上传数据。这意味着数据可能会出现事件时间偏差几分钟、几个小时、几天、几周甚至更长时间。这时使用处理时间窗口的话是无法从这样的数据集中推断出设备离线或者其他有用的信息的。

再举一个例子,当整个系统正常时,一些分布式输入源在一切正常运转时似乎能够提供事件时间有序(或接近有序)的数据。这个时候的事件时间偏差比较小,但并不意味着会始终这样。在全球性的场景中,Web 服务跨越了多个大陆,收集数据受限于跨洋线路的带宽,进一步降低了带宽和/或提高了延迟,那么输入数据的一部分可能突然以比以前更大的偏差到达系统。如果通过处理时间窗口处理数据,则窗口不再代表其中实际发生的数据;相反,窗口代表事件到达系统的时间窗口,而这会导致旧数据和当前数据混在一起。

在这两种情况下,要想事件按照事件时间顺序的处理,需要的是事件时间窗口。

基于事件时间的窗口

在你需要以事件真实发生的时间来观察数据时,需要使用事件时间窗口。这是窗口的黄金法则。可惜的是,当今的大多数系统缺乏对于事件事件的本地支持(虽然有些具有很好的一致性模型的系统,比如 Hadoop 或者 Spark Streaming,也可以构建这样的一个窗口的系统)。

下图显示了将无限数据源中的数据,按照1小时长度的固定窗口进行切分的示例:

img

该图中的白实线标出了两个感兴趣的数据。 从图中可以看出,这两类数据都到达处理时间窗口,但是与它们所属的事件时间窗口不匹配。 因此,想按照事件时间进行处理,如果使用处理时间窗口,则计算结果将不正确。显然,使用事件时间窗口才能达到事件时间上的正确性。

另外在处理无限数据的时候,使用事件时间窗口,可以创建动态大小的窗口。例如会话窗口,此种情况下,使用固定窗口会将紧密相关联数据分隔到不同的窗口(之前在“无限数据——批处理”部分的会话窗口示例中说明过):

img

当然,强大的语义来之不易,事件时间窗口也不例外。事件时间窗口由于通常比窗口本身的实际长度(处理时间)更长,因而具有以下两个缺点:

总结

哇喔,信息量好大啊。对于看到此处的你值得表扬!到这里我们已经完成了我想要介绍的一半的内容,所以回头看看,回顾一下之前介绍过的内容,在深入到第二部分之前,我们稍微放慢脚步。第一部分是无聊的阐述,第二部分才是真正有趣的地方。

回顾

总结下,我讲过:

接下来

本文为我在第二部分的具体案例做了铺垫,接下来将包含以下几点: