数据流处理在大数据当中是越来越重要,主要是因为:
尽管这些业务需求驱动了流式处理的发展,但与批处理相比,现有的流式处理系统仍然相对不成熟,这使得该领域最近产生了许多令人兴奋的发展。在本篇文章将会介绍一些基本的背景信息,再深入了解有关时间详细信息之前先明确饿一些术语的真实含义,并对批处理和流式处理的常用方法进行一些高层次的概述。
首先,我会介绍一些重要的背景信息,这些信息会有助于理解我要讨论的其它话题,我会从如下三个方面介绍:
在进一步讨论之前,我们首先要弄清楚一件事情:什么是流?流这个术语在今天已经有了不同的解释,这可能会对理解什么是真正的流以及流系统能用来干什么产生误解。因此,在这里我需要明确定义什么是流。
问题的症结在于,许多东西本应该用它们是什么来进行描述(例如,无限数据处理,近似结果等),但是却通过它是如何实现进行描述(例如,通过流执行引擎)。这种缺乏精准性描述的术语会使流的真正含义变得模糊,在某些情况下,带有这种负担(缺乏精准)的流系统,意味着它们的能力只能被限制在流经常描述的特性上,诸如近似或推测性结果。考虑到设计良好的流系统与现在的批处理引擎一样都能够产生正确、一致、可重复的结果,所以我更喜欢将流定义的更具体一些:一种为无限数据集设计的数据处理引擎。为了这个定义更完整,我必须强调这个定义包含了真正的流处理和微批处理。
我听到一些关于流其他的描述,这里分别给出了更精确的描述,建议社区也应该尝试采用:
在这里,当我使用流(streaming)这个术语时,你可以理解我想表达的是一个为无限数据集设计的处理引擎。当我指的是上述其他术语时,我会明确地说出无限数据,无限数据处理或低延迟/近似/推测结果。
接下来,我们谈一谈流处理系统能做什么以及不能做什么,当然重点肯定放在能做什么上。流处理系统长期以来一直被认为提供低延迟,不准确/推测结果的一个小众领域,通常与功能更强大的批处理系统相结合,来提供最终的正确结果,即 Lambda 架构。
对于那些还不熟悉 Lambda 架构的人来说,Lambda 的基本的思想就是,流处理系统与批处理系统一起运行,执行一样的计算逻辑。流处理系统为你提供低延迟,不准确的结果(或者是因为使用近似算法,或者是因为流处理系统本身不能提供正确性),一段时间后,批处理系统会为你提供正确的输出。Lambda 架构最初由 Twitter 的 Nathan Marz (Storm 的创始人)提出,在当时这是一个非常好的主意。流处理引擎在正确性上可能不尽如人意,同样批处理引擎也如我们预料的那样笨重,所以 Lambda 架构给了你一个鱼与熊掌两者兼得的方法。但不幸的是,维护 Lambda 架构系统非常麻烦:你需要构建、配置和维护两套独立版本的管道,最后还需要以某种方式合并最后两套管道的结果。
作为一个花费了好几年时间研究强一致性流处理引擎的人,我也发现了 Lambda 架构的缺点。我也非常赞同 Jay Kreps 的 Lambda 架构的质疑 文章中的观点。这是第一个对双引擎执行必要性的远见陈述。Kreps 使用像 Kafka 这样的可重放系统作为流连接器解决了可重复性的问题,甚至提出了 Kappa 架构,这基本上意味着使用设计良好的系统可以只运行一个管道。我虽然不认为该想法需要一个新的名字,但原则上我完全支持该想法。说实话,我进一步认为设计良好的流处理系统可以提供比批处理更多的功能。
为了说清楚无限数据处理,我们需要了解几个时间概念。在任何数据处理系统中,我们通常比较关心两个时间概念:
并不是所有的用例都关心事件时间,但是很多情况下是需要考虑事件时间的。例如,随着时间的推移分析用户行为,大多数计费应用程序以及许多类型的异常检测等等。在理想的世界中,事件时间和处理时间总是相等的,事件在产生时就会被立即处理。然而事实并非如此,事件时间和处理时间之间的偏差不仅不为零,而且通常跟底层输入源、执行引擎和硬件等有一定关系。如下是几个可能影响偏差的因素:
因此,如果你在现实世界的系统中绘制事件时间和处理时间的进度,那么通常会得到如下图所示的曲线:
X 轴表示系统中的事件时间,Y 轴表示处理时间
斜率为 1 的黑色虚线表示理想情况,即处理时间和事件时间完全相等,而红线表示现实情况。在这个例子中,在系统刚开始的时候处理时间稍微有一些延迟,在中间的时候比较接近理想状态,后面又稍微延迟了一点。理想情况的黑线和现实情况的红线之间的水平距离就是处理时间和事件时间之间的偏差。这个偏差本质上是由流水线处理引入的延迟。
由于事件时间和处理时间之间的偏差不是固定的,这意味着如果你关心数据的事件时间(即事件实际发生的时间),你就不能在管道中看到数据时才分析你的数据。不幸的是,大多数为无限数据处理设计的系统都只考虑了处理时间。为了处理无限数据集的无限特性,这些系统通常提供输入数据上窗口的概念。我们将在下面深入讨论窗口,它实质上是沿着时间边界将数据集切成有限个片段。
如果你关心数据的正确性,并对基于事件时间的数据分析感兴趣,那么不能像现在大多数系统那样使用处理时间(即处理时间窗口)来定义这些时间边界;由于处理时间和事件时间没有一致性,一些数据可能会出现在错误的处理时间窗口中(由于分布式系统固有的延迟,许多类型的输入源的在线/离线特性,等等),从而导致计算不准确。我们会在下面的例子中以及下一篇文章中更详细地讨论这个问题。
不幸的是,即使按照事件时间划分窗口,情况也不乐观。在无限数据下,乱序和可变的偏差都会带来事件时间窗口完整性问题:在处理时间和事件时间之间缺乏可预测的映射时,我们如何确定什么时候能观察到给定事件时间 X 的所有数据?对于许多现实世界的数据源,我们根本无法确定数据是否完整。目前使用的绝大多数数据处理系统都会依赖一些完整性的概念,这使得它们在处理无限数据集时显得力不从心。
我认为,与其尝试将无限数据切分成有限批次(最终每个批次都是完整的),不如设计一个工具让我们能够解决这些复杂数据集所带来的不确定性问题。新数据会到来,旧数据可能被撤回或更新,我们设计的系统都应该能够独立应对这些情况,在这些系统中完整性概念只是一个辅助的优化,而不是语义上的必要条件。在我们深入了解如何使用 Cloud Dataflow 中 Dataflow 模型来构建这样一个系统之前,让我们先了解一个更有用的背景:通用数据处理模式。
到了这个时候,我们已经了解了足够的背景知识,可以开始看一下有限和无限数据处理的常见模式。我们将在我们关心的两种引擎(批处理和流处理,在这种情况下,我将微批处理与流处理放在一起,因为在这个级别上两者之间的差异并不是非常重要)的背景下研究这两种类型的处理以及相关性。
有限数据处理比较简单,大家也都比较熟悉。在下图中,左边是一个杂乱无序的数据集,经过数据处理引擎(通常是批处理引擎,设计良好的流处理引擎也可以处理)处理,比如 MapReduce,最后生成更有价值的结构化数据集:
我们更感兴趣的是对无限数据集的处理。现在让我们来看看通常无限数据处理的各种方式,先从传统的批处理引擎使用的方法开始,最后是为无限数据设计的系统所使用的方法,如常见的流式处理或微批处理引擎。
批处理引擎虽然不是为无限数据设计的,但自从首次构思出来以来,已经被用来处理无限数据集了。正如人们所预料的那样,这种方法的核心是将无限数据分割成适合于批处理的有限数据集的集合。
使用批处理引擎处理无限数据集的最常见方法是将输入数据切分到不同固定大小的窗口中,然后将每个窗口作为单独的有限数据源进行处理。特别是对于像日志这样的输入源,事件可以写入目录和文件层次结构中,这些目录和文件的名称比较适合命名为对应的时间窗口(一个文件或者目录可以对应一个时间点的窗口),这样一眼看上去就比较简单,我们需要做的就是基于时间重新 shuffle,提前将数据分配到对应的事件时间窗口内。
但实际上,大多数系统仍然会面临完整性问题:如果由于网络分裂导致某些事件延迟到达,那该怎么办?如果你的事件是全局收集,并且在处理之前必须转移到一个共同的地点,那该怎么办?如果你的事件来自移动设备?这意味着可能需要采取某种缓解措施(例如,延迟处理,直到确保所有事件都已收集,或者只要有新数据到达就重新处理给定窗口的整个批次数据)。
一个无限数据集被预先收集到有限、固定大小的有限数据窗口中,然后通过经典批处理引擎的连续运行进行处理。
当你尝试使用批处理引擎在更复杂的窗口(如会话窗口)中处理无限数据时,上述方法会比较糟糕。会话通常被定义为由不活动间隔终止的活动时段(针对指定用户)。使用传统的批处理引擎计算会话时,通常分割的会话会跨越多个批次,如下图中的红色所示。可以通过增加批次大小来减少分割数量,但是这样会增加延迟。另一个选择是添加额外的逻辑来拼接先前运行的会话(将断裂的会话通过逻辑处理拼接在一起),但这代价更高。
与大多数基于批处理来处理无限数据的即席特性相反,流处理系统是专门为无限数据设计的。正如我前面提到的,对于许多现实世界的分布式输入源,你发现不仅需要处理无限数据,而且还需要处理:
处理具有这些特征的数据时,可以采取一些方法。通常将这些方法分为四类:
现在我们将花一点时间来看看这些方法。
与时间无关(Time-Agnostic)处理主要用于与时间不相关的场景下,例如,所有逻辑都是数据驱动的。因为在这种场景下所有事情都只与数据到来的多少有关系,所以除了基本的数据传输之外,流引擎不需要特别支持。现在的流处理系统基本上都开箱即用的支持这种与时间无关的场景。批处理系统也非常适合无限数据的与时间无关处理场景,只需简单的将无限数据分割为有限数据集合的序列并独立处理这些数据集合即可。考虑到与时间无关场景比较简单,我们在本文中只看一些具体的例子,除此之外不会花费太多的时间。
(1) 过滤
与时间无关(Time-Agnostic)处理的一个常见场景就是过滤。假设你正在处理 Web 流量日志,并且想要过滤掉不是来自指定域的流量。当每个记录到达时,首先看看它是否属于你感兴趣的域,如果不是就过滤掉掉。由于这种情况在任何时候都只依赖于单一元素,所以即使数据源是无限的,乱序的以及与事件时间出现不同的偏差都无所谓。
(2) 内连接
另一个与时间无关(Time-Agnostic)处理的场景是内连接。当两个无限数据源 JOIN 时,如果你只关心当一个元素从两个数据源到达时 JOIN 的结果,那么逻辑中不需要考虑时间因素。一旦从一个数据源看到一个值,你可以简单地缓存在持久存储中;一旦该值从第二个数据源中到达时,你就可以发送 JOIN 后的记录。
对于外连接来说会引入了我们上述讨论的数据完整性问题:一旦你看到了 JOIN 一边的元素,你怎么知道另一边的元素是否会到达?我们不知道,所以我们必须引入超时
概念,这同时也引入了时间因素。这个时间元素本质上是一种窗口的形式,我们稍后会更仔细地看一下。
第二种方法是近似算法,例如,Top-N 近似算法,K-means 流式算法等。它们接收无限输入数据并输出结果。如果你对结果要求不高,它们或许能满足我们的预期。近似算法的优点是,开销比较低,并且是专为无限数据设计的。缺点是算法本身往往很复杂,它们的近似性质限制了它们的实用性。
值得注意的是:这些算法通常在其设计中包含一些时间因素(例如,某种内置的衰减因子)。而且都是在元素到达时对其进行处理,因此通常是基于处理时间处理。这对于在近似值上提供某种可控的误差范围的算法来说尤为重要。如果误差范围在按顺序到达的数据上是可预测的,那么当你提供的是具有不同事件时间偏差的无序数据时,它们本质上是没有意义的。
近似算法本身就是一个让热感兴趣的话题,但由于它们本质上是与时间无关处理的例子,它们使用起来相当简单,因此我们目前的关注点没有必要进一步进行探讨。
剩下的两种处理无限数据的方法都是窗口的变体。在深入探讨它们之间差异之前,我应该明确地说明我说的窗口的含义,因为之前我只是简单地谈及了一下。窗口就是将数据源(有限或者无限)沿着时间边界分割成有限数据块进行处理的一个简单概念。下图显示了三种不同的窗口模式:
核心我们关心的是处理时间和事件时间这两个时间概念。在这两个时间上的窗口都是有意义的,所以我们每个都会详细看看以及看看它们有何不同。由于基于处理时间的窗口在现在系统中非常普遍,因此我将从它开始。
处理时间窗口的系统实质上将输入数据缓冲到窗口中,直到经过一定的处理时间。例如,在五分钟的固定窗口下,系统将缓冲处理时间五分钟内的数据,之后将在那五分钟内观察到的所有数据视为在一个窗口内,并将它们发送到下游进行处理。
处理时间窗口有几个很好的特性:
除了优点之外,处理时间窗口有一个非常大的缺点:如果所讨论的数据有对应的事件时间,处理时间窗口要反映这些事件实际发生的时间,那么这些数据必须以事件时间顺序到达。不幸的是,按事件时间有序的数据在许多现实世界的分布式输入源中是不常见的。
举一个简单的例子,假设我们有一个收集统计信息以供日后处理的移动应用程序。当移动设备在一段时间内没有连接上网络时,在这段时间内记录的数据直到设备再次连上网络时才会被上传。这意味着可能会出现几分钟、几小时、几天、几周或者更长时间延迟的事件时间数据到达。在使用基于处理时间的窗口时,从这样的数据集中都不可能得出有用的推论。
在这种情况下,我们真正想要的是按照事件到达的顺序,按照事件时间对数据进行窗口化。其实我们真正想要的是基于事件时间的窗口。
当你需要用反映事件实际发生时间来观察一个数据源时,你需要使用基于事件时间的窗口。令人遗憾的是,目前使用的大多数数据处理系统都缺乏原生支持。下图展示了将一个无限数据源窗口化为一小时固定窗口的示例:
图中白色实线表示我们感兴趣的两个指定数据。这两个数据到达了与它们所属事件时间窗口不匹配的处理时间窗口内。因此,如果在用户关心事件时间的用例中,将这些数据分发到处理时间窗口,那么计算结果是不正确的。正如人们所预料的那样,事件时间的准确性是使用事件时间窗口的一个好处。
在无限数据源上使用事件时间窗口的另一个好处是,你可以创建动态大小的窗口如会话窗口,而不是使用固定窗口生成会话(这样会造成一个会话分布在不同窗口中):
当然,天下没有免费的午餐,基于事件时间的窗口也不例外。基于事件时间的窗口有两个明显的缺点,这是因为窗口(在处理时间中)通常有比窗口本身的实际时间长度更长的寿命:
总而言之,通过这篇文章我们能可以了解到: