Streaming 101:批处理之外的流式世界第一部分

数据流处理在大数据当中是越来越重要,主要是因为:

  • 希望得到更及时的数据,切换到流处理以实现更低的延迟。
  • 使用这种为无限数据流设计的系统更容易处理越来越普遍的海量、无限数据集。
  • 在数据到达时对其进行处理可以随着时间的推移更均匀地分散负载,从而产生更一致和可预测的资源消耗。

尽管这些业务需求驱动了流式处理的发展,但与批处理相比,现有的流式处理系统仍然相对不成熟,这使得该领域最近产生了许多令人兴奋的发展。在本篇文章将会介绍一些基本的背景信息,再深入了解有关时间详细信息之前先明确饿一些术语的真实含义,并对批处理和流式处理的常用方法进行一些高层次的概述。

1. 背景

首先,我会介绍一些重要的背景信息,这些信息会有助于理解我要讨论的其它话题,我会从如下三个方面介绍:

  • 术语:如果要对复杂话题进行准确地谈论,那么就需要对术语有准确的定义。对于某些在当前使用中有不同解释(解释不清或者有歧义)的术语,我会明确地解释它们的含义。
  • 能力:我会介绍流处理系统的缺点(能做什么以及不能做什么)。我还将提出我认为数据处理系统建设者应该具有的思路,以满足未来现代数据消费者的需求。
  • 时间概念:我会将介绍与数据处理相关的两个时间概念以及它们之间的区别,并解答这两个概念给我们带来的的一些疑惑。

1.1 术语:什么是流?

在进一步讨论之前,我们首先要弄清楚一件事情:什么是流?流这个术语在今天已经有了不同的解释,这可能会对理解什么是真正的流以及流系统能用来干什么产生误解。因此,在这里我需要明确定义什么是流。

问题的症结在于,许多东西本应该用它们是什么来进行描述(例如,无限数据处理,近似结果等),但是却通过它是如何实现进行描述(例如,通过流执行引擎)。这种缺乏精准性描述的术语会使流的真正含义变得模糊,在某些情况下,带有这种负担(缺乏精准)的流系统,意味着它们的能力只能被限制在流经常描述的特性上,诸如近似或推测性结果。考虑到设计良好的流系统与现在的批处理引擎一样都能够产生正确、一致、可重复的结果,所以我更喜欢将流定义的更具体一些:一种为无限数据集设计的数据处理引擎。为了这个定义更完整,我必须强调这个定义包含了真正的流处理和微批处理。

我听到一些关于流其他的描述,这里分别给出了更精确的描述,建议社区也应该尝试采用:

  • 无限数据(Unbounded Data):一种不断增长,实质上是无限的数据集。它们通常被称为流数据,然而,当使用流(streaming)或者批(batch)来描述数据集时,这是有问题的,正因为如上所述,流和批只是表示使用哪种执行引擎来处理数据集。实际上这两类数据集的关键区别是数据是否有限,因此最好用能够描述它们之间区别的术语来表示它们。因此,我将’流’数据集称为无限数据,将’批’数据集称为有限数据。
  • 无限数据处理(Unbounded data processing):一种数据持续处理的模式,应用在上面描述的无限数据上。尽管我个人喜欢使用术语流来描述这种类型数据的处理,但在我们这里流又意味着使用流执行引擎,这很容易产生误导。自批处理系统第一次被提出来,批处理引擎就可以通过重复运行来处理无限数据(同样,设计良好的流处理系统同样能够处理有限数据)。因此,为了能够跟真正的流区分开,这里我们称之为无限数据处理。(流处理引擎和批处理引擎都能够处理无限数据,因此无限数据处理不能使用流来描述)
  • 低延迟,近似或者推测结果:这些通常是流处理引擎的特征。批处理引擎在设计之初就没有考虑过低延迟或者推测性结果。当然,批处理引擎也完全可以产生近似结果。因此,使用这些特征描述为流是什么(低延迟,近似或推测)比通过如何实现的(通过流式引擎)要好得多。

在这里,当我使用流(streaming)这个术语时,你可以理解我想表达的是一个为无限数据集设计的处理引擎。当我指的是上述其他术语时,我会明确地说出无限数据,无限数据处理或低延迟/近似/推测结果。

1.2 能力:流的局限性

接下来,我们谈一谈流处理系统能做什么以及不能做什么,当然重点肯定放在能做什么上。流处理系统长期以来一直被认为提供低延迟,不准确/推测结果的一个小众领域,通常与功能更强大的批处理系统相结合,来提供最终的正确结果,即 Lambda 架构。

对于那些还不熟悉 Lambda 架构的人来说,Lambda 的基本的思想就是,流处理系统与批处理系统一起运行,执行一样的计算逻辑。流处理系统为你提供低延迟,不准确的结果(或者是因为使用近似算法,或者是因为流处理系统本身不能提供正确性),一段时间后,批处理系统会为你提供正确的输出。Lambda 架构最初由 Twitter 的 Nathan Marz (Storm 的创始人)提出,在当时这是一个非常好的主意。流处理引擎在正确性上可能不尽如人意,同样批处理引擎也如我们预料的那样笨重,所以 Lambda 架构给了你一个鱼与熊掌两者兼得的方法。但不幸的是,维护 Lambda 架构系统非常麻烦:你需要构建、配置和维护两套独立版本的管道,最后还需要以某种方式合并最后两套管道的结果。

作为一个花费了好几年时间研究强一致性流处理引擎的人,我也发现了 Lambda 架构的缺点。我也非常赞同 Jay Kreps 的 Lambda 架构的质疑 文章中的观点。这是第一个对双引擎执行必要性的远见陈述。Kreps 使用像 Kafka 这样的可重放系统作为流连接器解决了可重复性的问题,甚至提出了 Kappa 架构,这基本上意味着使用设计良好的系统可以只运行一个管道。我虽然不认为该想法需要一个新的名字,但原则上我完全支持该想法。说实话,我进一步认为设计良好的流处理系统可以提供比批处理更多的功能。

1.3 时间概念:事件时间与处理时间

为了说清楚无限数据处理,我们需要了解几个时间概念。在任何数据处理系统中,我们通常比较关心两个时间概念:

  • 事件时间:事件发生的时间。
  • 处理时间:在系统中观察到事件的时间。

并不是所有的用例都关心事件时间,但是很多情况下是需要考虑事件时间的。例如,随着时间的推移分析用户行为,大多数计费应用程序以及许多类型的异常检测等等。在理想的世界中,事件时间和处理时间总是相等的,事件在产生时就会被立即处理。然而事实并非如此,事件时间和处理时间之间的偏差不仅不为零,而且通常跟底层输入源、执行引擎和硬件等有一定关系。如下是几个可能影响偏差的因素:

  • 共享资源的限制,如网络拥塞,网络分裂或非专用环境中的共享 CPU。
  • 软件原因,如分布式系统逻辑等
  • 数据本身的特点,包括 Key 分布,吞吐量变化等。

因此,如果你在现实世界的系统中绘制事件时间和处理时间的进度,那么通常会得到如下图所示的曲线:

X 轴表示系统中的事件时间,Y 轴表示处理时间

斜率为 1 的黑色虚线表示理想情况,即处理时间和事件时间完全相等,而红线表示现实情况。在这个例子中,在系统刚开始的时候处理时间稍微有一些延迟,在中间的时候比较接近理想状态,后面又稍微延迟了一点。理想情况的黑线和现实情况的红线之间的水平距离就是处理时间和事件时间之间的偏差。这个偏差本质上是由流水线处理引入的延迟。

由于事件时间和处理时间之间的偏差不是固定的,这意味着如果你关心数据的事件时间(即事件实际发生的时间),你就不能在管道中看到数据时才分析你的数据。不幸的是,大多数为无限数据处理设计的系统都只考虑了处理时间。为了处理无限数据集的无限特性,这些系统通常提供输入数据上窗口的概念。我们将在下面深入讨论窗口,它实质上是沿着时间边界将数据集切成有限个片段。

如果你关心数据的正确性,并对基于事件时间的数据分析感兴趣,那么不能像现在大多数系统那样使用处理时间(即处理时间窗口)来定义这些时间边界;由于处理时间和事件时间没有一致性,一些数据可能会出现在错误的处理时间窗口中(由于分布式系统固有的延迟,许多类型的输入源的在线/离线特性,等等),从而导致计算不准确。我们会在下面的例子中以及下一篇文章中更详细地讨论这个问题。

不幸的是,即使按照事件时间划分窗口,情况也不乐观。在无限数据下,乱序和可变的偏差都会带来事件时间窗口完整性问题:在处理时间和事件时间之间缺乏可预测的映射时,我们如何确定什么时候能观察到给定事件时间 X 的所有数据?对于许多现实世界的数据源,我们根本无法确定数据是否完整。目前使用的绝大多数数据处理系统都会依赖一些完整性的概念,这使得它们在处理无限数据集时显得力不从心。

我认为,与其尝试将无限数据切分成有限批次(最终每个批次都是完整的),不如设计一个工具让我们能够解决这些复杂数据集所带来的不确定性问题。新数据会到来,旧数据可能被撤回或更新,我们设计的系统都应该能够独立应对这些情况,在这些系统中完整性概念只是一个辅助的优化,而不是语义上的必要条件。在我们深入了解如何使用 Cloud Dataflow 中 Dataflow 模型来构建这样一个系统之前,让我们先了解一个更有用的背景:通用数据处理模式。

2. 数据处理模式

到了这个时候,我们已经了解了足够的背景知识,可以开始看一下有限和无限数据处理的常见模式。我们将在我们关心的两种引擎(批处理和流处理,在这种情况下,我将微批处理与流处理放在一起,因为在这个级别上两者之间的差异并不是非常重要)的背景下研究这两种类型的处理以及相关性。

2.1 有限数据

有限数据处理比较简单,大家也都比较熟悉。在下图中,左边是一个杂乱无序的数据集,经过数据处理引擎(通常是批处理引擎,设计良好的流处理引擎也可以处理)处理,比如 MapReduce,最后生成更有价值的结构化数据集:

我们更感兴趣的是对无限数据集的处理。现在让我们来看看通常无限数据处理的各种方式,先从传统的批处理引擎使用的方法开始,最后是为无限数据设计的系统所使用的方法,如常见的流式处理或微批处理引擎。

2.2 无限数据-batch

批处理引擎虽然不是为无限数据设计的,但自从首次构思出来以来,已经被用来处理无限数据集了。正如人们所预料的那样,这种方法的核心是将无限数据分割成适合于批处理的有限数据集的集合。

2.2.1 固定窗口

使用批处理引擎处理无限数据集的最常见方法是将输入数据切分到不同固定大小的窗口中,然后将每个窗口作为单独的有限数据源进行处理。特别是对于像日志这样的输入源,事件可以写入目录和文件层次结构中,这些目录和文件的名称比较适合命名为对应的时间窗口(一个文件或者目录可以对应一个时间点的窗口),这样一眼看上去就比较简单,我们需要做的就是基于时间重新 shuffle,提前将数据分配到对应的事件时间窗口内。

但实际上,大多数系统仍然会面临完整性问题:如果由于网络分裂导致某些事件延迟到达,那该怎么办?如果你的事件是全局收集,并且在处理之前必须转移到一个共同的地点,那该怎么办?如果你的事件来自移动设备?这意味着可能需要采取某种缓解措施(例如,延迟处理,直到确保所有事件都已收集,或者只要有新数据到达就重新处理给定窗口的整个批次数据)。

一个无限数据集被预先收集到有限、固定大小的有限数据窗口中,然后通过经典批处理引擎的连续运行进行处理。

2.2.2 会话

当你尝试使用批处理引擎在更复杂的窗口(如会话窗口)中处理无限数据时,上述方法会比较糟糕。会话通常被定义为由不活动间隔终止的活动时段(针对指定用户)。使用传统的批处理引擎计算会话时,通常分割的会话会跨越多个批次,如下图中的红色所示。可以通过增加批次大小来减少分割数量,但是这样会增加延迟。另一个选择是添加额外的逻辑来拼接先前运行的会话(将断裂的会话通过逻辑处理拼接在一起),但这代价更高。

2.3 无限数据-streaming

与大多数基于批处理来处理无限数据的即席特性相反,流处理系统是专门为无限数据设计的。正如我前面提到的,对于许多现实世界的分布式输入源,你发现不仅需要处理无限数据,而且还需要处理:

  • 相对于事件时间的乱序,这意味着如果你想在事件发生时的上下文中的分析数据,你就需要在管道中进行某种基于时间的 shuffle。
  • 事件时间的不同偏差,这意味着你不能假设在一个常数时间 Y 上下浮动的时间区间([X-Y, X+Y])内看到事件时间 X 全部数据。

处理具有这些特征的数据时,可以采取一些方法。通常将这些方法分为四类:

  • 与时间无关
  • 近似算法
  • 基于处理时间的窗口
  • 基于事件时间的窗口

现在我们将花一点时间来看看这些方法。

2.3.1 与时间无关

与时间无关(Time-Agnostic)处理主要用于与时间不相关的场景下,例如,所有逻辑都是数据驱动的。因为在这种场景下所有事情都只与数据到来的多少有关系,所以除了基本的数据传输之外,流引擎不需要特别支持。现在的流处理系统基本上都开箱即用的支持这种与时间无关的场景。批处理系统也非常适合无限数据的与时间无关处理场景,只需简单的将无限数据分割为有限数据集合的序列并独立处理这些数据集合即可。考虑到与时间无关场景比较简单,我们在本文中只看一些具体的例子,除此之外不会花费太多的时间。

(1) 过滤

与时间无关(Time-Agnostic)处理的一个常见场景就是过滤。假设你正在处理 Web 流量日志,并且想要过滤掉不是来自指定域的流量。当每个记录到达时,首先看看它是否属于你感兴趣的域,如果不是就过滤掉掉。由于这种情况在任何时候都只依赖于单一元素,所以即使数据源是无限的,乱序的以及与事件时间出现不同的偏差都无所谓。

(2) 内连接

另一个与时间无关(Time-Agnostic)处理的场景是内连接。当两个无限数据源 JOIN 时,如果你只关心当一个元素从两个数据源到达时 JOIN 的结果,那么逻辑中不需要考虑时间因素。一旦从一个数据源看到一个值,你可以简单地缓存在持久存储中;一旦该值从第二个数据源中到达时,你就可以发送 JOIN 后的记录。

对于外连接来说会引入了我们上述讨论的数据完整性问题:一旦你看到了 JOIN 一边的元素,你怎么知道另一边的元素是否会到达?我们不知道,所以我们必须引入超时概念,这同时也引入了时间因素。这个时间元素本质上是一种窗口的形式,我们稍后会更仔细地看一下。

2.3.2 近似

第二种方法是近似算法,例如,Top-N 近似算法,K-means 流式算法等。它们接收无限输入数据并输出结果。如果你对结果要求不高,它们或许能满足我们的预期。近似算法的优点是,开销比较低,并且是专为无限数据设计的。缺点是算法本身往往很复杂,它们的近似性质限制了它们的实用性。

值得注意的是:这些算法通常在其设计中包含一些时间因素(例如,某种内置的衰减因子)。而且都是在元素到达时对其进行处理,因此通常是基于处理时间处理。这对于在近似值上提供某种可控的误差范围的算法来说尤为重要。如果误差范围在按顺序到达的数据上是可预测的,那么当你提供的是具有不同事件时间偏差的无序数据时,它们本质上是没有意义的。

近似算法本身就是一个让热感兴趣的话题,但由于它们本质上是与时间无关处理的例子,它们使用起来相当简单,因此我们目前的关注点没有必要进一步进行探讨。

2.3.3 窗口

剩下的两种处理无限数据的方法都是窗口的变体。在深入探讨它们之间差异之前,我应该明确地说明我说的窗口的含义,因为之前我只是简单地谈及了一下。窗口就是将数据源(有限或者无限)沿着时间边界分割成有限数据块进行处理的一个简单概念。下图显示了三种不同的窗口模式:

  • 固定窗口:固定窗口将时间划分为具有固定大小时间长度的片段。通常(如上图所示),固定窗口的分段被统一应用于整个数据集,这是对齐窗口的一个例子。在某些情况下,希望对窗口进行移动处理不同数据子集,以便随着时间的推移更均匀地分散窗口的整体负载。
  • 滑动窗口:固定窗口的一种广义形式(固定窗口是一种特殊的滑动窗口的),滑动窗口由窗口大小(固定长度)和滑动步长(固定周期)来定义。如果滑动步长小于窗口大小,那么窗口就会出现重叠。如果滑动步长等于窗口大小,就是一种固定窗口。如果滑动步长大于窗口大小,则是一种采样窗口,只能查看一段时间内数据。与固定窗口一样,滑动窗口通常也是对齐窗口,但在某些情况下可能会使用非对齐窗口来优化性能。
  • 会话窗口:会话是由事件序列组成的,如果不活跃的间隔时间大于超时时间会话被中断,会生成一个新的会话。会话通常用来分析用户行为。

核心我们关心的是处理时间和事件时间这两个时间概念。在这两个时间上的窗口都是有意义的,所以我们每个都会详细看看以及看看它们有何不同。由于基于处理时间的窗口在现在系统中非常普遍,因此我将从它开始。

2.3.3.1 基于处理时间的窗口

处理时间窗口的系统实质上将输入数据缓冲到窗口中,直到经过一定的处理时间。例如,在五分钟的固定窗口下,系统将缓冲处理时间五分钟内的数据,之后将在那五分钟内观察到的所有数据视为在一个窗口内,并将它们发送到下游进行处理。

处理时间窗口有几个很好的特性:

  • 简单:实现起来非常简单,因为你不用担心根据时间进行数据重洗。当他们到达时,你只需将其缓存起来,并在窗口关闭时向下游发送。
  • 判断窗口的完整性比较简单:系统知道窗口内的所有输入数据是否都已经到达,因此可以很简单的判断窗口的完整性。这意味着基于处理时间的窗口不需要处理延迟数据。
  • 如果想要根据观察到的去判断数据源的信息,那么处理时间窗口就是你想要的。许多监控方案属于这一类。想象一下,跟踪发送到全球 Web 服务器上的每秒请求数量,计算这些请求的速率以检测中断是处理时间窗口的完美使用场景。

除了优点之外,处理时间窗口有一个非常大的缺点:如果所讨论的数据有对应的事件时间,处理时间窗口要反映这些事件实际发生的时间,那么这些数据必须以事件时间顺序到达。不幸的是,按事件时间有序的数据在许多现实世界的分布式输入源中是不常见的。

举一个简单的例子,假设我们有一个收集统计信息以供日后处理的移动应用程序。当移动设备在一段时间内没有连接上网络时,在这段时间内记录的数据直到设备再次连上网络时才会被上传。这意味着可能会出现几分钟、几小时、几天、几周或者更长时间延迟的事件时间数据到达。在使用基于处理时间的窗口时,从这样的数据集中都不可能得出有用的推论。

在这种情况下,我们真正想要的是按照事件到达的顺序,按照事件时间对数据进行窗口化。其实我们真正想要的是基于事件时间的窗口。

2.3.3.2 基于事件时间的窗口

当你需要用反映事件实际发生时间来观察一个数据源时,你需要使用基于事件时间的窗口。令人遗憾的是,目前使用的大多数数据处理系统都缺乏原生支持。下图展示了将一个无限数据源窗口化为一小时固定窗口的示例:

图中白色实线表示我们感兴趣的两个指定数据。这两个数据到达了与它们所属事件时间窗口不匹配的处理时间窗口内。因此,如果在用户关心事件时间的用例中,将这些数据分发到处理时间窗口,那么计算结果是不正确的。正如人们所预料的那样,事件时间的准确性是使用事件时间窗口的一个好处。

在无限数据源上使用事件时间窗口的另一个好处是,你可以创建动态大小的窗口如会话窗口,而不是使用固定窗口生成会话(这样会造成一个会话分布在不同窗口中):

当然,天下没有免费的午餐,基于事件时间的窗口也不例外。基于事件时间的窗口有两个明显的缺点,这是因为窗口(在处理时间中)通常有比窗口本身的实际时间长度更长的寿命:

  • 缓存:由于延迟了窗口的生命周期,所以需要缓存更多的数据。值得庆幸的是,持久性存储通常是数据处理系统依赖的最便宜的资源类型(其他主要是CPU,网络带宽和RAM)。当使用设计良好的具有强一致持久状态和不错的内存缓存层的数据处理系统时,不必过分担心这个问题。而且,许多优化的聚合不需要缓存整个输入集合(例如,总和或平均),我们可以增量执行,可以持久存储更少的中间聚合数据。
  • 完整性:鉴于我们通常没有很好的方法知道我们何时看到窗口的所有数据,以及我们如何知道窗口的结果何时触发计算?事实上,我们根本就没有办法。对于许多类型的输入,系统可以通过类似 MillWheel 的 Watermarks (我将在第二部分中进一步讨论)给出一个合理准确的启发式窗口来完成评估。但是,在正确性至关重要的用例下(例如,计费),唯一真正的选择是为管道建设者提供一种方法让系统自己控制实现的窗口何时计算,以及能让系统随着时间修改这些结果。

3. 总结

总而言之,通过这篇文章我们能可以了解到:

  • 明确常用术语的含义,特别是将流的定义缩小到执行引擎层面,同时对通常归入流范畴的不同概念使用更具描述性的术语进行明确,例如,无限数据和近似/推测性结果。
  • 评估了设计良好的批处理和流处理系统的能力,并提出流处理实际上是批处理的严格超集,同时认为像 Lambda 架构这样基于流不如批处理的概念注定会随着流处理的发展被淘汰。
  • 提出了流处理系统赶上并最终超越批处理所必需的两个高级概念:正确性和在各时间上处理数据的能力。
  • 确定了事件时间和处理时间之间的重要差异,描述了这些差异在分析数据时所带来的困难。
  • 通过批处理和流处理引擎查看当今常用的有限和无限数据的处理方法,大致将无限方法分为:与时间无关、近似、基于处理时间的窗口和基于事件时间的窗口。

原文: Streaming 101: The world beyond batch

赏几毛白!