Streaming 101 The world beyond batch

作者:Tyler Akidau 原文链接:

https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-101

https://www.oreilly.com/ideas/the-world-beyond-batch-streaming-102

本文是关于数据处理发展的系列文章的第一篇,一共有两篇,主要涉及 streaming系统,无穷数据集,以及大数据的未来。

我们有足够的理由可以说,流数据处理对于现今的大数据来说,是一个不容小觑的内容。这些理由包括但不限于:

尽管市场对的流处理的热情正愈发高涨,但它的现状是:流处理仍处在不成熟的阶段,与此同时,它的兄弟批处理,最近在数据处理领域取得了瞩目的成就与长足的发展。

但至少,作为一个在谷歌从事海量流式数据处理工作已超过五年的人,我很高兴看到这个时代对流处理的热情。我也很乐意尽我所能,让人们了解到流式处理能够做到的一切以及如何最高效地使用它们,特别是考虑到大多数现有的批处理和流处理系统之间存在着巨大的鸿沟。与此同时,五个 O’Reilly 的伙伴邀请我把之前在Strata + Hadoop World London 2015 所做的演讲 Say Goodbye to Batch 中的内容写成书,这样这个系列就应运而生了。因为我有很多的内容要说,所以我将这些内容分成了两个部分:

  1. Streaming 101:在这部分中,我将会介绍一些基本的背景知识,以及阐述一些术语,之后深入介绍时间域,并给出一些对常见数据处理方法的高度概括(包括批处理和流处理)。
  2. The dataflow Model: 在这部分中,我们将通过一些不同领域的例子,快速地了解一种综合使用了批处理+流处理的模型——云数据流。之后,我会对现存的批处理和流处理系统之间的差别做一个简单总结。

好了,冗长的绪论就此打住,让我们开始变身书呆子吧!

背景介绍

那么,首先,我想介绍一些重要的背景知识来帮助大家搭建知识框架。我们将从三个方面来了解:

术语:什么是流?

在进一步讲解之前,我们首先得明确一件事:什么是流?“流”这个词现在被用来指代各种不同的东西,而这有可能导致我们误解“流”到底是什么,以及流处理系统能做什么(你看,为了简单起见,我之前也或多或少不严谨地使用了流这个词)。因此,我想对流是什么给出一个准确的定义。

问题的关键在于,很多东西本该由“它们是什么”来描述(如无界数据处理,近似结果等),最后却变成了由“它们的实现方式”来描述(如流式执行引擎)。这样不准确的术语表述,使得“流”到底是什么变得模糊不清,一些情况下,这使得“流式系统”表达出的意思被狭义地理解为它们只是拥有某些称为“流”的特性,例如近似结果。鉴于一个设计精巧的流式系统能够和任何现存的批处理引擎一样,具有正确性、一致性、可重现性,我希望将“流”这个词定义为:一种被用来处理无界数据集的数据处理引擎,就是这么简单。(为了定义的完整性,值得指出的是,这个定义包括了常说的流处理和微型批处理方法)

至于一些常常听到的与“流”相关的其他用法,在这里我提出一些更准确、描述性更强的词语来代替它们,并且建议社区中的每一员都使用它们:

  1. 无界数据: 一种不断增长,本质上无穷大的数据集。这些数据集经常被称为“流式数据”。但是,由上面的批注可知,“流”或者“批”用来修饰数据集是有问题的,“流”和“批”应该用来表达处理数据集所用的引擎的类型。而区分两种不同数据集的关键是它们的“有限性”,因此,我建议使用能够体现这种区别的词来修饰数据集。出于这样的原因,我建议将无穷的“流数据”表达为“无界数据”,将有限的“批数据”表达为“有界数据”。
  2. 无界数据处理: 一种持续的数据处理模式,运用于之前提到的无界数据。正如之前所说,我建议仅使用“流”来表述数据处理的方式,那么“流式执行引擎”这种表述就是极大的误导:反复地运行批处理引擎可以实现对无界数据的处理(反过来,设计精巧的流处理系统,也能很好的处理“有界数据”)。因此,为了清楚起见,我建议将这种处理方式称为无界数据处理。
  3. 低延迟,近似结果: 这些结果经常和流处理引擎联系在一起。事实上,这只是因为批处理系统在传统设计上并没有考虑低延迟或近似结果而已。这仅仅是历史原因导致的。当然,如果需要的话,批处理系统是能够完美处理近似结果的。因此,正如之前所说,我建议用“它们的特点是什么”来描述结果(低延迟、近似),而不要使用“怎么得到它们的”来描述结果(比如通过流式引擎所得)。

关于流处理的夸张限制

接下来,我们讨论一下流处理系统能做什么,不能做什么。这里重点关注能做什么:一个设计精巧的流处理系统有多强。流式处理系统长期被用在市场中计算低延迟、近似结果,并且经常结合一个更强的批处理系统来提供最终的正确结果,例如 the Lambda 架构。

如果你还并不熟悉 Lambda 架构,可以这样理解,其基本思想是,你同时运行一个流处理系统和批处理系统,它们本质上进行着基本相同的计算。流处理提供给你一个低延迟、不准确的结果(可能是因为使用了近似算法,或者因为那个流式系统本身无法提供足够的精确度),有时候,我们会在稍后用批处理系统的结果来覆盖它,得到一个正确的输出。这个想法最初由 Twitter 的 Nathan Marz(Storm的创始人)提出,它最终取得了相当大的成功,事实上,这在当时来说是一个精妙绝伦的想法;流式引擎在精确度的表现上不尽如人意,批处理系统足够精确但是比较笨重,因此,Lambda 给了你一个较好的处理方法能够二者兼得。不幸的是,维持一个 Lambda 系统是一件艰难的事:你需要搭建、配置与维护两套独立的管道,并且在最后合并两套管道的结果。

作为一个使用了多年强一致流处理系统的人,我并不是特别认同 Lambda Architecture 提出的整个原理。毫不意外地, Jay Kreps 的 Questioning the Lambda Architecture 刚面世的时候,我成为了它的忠实支持者——它是第一篇对双模式执行提出质疑的文章,很有远见,令人愉快。Kreps 在使用像 Kafka 这样的可重放系统作为流式互连的情况下解决了可重复性问题,甚至提出了Kappa 架构,这基本上意味着,如果我们使用精心设计的系统,那么只需要运行一套管道就能解决问题。虽然我不主张这个概念本身需要一个名字,但我完全支持这个概念的思想。

坦诚地说,我想要讨论得更深入一些。我认为,精心设计的流媒体系统能够提供比批处理系统更多的功能。甚至说,到那个时候,我们将不再需要批处理系统了。一些很酷的伙计们秉承着这个思想,实现了一个彻头彻尾的流处理系统——即使它工作在“批处理”模式下也是一样!这就是 Flink,我很喜欢它!

综上,我们可以预见得到的是,流式系统一旦成熟,能够用强大的框架来处理无界数据,那么 Lambda 架构将回归到他所属的大数据发展的历史长河中。我相信,现在已经到了将这变成现实的时候了。因为,想要在批处理自己擅长的领域击败它,你只需要做两件事:

  1. 正确性 – 这让流处理能够与批处理势均力敌。 核心的问题在于,把正确性归结为一致的存储。流式系统需要一种持续检查状态的方法(Kreps 在他的Why local state is a fundamental primitive in stream processing 里讨论过),而且它必须设计良好,足够在机器故障的情况下保持一致。当几年前 Spark 流式系统第一次出现在大数据领域的公众视野中时,就像是流式系统一致性问题黑暗中的一座灯塔。 幸运的是,从那以后,情况开始越来越好。值得称道的是,许多流式系统模型在没有强一致性的情况下,努力尝试攻克正确性的问题。我真的不敢相信,至多一次数据处理的方式是多么难的事,但事实确实如此。 重申一点重要的事情:一个系统想要比肩甚至超越批处理系统,那么它需要有正确性;想要有正确性,那么它需要有强一致性来进行单次数据处理。除非你真的不在乎你的结果,否则,我建议你避免使用那些不具有强一致性的流式系统。批处理系统不需要预先校验,是因为它们有能力得出正确的运算结果。因此,不要浪费时间在那些无法具有同样性质的流式系统上。 如果你有兴趣深入了解如何才能使得一个流式系统具有强一致性,那么我建议你阅读 MillWheel 和 Spark Streaming。这两篇文章都花了大量的篇幅来讨论一致性问题。鉴于在各种论文或者别的地方已经有很多关于这一话题的高质量信息,在此我将不再展开。
  2. 推断时间的工具 – 这让流处理超越批处理 想要处理具有不同的事件时间偏差的无界数据和无序数据,一个能够有效推断时间的工具是不可或缺的。越来越多的现代数据集表现出这些特点,然而现有的批处理系统(以及大多数流式系统)缺乏必要的工具来应对这些数据带来的困难。我将在这篇文章后续的内容以及下一篇文章的大部分内容里,详细解释和讨论这一点。 为了开始这部分内容,我们需要理解时间域的基本概念,之后我们更深入地了解具有不同事件时间偏差的无界、无序数据意味着什么。接着,在本文后续部分,我们将了解一些常用的处理有边界数据和无边界数据的批处理方法以及流处理方法。

事件时间与处理时间

为了更准确地讨论无界数据处理,我们需要对时间域有一个清晰的认识。在数据处理系统中,有两种时间域是我们最关心的:

并不是每一个使用案例都关心事件时间(而且如果你不关心的话,你的生活会更轻松),但是很多案例还是关心的。例如,实时分析用户表现特征,以及大多数的计费系统,还有许多不规则的发现等。

在理想情况下,活动一出现就会被观测到,此时事件时间和处理时间总是一样的。但是实际上,并没有那么容易。事件时间与处理时间的偏差会随着底层输入源、执行引擎和硬件特性而改变。能够影响偏差等级的因素有:

因此,如果你尝试画出一个真实系统的事件时间与处理时间,你一般会得到一个类似的图的图形。

图中斜率为1的黑色虚线代表的是理想情况,此时处理时间等于事件时间。红线代表的是实际情况。在这个例子中,处理时间开始时系统滞后了一些,中间开始趋向于理想情况,最后又开始滞后。这个偏差本质上是由处理管道引起的延迟。

既然事件时间和处理时间之间的映射并不是固定的,这就意味着如果你关心你的数据的事件时间的话(即何时事件真正发生),就不能仅仅凭借着你在管道中观测到的内容进行分析。不幸的是,当今的大多数用来处理无界数据的系统都是这样设计的。为了应付无界数据的无限性,这些系统一般都需要提供一些窗口化输入数据的概念。后面我们会深入讨论窗口化的问题,但它其实本质上意味着将数据集沿时间边界切割成有限的片段。

如果你很在意正确性,并且希望通过数据的事件时间来分析它们,那么你不应该像很多现存的系统一样用处理时间来划分时间边界(即处理时间窗口化)。如果处理时间和事件时间之间没有良好的一致相关性,那么你的事件时间数据可能会进入错误的处理时间窗口(由于从分布式系统的滞后性,许多类型输入源的滞后性等等),进而失去正确性。我们在下一篇中,将会用更多的例子来理解这个问题。

不幸的是,当按照事件时间窗口化的时候,图片也并不完美。在无界数据的背景环境下,数据的无序性和变量倾斜导致了事件时间窗口的完备性问题:处理时间和事件时间之间没有一个可预测的映射关系的话,如何确定给定的事件时间X处的数据呢?对于一些真实的数据资源来说,没法做到。然而,当今运用的绝大多数数据处理系统都需要一些关于完备性问题的概念,这就使得这些系统在处理无界数据时会有很大的不足。

我建议,与其尝试整理无界数据为最终会结束的批次数据,我们不如设计出有效的工具来应对那些会带来不确定性的复杂数据集。随时都有新的数据会到达,也随时可能有旧的数据被更新或者删除,我们建立的任何系统都必须有能力解决这个问题,而完备性问题的概念正好是一个很方便的优化方案,而不是必须的。

在深入探讨我们应该如何使用云数据流的数据流模型来构建这样一个系统之前,我们还需要学习一章更有用的背景知识:常见的数据处理模式。

数据处理模式

现在,我们已经有了足够的背景知识来学习有界数据处理和无界数据处理方法的核心部分。我们将基于我们所关心的这两种主要的引擎类型,在相关背景下进行学习(关于批和流,在这部分内容里,出于实际情况,我把微型批处理系统划归到了流处理系统)。

有界数据

对于有界数据的处理非常直接,而且可能对我们每一个人来说都很熟悉。在下图中,我们从左边这个充满熵的数据集开始。我们把它投进某些数据处理引擎中(一般来说是批处理系统,尽管设计优良的流处理系统也能很好地完成任务),例如 MapReduce,然后,在右边得到了一个具有更大实际意义的数据集:

尽管你有无数种方法实现它,但是从总体上看,这模型还是很简洁的。但对我们来说,更有趣的东西是处理无界数据集。现在我们来看看处理无界数据的几种不同典型方法,从传统的批处理引擎开始,一直到某个针对无界数据的系统,例如大部分的流式处理系统或者微型批处理系统。

无界数据 – 批处理

批处理引擎,尽管在设计的时候并没有针对无界数据进行考虑,但是由于其成型较早,所以已经广泛运用于无界数据的处理。正如人们所料,其核心思想就是把无界数据切割为很多的有界数据集,然后用批处理的方法进行操作。

固定窗口

最常规的处理无界数据的方法是,不断地把数据窗口化到固定尺寸的窗口中,然后不停地传递给批处理引擎,对每个有界数据集反复独立地运行引擎。特别是对于日志这样的输入文件,其事件可以被写入目录中,其文件层次结构按照文件名对应的窗口进行编码。这种事情看起来很简单,因为你实质上执行了基于时间的洗牌,从而把数据提前提交到了合适的事件时间窗口。

而实际上,大多数的系统仍然存在完备性问题:如果你的事件由于网络间隔而延迟了路由到日志的时间呢?如果你的事件需要全局收集,然后在处理之前需要传输到同一个位置呢?如果你的事件是从移动设备过来的呢?这意味着需要进行一些适当的优化(例如,推迟数据处理直至你确认所有的数据都被收集到了;或者你也可以在发现某一个数据窗口迟到后后重新运行一次对应的批处理引擎)。

会话

这种途径可以取得更大的突破,它尝试使用批处理引擎来处理那些使用了复杂的窗口化策略的无界数据。每一个会话是一段活动时段,会话间由不活动间隙来分隔。当使用典型的批处理引擎来计算会话的时候,通常最终的会话会被跨批次分隔,如下图中的红色标记所示。

无论哪种情况,使用传统批处理引擎来计算会话都不太理想。更好的方法是使用流式规则来构造会话,我们之后会说到这个问题。

无界数据 – 流处理

与大多数基于批处理的无界数据处理方法的特殊性质相反,流式系统是针对无界数据设计的。正如我之前提到的,实际上,很多分布式输入源,不仅处理无界数据,而且也处理具有以下特点的数据:

只有少数几种办法可以用来处理具有这些特征的数据。我大致上将这些方法分为四类:

时间-无关

时间-无关的情况是用来处理那些时间本质上不相关的情况的,即所有的相关逻辑都是数据驱动的。既然这种情况下,结果仅由更多的数据到来所影响,那么流式系统仅仅需要支持基本的数据传输。结果就是,所有现有的流式系统都支持时间无关的用例(当然,如果你关注正确性的话,它们在保证一致性的情况下存在模系统间的差异)。批处理系统也能很好地处理时间-无关的无界数据——只需要简单地把无界数据切割为任意序列的有界数据集,然后分别单独处理他们即可。我们将在本节中介绍几个具体示例,但是考虑到处理时间-无关数据的直接性,除此之外不会花太多的时间。

过滤

有一种非常基本的时间无关数据处理方法,叫做过滤。假设你想要处理网络传输日志,过滤掉其中不属于某一特定域名发送的传输。你要做的是在记录到来时逐条检查,看其是否属于指定的域名,如果不是的话就丢弃它。由于这些操作只需要一个简单的域名元素,而不用管它何时到来的,所以这些数据资源是无界、无序且关于事件时间的变化不相关的。

内连接

另一个时间-无关的例子是内连接(或者说哈希连接)。如果连接两个无界数据源的时候,你关注的只是何时能将两个数据源中的某一元素都接收到,那么逻辑思路与时间因素无关。当你接收到了其中一个数据源的数据之后,你可以先将其缓存,直至你接收到另一个数据源后,发出连接记录即可。(实际上,你可能需要一些垃圾收集策略来处理无法连接的数据,这个操作一般是基于时间的。但是如果你的数据源很少存在这样的问题,那影响不大。)

现在我们来看看外连接,这种情况情况会出现我们讨论过的完备性问题:当你接收到需要连接的其中一端数据时,怎么才能知道另一端是否能接收到呢?现在告诉你,实际上并不能,所以你需要设置一些关于超时的规则,这用到了关于时间的元素。这种时间元素实际上是一种窗口化,我们马上就会进一步了解它。

近似算法

算法的第二部分内容是近似算法,例如近似的Top-N,流式的K-means等。他们使用无界数据源作为输出,并且提供一些近似的输出结果。近似算法的优点是,其针对无界数据进行设计,且开销相对较低。其缺点是,它们存在一定的局限性,算法本身非常复杂(这意味着构造新算法十分困难),并且它们的近似性质限制了它们的效果。

值得注意的是:一般来说这些算法在设计之初就有一些与时间相关的元素(例如某种内置的衰减)。既然这些算法在时间元素一到达的时候就开始处理了,那么就意味着这些时间元素通常是基于处理时间的。对于近似算法来说,特别重要的一点是设置错误界限。但是如果误差界限是根据数据到达的时序来预测的,那么你以不同的事件时间偏差来提供数据时,并不会有任何影响。这点要牢牢记住。

近似算法本身是一类迷人的课题,但是它们本质上是时间-无关数据处理的一类例子,它们的使用十分直接,因此我们没有必要展开讨论。

窗口化

剩余的两种无界数据处理方法都是窗口化的变种。在深入学习它们之前,我需要清楚地说明我说的窗口化指的是什么。窗口化是获取数据源方法中的一种(不论有界还是无界数据),根据时间边界将数据切割为有限块来处理。下图展示了三种不同的窗口化模式:

固定窗口

固定窗口将时间分割为固定长度的时间片,标定在内存中。一般来说,固定窗口的分割会统一应用于整个数据集,这是对齐窗口的一个例子。在某些情况下,需要对不同的数据子集进行相移(例如每一个key),以此让窗口完成负载的传播更加均匀,这是一个非对齐窗口的例子。

滑动窗口

滑动窗口是一种广义的固定窗口,具有固定的长度和周期。如果周期比长度小,那么窗口之间会出现重叠部分。如果周期等于长度,那么就是一个固定窗口。如果周期大于长度,那么你会得到一个随时间推移的数据子集。正如固定窗口一样,滑动窗口一般都是对齐的,尽管在某些情况下出于优化需要而不对齐。图8中的滑动窗口是为了给出了关于滑动的直观感受才这样画的,实际上,全部五个窗口都适用于整个数据集。

会话

动态窗口中的一种,会话是由那些以超时为间隔所划分的事件序列。会话通过将一系列时间相关的事件分组,实现了以时间为线索来研究用户的行为(一次观看视频的序列)。会话有趣的地方在于,其长度无法预先设定,而是由实际数据输入所决定。这是非对齐窗口的典型例子,会话在不同的数据子集中实际上从未完全相同(例如,不同的用户)。

上述讨论的处理时间、事件时间这两种时间域,是我们所关心的。窗口化在两种时间域里都有重要作用,因此我们现在要详细讨论每一类的细节,并且观察它们有哪些不同。因为处理时间窗口化在实际运用中更加广泛,所以我们从这里开始。

基于处理时间的窗口化

当基于处理时间进行窗口化时,系统实质上是将输入数据缓存到窗口中,直到其中的数据被处理完毕。例如,在五分钟固定窗口的例子中,系统将会进行五分钟的数据收集与缓存,之后,把这五分钟内观测到的数据发送到下游数据流中以便处理。

处理时间窗口化有几种比较好的属性:

除了优点之外,它也有一个巨大的缺点:如果数据存在与事件时间的关联,且依靠处理时间来推断事件时间,那么数据必须以事件时间的顺序到达。不幸的是,以事件时间序列到达的数据在实际的分布式输入源中非常难得。

举一个简单的例子,有一个移动APP需要收集使用情况统计信息,以便之后处理。在移动设备离线的时间段内(比如说短暂连接中断,或者飞行模式等),数据记录不会被上传,直到移动设备恢复连接。这就意味着,数据的到达时间相较其事件时间,会存在几分钟、几小时、几天、几周甚至更长的误差。当使用处理时间窗口化时,用这样的数据集几乎无法推导出任何有用的信息。

再举一个例子,在系统状态良好的情况下,许多分布式输入源能够提供事件时间序列的数据(或非常接近)。不幸的是,当输入源健康的时候事件时间偏差比较低并不意味着总能保持这样。假设有一个全球服务,它需要处理从各大洲收集来的数据。如果网络需要跨越带宽受限的线路,那么其带宽将会降低或者延迟将会增加,在这种情况下,一部分的输入数据的偏差可能突然大得多。如果你正在基于处理时间窗口化数据,那么你的窗口将无法反映事件真正的发生事件。取而代之的是,它们只能反映出事件何时到达数据处理管道,整个管道将混杂着旧数据和新数据。

在上面的两个例子里,我们所做的是依据事件到达的顺序,来实现按照事件时间窗口化数据。重点是,我们真正想做的是按照事件时间进行窗口化。

基于事件时间窗口化

事件时间窗口化可以通过那些能够反映事件实际发生的时间的有限数据块来观测数据源。这是窗口化的黄金法则。不幸的是,现今使用的大多数数据处理系统缺少对它的天然支持(尽管一些系统具有不错的一致性模型,比如Hadoop和Spark的流处理,能够作为一个合理的底层服务来支持窗口化系统)。

下图例子是将一个无界数据源用固定一小时的窗口进行窗口化。

图中的白色实线代表着两个特定的所需数据。这两个数据都是按处理时间并入窗口的,而这和他们所属的事件时间窗口可能不符合。因此,如果这些数据被放入处理时间窗口而业务需求的是事件时间窗口的话,可能会导致这些计算结果不正确。正如人们所期望的那样,事件时间的正确性是使用事件时间窗口的一个好处。

使用事件时间来窗口化无界数据的另一个好处是,你能创建动态大小的窗口,例如会话,这样就不需要在会话数据量大于固定窗口大小的时候进行拆分了(正如我们在上一章的会话例子“无界数据-批处理”所见):

当然,强大的性能是要付出代价的,事件时间窗口也不例外。事件时间窗口具有两个非常显著的缺点,那就是窗口一般来说需要更长的存活时间:

结论

好样的!到此为止,我们已经走过了总路程的一半,学习到了很多内容,值得祝贺!在我们开始下一部分的学习之前,我们先回顾一下所学过的东西。令人高兴的是,第一部分内容是比较无聊的,而第二部分内容要有趣的多。

概括

总结一下,在这篇文章里,我们学习了如下内容:

下一章要学习什么

这一篇文章提供了学习具体实例所需的背景知识。下一章我们将学习具体实例,其中主要包括以下内容: