-
Notifications
You must be signed in to change notification settings - Fork 0
Chapter 1. Streaming 101
最近,流式数据处理是大数据领域的一个大话题,这有很好的理由;其中包括以下几点。
- 企业渴望更及时地了解他们的数据,而转向流式是实现更低延迟的一个好方法。
- 现代商业中越来越常见的大规模、无限制的数据集,使用为这种永无止境的数据量设计的系统更容易驯服。
- 在数据到达时对其进行处理,使工作负载在一段时间内更均匀地分布,产生更一致和可预测的资源消耗。
尽管企业对流式系统的兴趣激增,但流式系统与它们的批处理兄弟相比,长期以来仍然相对不成熟。只是在最近,潮流才决定性地转向另一个方向。在我比较激动的时候,我希望这在一定程度上是由于我最初在 “流式 101” 和 “流式 102” 博客文章(本书的前几章显然是基于这两篇文章)中所提供的大量诱导。但实际上,业界对流式系统的成熟有很大的兴趣,而且有很多聪明而活跃的人喜欢建立这些系统。
尽管在我看来一般的流式宣传战已经取得了有效的胜利,但我仍将或多或少地提出我在 “流式 101” 中的原始论点,不做任何改动。首先,它们在今天仍然非常适用,即使大部分行业已经开始听从战斗的呼声。第二,有很多人还没有得到备忘录;这本书是一个扩展的尝试,试图把这些观点传达出去。
首先,我介绍了一些重要的背景信息,这将有助于构建我想讨论的其他主题。我在三个具体部分做了这个工作。
术语
要准确地谈论复杂的话题,需要对术语进行精确的定义。对于一些在当前使用中解释过多的术语,我将尝试在我说这些术语时准确地确定其含义。
能力
我谈到了人们经常认为的流式系统的缺点。我还提出了我认为数据处理系统建设者需要采用的思维框架,以满足现代数据消费者的需求。
时间域
我介绍了与数据处理有关的两个主要的时间域,说明了它们之间的关系,并指出了这两个域带来的一些困难。
在进一步讨论之前,我想先说明一件事:什么是流?今天,流一词被用来表示各种不同的东西(为了简单起见,我到现在为止一直在使用它,有点松散),这可能导致对流的真正含义或流式系统的实际能力的误解。因此,我更愿意对这个词进行精确的定义。
问题的关键在于,许多应该用它们是什么来描述的东西(无限制的数据处理、近似的结果等),已经被俗称为它们在历史上是如何完成的(即,通过流式执行引擎)。这种缺乏精确性的术语模糊了流的真正含义,在某些情况下,它给流式系统本身带来了负担,暗示它们的能力仅限于历史上被描述为 “流" 的特征,如近似或猜测的结果。
鉴于设计良好的流式系统与任何现有的批处理引擎一样有能力(技术上更有能力)产生正确、一致、可重复的结果,我更愿意将 “流" 一词隔离为一个非常具体的含义。
流式系统
一种数据处理引擎,其设计考虑到了无限的数据集。
如果我想谈论低延迟、近似或推测性的结果,我会使用这些具体的词语,而不是不准确地称其为 "流"。
在讨论可能遇到的不同类型的数据时,精确的术语也很有用。从我的角度来看,有两个重要的(也是正交的)维度来定义一个给定的数据集的形状:cardinality 和 constitution。
数据集的 cardinality 决定了它的大小,cardinality 最突出的方面是一个特定的数据集是有限的还是无限的。以下是我喜欢用的两个术语,用来描述一个数据集的粗略的卡度。
有界数据
一种数据集,其大小是有限的。
无界数据
一种数据集,其大小是无限的(至少在理论上)。
Cardinality 很重要,因为无限数据集的无界性质给消费它们的数据处理框架带来了额外的负担。关于这一点,下一节会有更多的介绍。
另一方面,数据集的构成决定了其物理表现形式。因此,结构定义了人们与相关数据的交互方式。在第 6 章之前,我们不会深入研究结构,但为了给你一个简单的感觉,有两个重要的主要结构。
表
在一个特定的时间点上对一个数据集的整体看法。SQL 系统传统上是以表的形式处理。
流
对数据集随时间变化的逐个元素的观察。MapReduce 系列的数据处理系统传统上都是以流的形式处理。
我们在第 6 章、第 8 章和第 9 章中相当深入地研究了流和表之间的关系,在第 8 章中我们还了解了将它们联系在一起的统一的底层概念--时变关系。但在此之前,我们主要处理流,因为这是今天大多数数据处理系统(包括批处理和流)中管道开发者直接交互的结构。它也是最自然地体现了流处理所特有的挑战的结构。
关于这一点,让我们接下来谈谈流式系统能做什么和不能做什么,重点是能做什么。在这一章中,我想说的最大的一件事是,一个设计良好的流式系统可以有多大能力。流媒体系统在历史上一直被归入一个小众市场,即提供低延迟、不准确或投机性的结果,通常与能力更强的批处理系统一起提供最终的正确结果;换言之,Lambda 架构。
对于那些还不熟悉 Lambda 架构的人来说,其基本思想是,你在运行一个流式系统的同时也运行一个批处理系统,两者都在进行同样的计算。流式系统给你低延迟、不准确的结果(要么是因为使用了近似算法,要么是因为流式系统本身不提供正确性),一段时间后,批处理系统出现,为你提供正确的输出。最初是由 Twitter 的 Nathan Marz(Storm 的创造者)提出的,它最终相当成功,因为事实上,这在当时是一个绝妙的想法;流式引擎在正确性方面有点逊色,而批处理引擎正如你所期望的那样,本质上是不方便的,所以 Lambda 给了你一个方法,让你拥有传说中的蛋糕,也可以吃它。不幸的是,维护 Lambda 系统是一件很麻烦的事情:你需要建立、提供和维护两个独立的管道版本,然后在最后以某种方式合并两个管道的结果。
作为一个在强一致性流引擎上工作多年的人,我也发现 Lambda 架构的整个原理有点不靠谱。不出所料,当 Jay Kreps 的 “质疑Lambda 架构” 的帖子出来时,我是他的忠实粉丝。这是最早的反对双模式执行的必要性的高度可见的声明之一。令人高兴的是。Kreps 在使用像 Kafka 这样的可重放系统作为流媒体互连的背景下讨论了可重复性的问题,甚至提出了 Kappa 架构,这基本上意味着使用一个精心设计的系统来运行单一的管道,为手头的工作适当地构建。我不相信这个概念需要自己的希腊字母名称,但我原则上完全支持这个想法。
说实话,我想把事情做得更进一步。我认为,设计良好的流式系统实际上提供了批处理功能的严格超集。除了效率上的差异外,应该不需要今天的批处理系统了。为 Apache Flink 的人们点赞,因为他们把这个想法放在心上,并建立了一个系统,即使在 “批处理" 模式下,也是所有的流--所有的时间,我喜欢它。
批量和流媒体的效率差异
我认为这不是流媒体系统固有的限制,而只是迄今为止大多数流媒体系统的设计选择的结果。批处理和流媒体之间的效率差异主要是批处理系统中增加的捆绑和更有效的 shuffle 传输的结果。现代批处理系统不遗余力地实施复杂的优化,允许使用令人惊讶的适度的计算资源来实现显著的吞吐量水平。没有理由不把使批处理系统成为今天的高效重量级系统的聪明见解纳入为无界数据设计的系统,为用户在我们通常认为是高延迟、高效率的 “批" 处理和低延迟、低效率的 “流" 处理之间提供灵活的选择。这实际上就是我们在谷歌的 Cloud Dataflow 所做的,在同一个统一的模型下提供批处理和流处理的运行器。在我们的案例中,我们使用独立的运行器,因为我们碰巧有两个独立设计的系统,为其特定的使用情况进行了优化。从长远来看,从工程的角度来看,我很希望看到我们把这两个系统合并成一个单一的系统,在保留选择适当的效率水平的灵活性的同时,将两者的最佳部分融合在一起。但这不是我们今天的情况。而且说实话,由于统一的数据流模型,它甚至没有严格的必要;所以它很可能永远不会发生。
所有这一切的推论是,流媒体系统的广泛成熟与无边界数据处理的强大框架相结合,将使 Lambda 架构及时归入大数据历史的古老时代,成为它的归宿。我相信,现在是实现这一目标的时候了。因为要做到这一点,也就是说,要在自己的游戏中击败批处理,你只需要两样东西。
正确性
这可以让你与批处理相提并论。在核心方面,正确性可以归结为一致的存储。流式系统需要一种方法来检查长期存在的状态(这一点Kreps 在他的 "为什么本地状态是流处理的基本单元” 一文中谈到过),而且它必须设计得足够好,以便在机器故障时保持一致。几年前,当 Spark Streaming 首次出现在公共大数据领域时,它是在一个黑暗的流媒体世界中一致性的灯塔。值得庆幸的是,从那时起,情况有了很大的改善,但令人惊讶的是,有多少流媒体系统仍然试图在没有强大的一致性的情况下获得成功。
重申一下--因为这一点很重要:强一致性是 exactly-once 处理所需要的,这也是正确性所需要的,这也是任何有机会达到或超过批处理系统能力的系统所需要的。除非你真的不关心你的结果,否则我恳请你回避任何不提供强一致性状态的流式系统。批处理系统不需要你提前验证它们是否能够产生正确的答案;不要把时间浪费在不能满足同样要求的流式系统上。
如果你想了解更多关于在流式系统中获得强一致性所需要的东西,我推荐你去看看 MillWheel、Spark Streaming 和 Flink snapshotting 的论文。这三篇论文都花了大量的时间来讨论一致性问题。Reuven 将在第 5 章深入探讨一致性保证,如果你仍然发现自己渴望更多,在文献和其他地方有大量关于这个主题的高质量信息。
关于时间的推理工具
这使你超越了批处理。良好的时间推理工具对于处理无界的、无序的、具有不同事件时间偏差的数据至关重要。越来越多的现代数据集表现出这些特征,而现有的批处理系统(以及许多流式系统)缺乏必要的工具来应对它们带来的困难(尽管现在这种情况正在迅速改变,甚至在我写这篇文章时)。我们将用本书的大部分时间来解释和关注这一点的各个层面。
首先,我们要对时间域这个重要的概念有一个基本的了解,之后我们要深入研究我所说的无界、无序的不同事件时间偏移的数据是什么意思。然后,我们用本章的其余部分来研究有界和无界数据处理的常见方法,使用批处理和流式系统。
要有说服力地谈论无限制的数据处理,需要对所涉及的时间域有一个清晰的理解。在任何数据处理系统中,通常有两个我们关心的时间域。
事件时间
这是事件实际发生的时间。
处理时间
这是在系统中观察到事件的时间。
并非所有的用例都关心事件时间(如果你的用例不关心,万岁!你的生活更容易了),但许多用例关心。这方面的例子包括随着时间的推移对用户行为进行定性,大多数计费应用,以及许多类型的异常检测,仅举几例。
在一个理想的世界里,事件时间和处理时间总是相等的,事件一发生就被立即处理。然而,现实并非如此,事件时间和处理时间之间的偏差不仅不是零,而且往往是底层输入源、执行引擎和硬件特性的高度可变的函数。能够影响偏移程度的事情包括以下几点。
- 共享资源的限制,如网络拥堵、网络分区,或在非专用环境中共享 CPU。
- 软件原因,如分布式系统逻辑、争论等。
- 数据本身的特点,如密钥分布、吞吐量的差异或无序性的差异(即满飞机的人在整个飞行过程中脱机使用手机后将其从飞行模式中取出)。
因此,如果你在任何现实世界的系统中绘制事件时间和处理时间的进度图,你最终得到的东西通常看起来有点像图1-1中的红线。
图1-1 时间域的映射。x 轴代表系统中的事件时间完整性;也就是说,事件时间中的 X 时间,到此为止,所有事件时间小于 X 的数据都被观察到。y 轴代表处理时间的进度;也就是数据处理系统执行时观察到的正常时钟时间。
在图 1-1 中,斜率为 1 的黑色虚线代表理想状态,即处理时间和事件时间完全相等;红线代表现实。在这个例子中,系统在处理时间的开始阶段有点滞后,在中间阶段向理想状态靠拢,然后在最后阶段又有点滞后。乍一看,这张图中有两种类型的倾斜,分别在不同的时间域。
处理时间
理想和红线之间的垂直距离是处理时间领域的滞后。这个距离告诉你,在给定时间的事件发生的时间和它们被处理的时间之间有多少延迟(处理时间)。这也许是两种倾斜中更自然和直观的一种。
事件时间
理想线和红线之间的水平距离是那一刻管道中的事件时间偏移量。它告诉你管道目前比理想状态(在事件时间上)落后多少。
实际上,在任何给定的时间点上,处理时间的滞后和事件时间的倾斜是相同的;它们只是观察同一事物的两种方式。因为事件时间和处理时间之间的整体映射不是静态的(即,滞后/偏斜可以随时间任意变化),这意味着如果你关心它们的事件时间(即,事件实际发生的时间),你不能只在管道观察到它们的背景下分析数据。不幸的是,这就是许多为无界数据设计的系统在历史上的运作方式。为了应对无界数据集的无限性,这些系统通常提供一些对传入数据进行窗口处理的概念。我们稍后会深入讨论窗口化,但它本质上意味着将数据集沿着时间边界切成有限的片段。如果你关心正确性,并且对在事件时间背景下分析数据感兴趣,你就不能像许多系统那样用处理时间来定义这些时间边界(即处理时间窗口化);由于处理时间和事件时间之间没有一致的关联,你的一些事件时间数据最终会出现在错误的处理时间窗口中(由于分布式系统固有的滞后性,许多类型的输入源的在线/离线性质,等等),从而将正确性抛出窗外,正如它所说。我们在后面的章节以及本书的其余部分的一些例子中更详细地研究了这个问题。
不幸的是,当按事件时间开窗时,情况也不完全乐观。在无界数据的背景下,无序和变量偏斜会引起事件时间窗口的完整性问题:在处理时间和事件时间之间缺乏可预测的映射,你如何确定你已经观察到了某个事件时间 X 的所有数据?对于许多现实世界的数据源,你根本无法做到。但是,今天使用的绝大多数数据处理系统都依赖于某种完整性的概念,这使得它们在应用于无界数据集时处于严重的不利地位。
我建议,我们不应该试图将无限制的数据梳理成最终变得完整的有限批次的信息,而是应该设计一些工具,使我们能够生活在这些复杂的数据集所带来的不确定性世界中。新的数据会到来,旧的数据可能会被收回或更新,我们建立的任何系统都应该能够自行应对这些事实,完整性的概念是对特定和适当的用例的方便优化,而不是所有用例的语义必要性。
在讨论这种方法的具体细节之前,让我们再来完成一个有用的背景:常见的数据处理模式。
在这一点上,我们已经有了足够的背景,我们可以开始研究当今有界和无界数据处理中常见的核心使用模式类型。我们研究这两种类型的处理,并且在相关的地方,在我们关心的两种主要类型的引擎(批和流,在这种情况下,我基本上把微批处理和流式混为一谈,因为两者之间的差异在这个层面上并不十分重要)。
处理有界数据在概念上是很简单的,而且大家可能都很熟悉。在图 1-2 中,我们从左边开始有一个充满熵的数据集。我们通过一些数据处理引擎(通常是批处理,尽管一个设计良好的流式引擎也可以工作得很好)来运行它,比如 MapReduce,最后在右边有一个新的结构化数据集,具有更大的内在价值。
图1-2. 用一个经典的批处理引擎进行有限制的数据处理。左边的有限的非结构化数据池通过数据处理引擎运行,在右边产生相应的结构化数据。
尽管在这个方案中,你可以实际计算的内容当然有无限的变化,但总体模型是相当简单的。更有趣的是处理无界数据集的任务。现在让我们来看看处理无界数据的各种典型方法,从传统的批处理引擎使用的方法开始,最后是你可以在为无界数据设计的系统中采取的方法,比如大多数流式或微批引擎。
批处理引擎,尽管在设计时没有明确考虑到无界数据,但自从批处理系统被首次设想以来,一直被用来处理无界数据集。正如你所期望的,这种方法围绕着将无界数据切成适合批处理的有界数据集的集合。
使用批处理引擎的重复运行来处理无界数据集的最常见方法是将输入数据窗口化为固定大小的窗口,然后将每个窗口作为一个单独的、有界的数据源来处理(有时也称为翻滚窗口),如图 1-3 所示。特别是对于像日志这样的输入源来说,事件可以被写入目录和文件层次,其名称对它们所对应的窗口进行编码,这种事情乍一看很简单,因为你基本上已经进行了基于时间的 shuffle,提前把数据放入适当的事件时间窗口。
然而,在现实中,大多数系统仍然有一个完整性的问题需要处理(如果你的一些事件由于网络分区而在通往日志的途中被延迟了怎么办?如果你的事件是在全球范围内收集的,并且在处理前必须转移到一个共同的地方,怎么办?如果你的事件来自移动设备呢?),这意味着某种缓解措施可能是必要的(例如,延迟处理,直到你确定所有的事件都被收集,或在数据到达较晚时重新处理整个批次的数据)。
图1-3. 通过经典批处理引擎的临时固定窗口处理无界数据。一个无界的数据集被预先收集到有限的、固定大小的有界数据窗口中,然后通过连续运行经典批处理引擎来处理。
当你试图使用批处理引擎将无界数据处理成更复杂的窗口策略时,这种方法就会出现更多的问题,比如会话。会话通常被定义为活动期(例如,对于一个特定的用户),由一个不活动的间隙终止。当使用一个典型的批处理引擎来计算会话时,你常常会发现会话被分割成不同的批次,如图 1-4 中的红色标记所示。我们可以通过增加批处理量来减少分割的数量,但代价是增加延迟。另一个选择是增加额外的逻辑来拼接以前运行的会话,但代价是进一步的复杂性。
图1-4。通过经典批处理引擎的临时固定窗口将无界数据处理成会话。一个无界数据集被预先收集到有限的、固定大小的有界数据窗口中,然后通过连续运行经典批处理引擎被细分为动态会话窗口。
不管怎么说,使用经典的批处理引擎来计算会话是不太理想的。一个更好的方法是以流的方式建立会话,这一点我们将在后面讨论。
与大多数基于批处理的无界数据处理方法的临时性质相反,流式系统是为无界数据而建立的。正如我们前面谈到的,对于许多现实世界的分布式输入源,你不仅发现自己在处理无界数据,而且还发现诸如以下的数据。
- 就事件时间而言,高度无序,这意味着如果你想在事件发生的背景下分析数据,你需要在你的管道中进行某种基于时间的 shuffle。
- 具有不同的事件时间偏差,这意味着你不能只是假设你总是能在某个恒定的伊普西隆时间 Y 内看到大部分给定事件时间 X 的数据。
在处理具有这些特征的数据时,你可以采取一些方法。我一般将这些方法分为四组:时间无关、近似、按处理时间开窗和按事件时间开窗。
现在让我们花一点时间来看看这些方法中的每一种。
时间无关的处理用于时间基本无关的情况;也就是说,所有相关的逻辑都是数据驱动的。因为这类用例的一切都由更多数据的到来所决定,除了基本的数据传输外,流媒体引擎真的没有什么特别需要支持的。因此,基本上现有的所有流媒体系统都支持开箱即用的时间无关的用例(当然,如果你关心正确性的话,还要考虑系统与系统之间在一致性保证方面的差异)。批量系统也很适合对无界数据源进行时间无关的处理,只需将无界数据源切成任意的有界数据集序列,并独立处理这些数据集。我们在这一节中看了几个具体的例子,但是考虑到处理时间无关的处理的直接性(至少从时间的角度来看),除此之外我们不会花更多的时间。
过滤
时间无关的处理的一个非常基本的形式是过滤,图 1-5 是一个例子。想象一下,你正在处理网络流量日志,你想过滤掉所有不是来自某个特定域名的流量。你会在每条记录到达时查看它是否属于感兴趣的域,如果不属于,就放弃它。因为这种事情在任何时候都只依赖于一个单一的元素,所以数据源是无界的、无序的,并且具有不同的事件时间偏差的事实是不相关的。
图 1-5. 过滤无边界的数据。一个不同类型的数据集合(从左到右流动)被过滤成一个包含单一类型的同质集合。
内联接
另一个与时间无关的例子是内连接,如图 1-6 所示。当连接两个无界数据源时,如果你只关心来自两个源的元素到达时的连接结果,那么逻辑中就没有时间因素。在看到一个源的值时,你可以简单地把它缓冲在持久化状态中;只有在另一个源的第二个值到达时,你才需要发出连接的记录。(事实上,你可能需要某种垃圾收集策略来处理未排放的部分连接,这可能是基于时间的。但对于一个很少或没有未完成的连接的用例来说,这样的事情可能不是问题)。
图 1-6. 在无界数据上执行一个内联。当观察到来自两个来源的匹配元素时就会产生连接。
将语义转换为某种外层连接,就引入了我们已经谈论过的数据完整性问题:在你看到连接的一边之后,你怎么知道另一边是否会到达?说实话,你不知道,所以你需要引入一些超时的概念,这就引入了一个时间元素。这个时间元素本质上是一种窗口化的形式,我们稍后会更仔细地看一下。
近似算法
第二大类方法是近似算法,如近似 Top-N、流式 K-means 等。它们接受一个无限制的输入源,并提供输出数据,如果你仔细观察的话,这些数据或多或少与你希望得到的数据相似,如图 1-7 所示。近似算法的优点是,在设计上,它们是低开销的,而且是为无界数据设计的。缺点是,这些算法的数量有限,算法本身往往很复杂(这使得人们很难想出新的算法),而且它们的近似性质限制了它们的效用。
图 1-7. 在无界数据上计算近似值。数据通过一个复杂的算法运行,产生的输出数据或多或少看起来像另一边的预期结果。
值得注意的是,这些算法通常在其设计中确实有一些时间元素(例如,某种内置的衰减)。而且,由于它们在元素到达时进行处理,该时间元素通常是基于处理时间的。这对那些为其近似值提供某种可证明的错误界限的算法来说尤其重要。如果这些误差界限是以数据按顺序到达为前提的,那么当你给算法提供具有不同事件时间偏差的无序数据时,这些误差界限基本上没有意义。需要记住的是。
近似算法本身是一个迷人的主题,但由于它们基本上是时间无关的处理的另一个例子(除了算法本身的时间特征外),它们是相当直接的使用,因此不值得进一步关注,因为我们目前的重点。
窗口
剩下的两种无界数据处理方法都是窗口化的变种。在深入探讨它们之间的差异之前,我应该明确说明我所说的窗口化是什么意思,因为我们在上一节只简单地提到了它。窗口化只是把一个数据源(无论是无界的还是有界的),沿着时间边界切成有限的小块进行处理。图 1-8 显示了三种不同的窗口化模式。
图 1-8. 窗口策略。每个例子都显示了三个不同的键,强调了对齐窗口(适用于所有数据)和非对齐窗口(适用于数据的一个子集)之间的区别。
让我们仔细看看每种策略。
固定窗(又称翻转窗)
我们在前面讨论过固定窗口。固定窗口将时间切成具有固定大小时间长度的片段。通常情况下(如图 1-9 所示),固定窗口的片段是统一应用于整个数据集的,这是一个对齐窗口的例子。在某些情况下,最好对数据的不同子集(例如,每个键)进行相位转移,以使窗口的完成负荷在时间上分布得更均匀,这反而是一个非对齐窗口的例子,因为它们在整个数据中是不同的。
滑动的窗户(又称跳跃的窗户)
作为固定窗口的概括,滑动窗口由一个固定的长度和一个固定的周期来定义。如果周期小于长度,窗口就会重叠。如果周期等于长度,你就有固定的窗口。如果周期大于长度,你就有一种奇怪的抽样窗口,只看一段时间内的数据子集。与固定窗口一样,滑动窗口通常是对齐的,尽管在某些使用情况下它们可以不对齐,作为一种性能优化。请注意,图 1-8 中的滑动窗口是为了给人一种滑动运动的感觉而绘制的;实际上,所有五个窗口都会适用于整个数据集。
会话
会话是动态窗口的一个例子,它由一系列事件组成,这些事件以大于某个超时的不活动间隙为终点。会话通常用于分析用户在一段时间内的行为,通过将一系列时间上相关的事件组合在一起(例如,在一个座位上观看的一系列视频)。会话很有趣,因为它们的长度不能被先验地定义;它们取决于所涉及的实际数据。它们也是不对齐窗口的典型例子,因为会话在不同的数据子集(例如,不同的用户)中几乎没有相同的。
我们之前讨论的两个时间域(处理时间和事件时间)基本上是我们关心的两个领域。窗口化在这两个领域都是有意义的,所以让我们详细看看它们的区别。因为处理时间的窗口化在历史上比较常见,我们就从这里开始。
按处理时间开窗
当按处理时间开窗时,系统基本上是将传入的数据缓冲到窗口中,直到一定的处理时间过去。例如,在 5 分钟固定窗口的情况下,系统将缓冲数据5分钟的处理时间,之后它将把在这5分钟内观察到的所有数据作为一个窗口,并将它们送到下游进行处理。
图 1-9. 按处理时间开窗进入固定窗口。数据根据它们到达管道的顺序被收集到窗口。
加工时间窗口法有几个不错的特性
- 它很简单。实现起来非常简单,因为你不用担心在时间内洗数据的问题。你只需在它们到达时进行缓冲,并在窗口关闭时将它们发送到下游。
- 判断窗口的完整性是直接的。因为系统对一个窗口的所有输入是否被看到有完美的了解,它可以对一个给定的窗口是否完整做出完美的判断。这意味着在通过处理时间进行窗口化时,不需要能够以任何方式处理 "迟到 "的数据。
- 如果你想在观察时推断出关于源的信息,处理时间窗口化正是你想要的。许多监控场景都属于这个类别。想象一下,跟踪每秒发送到全球规模的网络服务的请求数。计算这些请求的速率以检测故障,是对处理时间窗口法的完美运用。
除了好的观点之外,处理时间窗口法还有一个非常大的缺点:如果有关的数据有与之相关的事件时间,那么如果处理时间窗口要反映这些事件实际发生的时间,这些数据必须按事件时间顺序到达。不幸的是,事件时间顺序的数据在许多现实世界的分布式输入源中是不常见的。
作为一个简单的例子,想象一下任何收集使用统计数据供以后处理的移动应用程序。对于一个特定的移动设备在任何时间内离线的情况(短暂的失去连接,在飞往全国各地时的飞行模式,等等),在此期间记录的数据将不会被上传,直到设备再次上线。这意味着,数据到达时可能有几分钟、几小时、几天、几周或更长时间的事件时间偏差。如果以处理时间为窗口,基本上不可能从这样的数据集中得出任何有用的推论。
再比如,当整个系统健康时,许多分布式输入源可能会提供事件时间有序(或非常接近)的数据。不幸的是,当输入源健康时,事件时间偏移较低的事实并不意味着它将永远保持这种状态。考虑到一个处理在多个大洲收集的数据的全球服务。如果跨越带宽限制的跨洲线路的网络问题(可悲的是,这种情况出奇的普遍)进一步降低了带宽和/或增加了延迟,突然间,你的一部分输入数据可能开始以比以前大得多的偏移到达。如果你按处理时间对这些数据进行窗口处理,你的窗口就不再代表实际发生的数据;相反,它们代表事件到达处理管道时的时间窗口,这是一些旧的和当前数据的任意混合。
在这两种情况下,我们真正想要的是通过事件时间来为数据建立窗口,而这种方式对事件的到达顺序是稳健的。我们真正需要的是事件时间窗口化。
按事件时间开窗
当你需要用有限的块来观察一个数据源,反映这些事件实际发生的时间时,你就会使用事件时间窗口法。它是窗口化的黄金标准。在2016年之前,大多数正在使用的数据处理系统缺乏对它的原生支持(尽管任何具有体面的一致性模型的系统,如Hadoop或Spark Streaming 1.x,可以作为建立这样一个窗口化系统的合理底层)。我很高兴地说,今天的世界看起来非常不同,从Flink到Spark到Storm到Apex等多个系统都原生支持某种事件时间窗口化。
图1-10显示了一个将无界的源分成一小时固定窗口的例子。
图1-10. 按事件时间开窗进入固定窗口。数据被收集到基于其发生时间的窗口中。黑色箭头标出了到达处理时间窗口的数据例子,与它们所属的事件时间窗口不同。
图1-10中的黑色箭头指出了两个特别有趣的数据。这两个数据到达的处理时间窗口与每个数据所属的事件时间窗口不一致。因此,如果这些数据在一个关心事件时间的用例中被纳入处理时间窗口,那么计算出来的结果将是不正确的。正如你所期望的,事件时间的正确性是使用事件时间窗口的一个好处。
在无界数据源上进行事件时间窗口化的另一个好处是,你可以创建动态大小的窗口,比如会话,而不会出现在固定窗口上生成会话时观察到的任意分裂(正如我们之前在 "无界数据 "的会话例子中看到的那样。流 "中看到的),如图1-11所示。
图1-11. 按事件时间开窗进入会话窗口。数据被收集到会话窗口,根据相应事件发生的时间来捕捉活动的爆发。黑色的箭头再次指出了将数据放入正确的事件时间位置所需的时间洗牌。
当然,强大的语义很少是免费的,事件时间窗口也不例外。事件时间窗口有两个明显的缺点,这是因为窗口的寿命往往必须比窗口本身的实际长度更长(处理时间)。
缓冲
由于窗口寿命的延长,需要更多的数据缓冲。值得庆幸的是,持久性存储通常是大多数数据处理系统所依赖的资源类型中最便宜的(其他主要是CPU、网络带宽和RAM)。因此,在使用任何设计良好的数据处理系统时,这个问题通常比你想象的要少得多,因为它具有强烈一致的持久性状态和一个体面的内存缓存层。此外,许多有用的聚合不需要缓冲整个输入集(例如,总和或平均),而是可以逐步执行,在持久化状态中存储一个更小的中间聚合。
完备性
鉴于我们通常没有好办法知道我们何时看到了某个窗口的所有数据,我们如何知道该窗口的结果何时可以实现?事实上,我们根本不知道。对于许多类型的输入,系统可以通过类似于MillWheel、Cloud Dataflow和Flink中的 watermark(我们在第三章和第四章中会详细讨论),对窗口的完成给出合理准确的启发式估计。但对于绝对正确性至关重要的情况(再次考虑计费),唯一真正的选择是为管道建设者提供一种方法,以表达他们希望窗口的结果何时被物化,以及这些结果应如何随着时间的推移而被完善。处理窗口的完整性(或缺乏完整性)是一个迷人的话题,但也许最好是在具体的例子中进行探讨,我们接下来看看。
呜! 这是一个很大的信息量。如果你已经走到了这一步,你是值得赞扬的!但我们只是刚刚开始。但我们才刚刚开始。在继续深入研究 Beam 模型的方法之前,让我们简单地回顾一下到目前为止我们所学到的东西。在本章中,我们已经做了以下工作。
- 澄清了术语,将 “流" 的定义集中于指代以无界数据构建的系统,同时使用更多的描述性术语,如近似/预测性结果,用于通常归类于 “流" 下的不同概念。此外,我们强调了大规模数据集的两个重要维度:cardinality(即,有界与无界)和编码(即,表与流),后者将耗费本书后半部分的大部分内容。
- 评估了设计良好的批处理和流式系统的相对能力,认为流式系统实际上是批处理的一个严格的超集,而像Lambda架构这样以流式系统劣于批处理为前提的概念,随着流式系统的成熟,注定要被淘汰。
- 提出了流式系统赶上并最终超过批处理所必需的两个高级概念,分别是正确性和时间推理工具。
- 确立了事件时间和处理时间之间的重要区别,描述了这些区别在分析数据时带来的困难,并提出了一种方法的转变,即从完整性的概念转向简单地适应数据随时间的变化。
- 通过批处理和流式引擎,研究了目前普遍使用的有界和无界数据的主要数据处理方法,将无界方法大致分为:不分时间、近似、按处理时间开窗和按事件时间开窗。
接下来,我们深入了解Beam模型的细节,从概念上看我们是如何将数据处理的概念分成四个相关的轴:什么、哪里、什么时候和如何。我们还详细研究了一个简单的、具体的、跨越多种场景的数据集的处理,强调了Beam模型所能实现的多种用例,并有一些具体的API让我们在现实中立足。这些例子将帮助我们理解本章介绍的事件时间和处理时间的概念,同时还探索了一些新的概念,如 Watermark。