0%

《Streaming System》- 第一章:流处理入门[完整]

目录#

流式数据处理在大数据领域日益受到重视,原因如下:

  • 企业需要更及时的数据洞察,转向流式处理是实现更低延迟的好方法
  • 现代企业中越来越普遍的海量无界数据集,更容易使用针对这种不断增长的数据量设计的系统进行处理。
  • 随着数据的到达逐步处理,将工作负载均衡,积极消耗资源,从而产生更一致和可预测的结果。

尽管业务驱动的流式处理浪潮已经涌现,但与其批处理兄弟相比,流式处理系统仍然相对不成熟。直到最近,潮水才彻底向另一个方向倾斜。在我更加自大的时刻,我希望这在某种程度上是由于我最初在“流式处理101”和“流式处理102”博客文章中提供的坚实鞭策(本书的前几章明显基于这些文章)。但事实上,业界对流式处理系统成熟的兴趣很大,还有很多聪明而积极的人喜欢建立这些系统。

尽管我认为流式处理的普遍倡导已经取得了胜利,但我仍然会以几乎不改变的方式呈现我在“流式处理101”中提出的最初论点。首先,即使行业已经开始听从号召,这些论点仍然非常适用于今天。其次,仍有很多人没有收到这个备忘录;本书是一个长期的尝试,希望能够传达这些要点。

首先,我会涵盖一些重要的背景信息,这将有助于为我想讨论的其余主题提供框架。我将在三个具体的部分中进行:

  • 术语
    要精确地讨论复杂的主题,需要准确定义术语。对于某些具有超载解释的术语,我将尝试明确我所说的意思。
  • 功能
    我评论了流处理系统经常被认为存在的缺点。我还提出了我认为数据处理系统构建者需要采用的思维模式,以满足现代数据消费者的需求。
  • 时间领域
    我介绍了数据处理中相关的两个主要时间领域,展示它们的关系,并指出这两个领域所带来的一些困难。

术语:流处理是什么? |#

在进一步讨论之前,我想要澄清一件事:什么是流处理?如今,流处理这个术语被用来指代许多不同的事物(为了简便,到目前为止我也比较随意地使用了这个词),这可能会导致对流处理的真正含义或流处理系统实际能力的误解。因此,我更喜欢对这个术语进行一些明确的定义。

问题的关键在于,许多应该通过描述它们的“本质”(无限数据处理、近似结果等)来描述的内容,已经被俗称为通过流处理执行引擎等方式进行的操作。这种术语上的不精确使得流处理的真正含义变得模糊,而且在某些情况下,还会给流处理系统自身带来一些负担,即它们的能力被视为仅限于历史上被描述为“流处理”的特征,例如近似或推测结果。

鉴于设计良好的流处理系统与任何现有的批处理引擎一样有能力(在技术上甚至更有能力)产生正确、一致、可重复的结果,我更喜欢将“流处理”这个术语限定为一个非常具体的含义:

  • 流处理系统

    一种专门针对无限数据集设计的数据处理引擎。

如果我想谈论低延迟、近似或推测结果,我会使用那些具体的词汇,而不是不精确地称它们为“流处理”。

在讨论可能遇到的不同类型的数据时,使用准确的术语也是很有用的。在我看来,有两个重要(且正交的)维度定义了给定数据集的形状:基数和构成。

数据集的基数决定了它的大小,基数的最突出的方面是给定数据集是否有限或无限。这里是我用来描述数据集中粗略基数的两个术语:

  • 有界数据
    一种有限大小的数据集合。

  • 无界数据
    一种无限大小(至少在理论上)的数据集合。

基数很重要,因为无限数据集的无限本质会对处理它们的数据处理框架造成额外的负担。下一节中会详细讨论这个问题。

另一方面,数据集的构成则决定了它的物理表现形式。因此,构成定义了人们可以与所讨论的数据交互的方式。我们将在第6章深入研究构成,但为了让你对事情有一个简要的了解,这里有两个主要的重要构成:

  • 表格
    数据集在特定时间点的整体视图。传统上,SQL系统处理表格。


  • 数据集随时间演变的逐个元素视图。MapReduce数据处理系统的传承传统上处理流。

我们将在第6、8和9章深入探讨流和表格之间的关系,并且在第8章中,我们还会学习到将它们联系在一起的统一基本概念——时间可变关系。但在那之前,我们主要处理流,因为它是大多数数据处理系统(批处理和流处理)中管道开发人员直接交互的构成。它也是最自然地体现流处理所独有的挑战的构成。

关于流处理的严重夸大限制#

在这个话题上,让我们稍微谈谈流处理系统可以做什么和不能做什么,重点在于可以。我在本章中最想表达的一件事是,精心设计的流处理系统可以有多么强大。历史上,流处理系统一直被限制在提供低延迟、不准确或具有推测性的结果的某种利基市场上,通常与更有能力的批处理系统一起提供最终正确的结果;换句话说,这就是 Lambda 架构。

对于那些不熟悉 Lambda 架构的人,基本思路是同时运行一个流处理系统和一个批处理系统,两者都执行基本相同的计算。流处理系统提供低延迟、不准确的结果(要么是因为使用了近似算法,要么是因为流处理系统本身没有提供正确性),一段时间后,批处理系统会提供正确的输出。最初由 Twitter 的 Nathan Marz(Storm 的创作者)提出,它最终变得非常成功,因为它实际上是一个很棒的想法;流处理引擎在正确性方面有些令人失望,而批处理引擎与您所期望的一样不稳定,因此 Lambda 让您可以同时拥有两者的优点。不幸的是,维护 Lambda 系统很麻烦:您需要构建、提供和维护两个独立版本的管道,然后还要以某种方式合并两个管道的结果。

作为一位在强一致性流处理引擎上工作多年的人,我发现 Lambda 架构的整个原则有些不受欢迎。不出所料,当 Jay Kreps 的“质疑 Lambda 架构”文章出现时,我非常喜欢。这是对双模式执行必要性的首批高度可见声明。Kreps 在使用可回放系统(如 Kafka)作为流处理互联的情况下解决了可重复性问题,甚至提出了 Kappa 架构,这基本上意味着使用一个适合手头工作的精心设计的系统来运行单个管道。我不确定这个观点是否需要自己的希腊字母名称,但我完全支持这个原则。

说实话,我会更进一步。我会认为,精心设计的流处理系统实际上提供了批处理功能的严格超集。除了效率差异之外,批处理系统就像今天所存在的那样,不需要流处理系统。对于 Apache Flink 团队将这个想法贯彻到底并构建一个在“批处理”模式下始终是全流处理的系统,我表示赞赏。

1
2
3
4
5

**批处理和流处理的效率差异**

我认为这不是流处理系统的固有限制,而只是迄今为止大多数流处理系统所做的设计选择的结果。批处理和流处理之间的效率差异在很大程度上是批处理系统中更高效的捆绑和更有效的洗牌传输的结果。现代批处理系统采取了各种复杂的优化措施,允许使用令人惊讶的适度计算资源实现卓越的吞吐量。没有理由不能将使批处理系统成为效率重量级选手的那些聪明的洞察力类型纳入到为无界数据设计的系统中,为用户提供通常认为是高延迟、高效率的“批处理”处理和低延迟、低效率的“流处理”处理之间的灵活选择。这实际上是我们在 Google Cloud Dataflow 上所做的,通过在同一统一模型下提供批处理和流处理运行程序来实现。在我们的情况下,我们使用单独的运行程序,因为我们恰好有两个独立设计的系统,针对其特定的用例进行了优化。从工程角度来看,长期来看,我希望看到我们将两个系统合并成一个系统,同时保持选择适当效率级别的灵活性。但这不是我们今天拥有的东西。而且,由于统一的 Dataflow 模型,这甚至不是严格必要的;因此,它可能永远不会发生。

所有这些的推论是,流处理系统的广泛成熟,加上强大的无界数据处理框架,将在时间上允许将 Lambda 架构降级到大数据历史的古董中,它所属的地方。我相信现在是时候将其变为现实了。因为要做到这一点-也就是在其自己的游戏中击败批处理-您只需要两件事:

  • 正确性
    这可以使您与批处理平起平坐。在核心层面上,正确性归结为一致性存储。流处理系统需要一种方法来随着时间检查点持久状态(Kreps 在他的“为什么本地状态是流处理的基本原语”文章中谈到了这一点),并且必须设计得足够好,以在机器故障的情况下保持一致。几年前,当 Spark Streaming 第一次出现在公共大数据场景中时,它是一束光明中的一束一致性。幸运的是,从那时起,事情已经大大改善,但仍然令人惊讶的是,有多少流处理系统仍试图在没有强一致状态的情况下运行。

    重申一下-因为这一点很重要:强一致性对于精准一次处理是必需的,这对于任何具有超越批处理系统能力的机会的系统都是必需的。除非您真的不关心结果,否则请避免使用不提供强一致状态的流处理系统。批处理系统不需要您提前验证它们是否能够产生正确的答案;不要浪费时间在无法达到同样要求的流处理系统上。

    如果您想了解有关在流处理系统中实现强一致性所需的内容,我建议您查看 MillWheel、Spark Streaming 和 Flink 快照论文。所有这三个文件都花费了大量时间讨论一致性。Reuven 将在第 5 章深入探讨一致性保证,如果您仍然渴望更多,那么在文献和其他地方有大量关于这个话题的优质信息。

  • 关于时间的推理工具

    这可以让您超越批处理。对时间进行推理的良好工具对于处理不同事件时间偏移的无界、无序数据至关重要。越来越多的现代数据集呈现这些特征,而现有的批处理系统(以及许多流处理系统)缺乏应对它们所施加的困难的必要工具(尽管这正在迅速改变,即使在我写这篇文章时)。我们将在本书的大部分内容中解释和关注该点的各个方面。

    首先,我们对时间域的重要概念有了基本的理解,然后深入探讨了我所说的具有不同事件时间偏移的无界、无序数据。然后,我们将在本章的其余部分中使用批处理和流处理系统介绍有关有界和无界数据处理的常见方法。

事件时间(Event Time)与处理时间(Processing Time)#

要清晰地谈论无限数据处理,需要对所涉及的时间领域有清晰的理解。在任何数据处理系统中,通常有两个我们关心的时间领域:

  • 事件时间(Event time)
    这是事件实际发生的时间。

  • 处理时间(Processing time)
    这是事件在系统中被观察到的时间。

并非所有用例都关心事件时间(如果你的用例不关心,那太好了!你的生活就更轻松),但许多用例都关心。例如,随时间表征用户行为、大多数计费应用以及许多类型的异常检测等。

在理想的世界中,事件时间和处理时间总是相等的,事件会在其发生时立即被处理。然而,现实并不是那么友好,事件时间和处理时间之间的偏差不仅非零,而且通常是与底层输入源、执行引擎和硬件特性的特征高度可变的函数。会影响偏差水平的因素包括以下内容:

  • 共享资源限制,例如网络拥塞、网络分区或非专用环境中的共享CPU
  • 软件原因,例如分布式系统逻辑、争用等
  • 数据本身的特征,例如键分布、吞吐量方差或无序方差(即,在整个航班中使用脱机模式的人们将其手机从飞行模式中取出后)

因此,如果在任何现实世界的系统中绘制事件时间和处理时间的进度,通常会得到类似于图1-1中红线的东西。

图1-1. 时间域映射。x轴表示系统中事件时间的完整性;即,事件时间为X的所有数据的事件时间小于X。y轴表示处理时间的进度;即,数据处理系统在执行时观察到的正常时钟时间。

在图1-1中,黑色的斜率为1的虚线代表理想状态,即处理时间和事件时间完全相等;红线代表现实。在本例中,系统在处理时间开始时稍有延迟,中间更接近理想,然后在结束时又稍有延迟。乍一看,此图中可见两种类型的偏差,每种偏差在不同的时间域内:

  • 处理时间(Processing time)
    理想状态和红线之间的垂直距离是处理时间领域中的延迟。该距离告诉你在事件发生时间时,给定时间的事件的处理时间延迟有多少(在处理时间上)。这是两种偏差中可能更自然和直观的偏差。

  • 事件时间(Event time)
    理想状态和红线之间的水平距离是通道中事件时间偏差的量。它告诉你通道当前距理想状态(在事件时间上)有多远。

在现实中,任何给定时间点的处理时间延迟和事件时间偏差是相同的;它们只是看同一件事情的两种方式。关于延迟/偏差的重要经验教训是:由于事件时间和处理时间之间的总体映射不是静态的(即,延迟/偏差可以任意随时间变化),这意味着如果你关心它们的事件时间(即事件实际发生的时间),那么你不能仅在你的管道观察到它们时分析你的数据。

不幸的是,这是历史上许多为无限数据设计的系统所操作的方式。为了应对无限数据集的无限性,这些系统通常提供一些关于传入数据的窗口的概念。我们稍后会深入讨论窗口,但它基本上意味着将数据集沿时间边界分成有限的数据块。如果你关心正确性并且有兴趣在事件时间的上下文中分析你的数据,你不能使用处理时间定义那些时间边界(即处理时间窗口),因为许多系统这样做;由于分布式系统的固有滞后性、许多类型的输入源的在线/离线性等,处理时间和事件时间之间没有一致的相关性,因此你的事件时间数据中的一些数据将会落入错误的处理时间窗口内,从而使正确性丢失。我们将在接下来的部分以及本书的其余部分中通过许多示例来详细探讨这个问题。

不幸的是,当按事件时间进行窗口化时,情况并不完全乐观。在无限数据的上下文中,无序和可变的偏差会为事件时间窗口带来完整性问题:由于处理时间和事件时间之间缺乏可预测的映射,你如何确定何时观察到给定事件时间X的所有数据?对于许多现实世界的数据源,你根本无法做到这一点。但是,今天使用的绝大多数数据处理系统都依赖于某种完整性的概念,这使它们在应用于无限数据集时处于严重劣势。

我建议,我们应该设计一些工具,允许我们生活在这些复杂数据集所施加的不确定性世界中,而不是试图将无限数据整理成最终变得完整的有限批次的信息。新数据将到达,旧数据可能会被撤回或更新,任何我们构建的系统都应该能够自己应对这些事实,完整性的概念只是特定和适当的用例的方便优化,而不是跨所有用例的语义必要性。

在探讨这种方法可能看起来像什么之前,让我们完成一个有用的背景:常见的数据处理模式。

数据处理模式#

至此,我们已经有足够的背景知识,可以开始研究跨界和未界限数据处理今天常见的核心使用模式。我们看一下两种类型的处理,并在相关情况下,在我们关心的两种主要引擎的背景下(批处理和流处理,在这种情况下,我基本上将微批处理与流处理混合在一起,因为这两者之间的差异在这个层次上并不重要)。

有界数据#

处理有界数据在概念上非常简单,可能对每个人都很熟悉。在图1-2中,我们从左边开始,有一个充满熵的数据集。我们将其通过一些数据处理引擎(通常是批处理,但是一个设计良好的流处理引擎也可以)运行,例如MapReduce,在右侧得到具有更高内在价值的新结构化数据集。

图1-2。使用经典批处理引擎的有界数据处理。在左侧是一组未结构化的有限数据池,经过数据处理引擎运行后,在右侧是相应的结构化数据。

尽管在实际计算中,你可以对此方案进行无限变化,但总体模型非常简单。更有趣的是处理未界限数据的任务。现在让我们看一下通常处理未界限数据的各种方式,从传统批处理引擎使用的方法开始,然后结束于可以用于未界限数据的系统的方法,例如大多数流处理或微批处理引擎。

无界数据:批处理#

虽然批处理引擎并没有专门设计用于未界限数据,但自从批处理系统首次被构思以来,它们一直被用于处理未界限数据集。正如你所期望的那样,这样的方法围绕将未界限数据切片成适合批处理的一组有界数据集展开。

固定窗口

使用批处理引擎的重复运行最常见的处理未界限数据集的方法是将输入数据分成固定大小的窗口,然后将每个窗口作为单独的有界数据源(有时也称为滚动窗口)进行处理,如图1-3所示。特别是对于像日志这样的输入源,对于这样一个源,事件可以写入目录和文件层次结构中,其名称编码了它们对应的窗口,这种处理方式一开始似乎非常简单,因为你本质上已经通过基于时间的洗牌将数据提前放入适当的事件时间窗口中。

然而,实际上,大多数系统仍然需要处理完整性问题(如果一些事件由于网络分区而延迟到日志中怎么办?如果事件是全球收集的,并且必须在处理之前传输到共同的位置?如果事件来自移动设备?),这意味着可能需要某种缓解措施(例如,推迟处理,直到你确定收集到所有事件或在数据到达后重新处理给定窗口的整个批次)。

图1-3。使用经典批处理引擎通过自适应固定窗口处理未界限数据。一组未界限数据通过前置收集成为有限的固定大小的有界数据窗口,然后通过经典批处理引擎的连续运行进行处理。

会话

当尝试使用批处理引擎将未界限数据处理成更复杂的窗口策略(如会话)时,这种方法会更加崩溃。会话通常被定义为活动期间(例如,对于特定用户)在不活动期间终止。在使用典型批处理引擎计算会话时,您通常会得到跨批次拆分的会话,如图1-4中的红色标记所示。我们可以通过增加批次大小来减少拆分的数量,但代价是增加延迟。另一个选择是添加额外的逻辑来从之前的运行中拼接会话,但代价是增加更多的复杂性。

图1-4。使用经典批处理引擎通过自适应固定窗口将未界限数据处理为会话。未界限数据通过前置收集成为有限的固定大小的有界数据窗口,然后通过经典批处理引擎的连续运行进行处理,再被细分为动态会话窗口。

无论哪种方式,使用经典批处理引擎计算会话都不理想。更好的方法是以流方式构建会话,我们稍后再看。

无界数据:流处理#

与大多数基于批处理的无限数据处理的临时性质不同,流处理系统是为无限数据而建立的。正如我们之前所讨论的,对于许多真实世界的分布式输入来源,你不仅需要处理无限数据,还需要处理以下类型的数据:

  • 与事件时间高度无序,这意味着如果你想在事件发生的上下文中分析数据,你需要在你的管道中进行某种基于时间的随机洗牌。
  • 具有不同的事件时间偏移,这意味着你不能仅仅假设对于一个给定的事件时间X,你总是能在一定的时间Y内看到大部分数据。

当处理这些特征的数据时,你可以采取一些方法。我通常将这些方法分为四类:时间不敏感、近似、按处理时间分窗口和按事件时间分窗口

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

时间不敏感#

时间不敏感处理用于时间基本无关的情况;也就是说,所有相关的逻辑都由数据驱动。因为这些用例的一切都是由更多数据的到来所决定的,所以流引擎需要支持的除了基本的数据传递之外,实际上没有什么特别之处。因此,实际上所有现有的流处理系统都支持时间不敏感的用例(当然,如果你关心正确性,系统之间的一致性保证可能存在差异)。对于无限数据源的时间不敏感处理,批处理系统也非常适用,只需要将无限源切割成任意顺序的有界数据集并独立处理这些数据集即可。我们在这一部分中看一些具体的例子,但是由于处理时间不敏感(至少从时间的角度来看)的简单性,我们不会花太多时间进行详细讨论。

过滤

时间不敏感处理的一个非常基本的形式是过滤,如图1-5所示的例子。想象一下,你正在处理网站流量日志,你想要过滤掉所有不来自特定域名的流量。当每个记录到达时,你会查看它是否属于感兴趣的域名,如果不属于,则删除它。因为这种处理方式只取决于任何时间的单个元素,所以数据源是无限的、无序的和具有不同事件时间偏移是不相关的。

图1-5。过滤无限数据。一个包含多种类型的数据集(从左到右流动)被过滤成一个包含单个类型的同质集合。

内连接

另一个时间不敏感的例子是内连接,如图1-6所示。当连接两个无限数据源时,如果你只关心两个源的元素到达时的连接结果,那么逻辑中就没有时间元素。当看到一个值来自一个源时,你可以简单地将其缓冲在持久状态中;只有另一个源的第二个值到达后,你才需要发出连接记录。(实际上,你可能需要为未发出的部分连接引入某种垃圾回收策略,这可能是基于时间的。但是对于没有或几乎没有未完成连接的用例来说,这种情况可能不是问题。)

图1-6。对无限数据执行内连接。当观察到两个源的匹配元素时,将产生连接。

将语义切换到某种外连接会引入我们所讨论的数据完整性问题:在你看到连接的一侧之后,你如何知道另一侧是否会到达?说实话,你不知道,因此需要引入某种超时的概念,这引入了时间元素。那个时间元素本质上是一种窗口,我们稍后会更仔细地看一下。

近似算法#

第二个主要的方法类别是近似算法,例如近似Top-N、流式K-means等。它们接受一个无限的输入源,并提供输出数据,如果你仔细看它们,它们看起来或多或少像你希望得到的,如图1-7所示。近似算法的优点是它们通过设计是低开销的,并且专为无限数据而设计。缺点是只有有限的一些算法,算法本身通常很复杂(这使得创建新算法变得困难),而且它们的近似性质限制了它们的效用。

图1-7。计算无限数据的近似值。数据被运行通过一个复杂的算法,产生的输出数据看起来或多或少像另一边所希望的结果。

值得注意的是,这些算法通常在设计中具有一定的时间元素(例如内置的衰减)。由于它们处理元素的方式是随着其到达,所以时间元素通常是基于处理时间的。这对于那些提供一些可证明误差边界的算法特别重要。如果这些误差边界是基于数据按顺序到达的,那么当你提供无序数据和具有不同事件时间偏移的算法时,它们基本上没有什么意义。这是需要记住的事情。

近似算法本身是一个迷人的主题,但是由于它们本质上是另一个时间不敏感处理的例子(除了算法本身的时间特征),因此它们非常容易使用,因此在我们当前的重点下,不值得进一步关注。

窗口化#

剩下的两种无限数据处理方法都是窗口化的变体。在深入探讨它们之间的差异之前,我应该明确一下我所说的窗口化的含义,因为在上一节中我们只简单提到了它。窗口化的本质就是将数据源(无论是无限的还是有限的)沿着时间轴切分成有限的块进行处理。图1-8展示了三种不同的窗口化模式。

图1-8. 窗口化策略。每个示例都显示了三个不同的键,突出了对齐窗口(适用于所有数据)和非对齐窗口(适用于数据子集)之间的差异。

让我们仔细看看每个策略:

  • 固定窗口(也称为滚动窗口)
    我们之前讨论过固定窗口。固定窗口将时间分成具有固定时长的段。通常(如图1-9所示),固定窗口的时间段会均匀地应用于整个数据集,这是对齐窗口的一个示例。在某些情况下,为了使窗口完成负载更均匀地分布在时间上,需要对不同数据子集(例如每个键)的窗口进行相位移动,这是非对齐窗口的一个示例,因为它们在数据集中的位置是不同的。

  • 滑动窗口(也称为跳跃窗口)
    滑动窗口是固定窗口的一种推广,由固定长度和固定周期定义。如果周期小于长度,则窗口会重叠。如果周期等于长度,则您有固定窗口。如果周期大于长度,则您将得到一种奇怪的采样窗口,该窗口只在一段时间内查看数据子集。与固定窗口一样,滑动窗口通常是对齐的,但在某些用例中,它们可以是非对齐的性能优化。请注意,图1-8中的滑动窗口是这样绘制的,以便给出滑动运动的感觉;实际上,所有五个窗口都适用于整个数据集。

  • 会话
    作为动态窗口的一个示例,会话由一段未活动时间大于某个超时时间的事件序列组成。会话通常用于分析用户随时间变化的行为,通过将一系列时间相关事件(例如一段时间内观看的视频序列)分组在一起。会话很有趣,因为它们的长度无法预先定义;它们取决于实际涉及的数据。它们也是非对齐窗口的典型示例,因为会话在不同的数据子集(例如不同的用户)之间几乎永远不会完全相同。

我们之前讨论过的两个时间域(处理时间和事件时间)是我们关心的两个域。窗口化在两个域中都有意义,因此让我们详细看看它们的区别。因为处理时间窗口化历史上更加常见,所以我们从那里开始。

按处理时间窗口化#

当按处理时间窗口化时,系统基本上会缓冲输入数据直到一定的处理时间已经过去为止。例如,在五分钟的固定窗口的情况下,系统将缓冲五分钟的处理时间的数据,之后,将观察到的这五分钟的所有数据作为一个窗口发送到下游进行处理。

图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中找到的水位线等方式给出窗口完成的合理准确的启发式估计(我们将在第3章和第4章中更多地讨论它们)。但对于绝对正确性至关重要的情况(再次考虑计费),唯一的选择是为流水线构建者提供一种表达他们希望何时对窗口的结果进行物化以及如何随时间改进这些结果的方法。处理窗口完整性(或缺乏完整性)是一个有趣的话题,但最好在具体示例的上下文中探讨,我们将在下一节中看到。

概述#

哇!这是很多信息。如果你已经走到了这一步,你应该受到赞扬!但是我们只是刚刚开始。在深入探讨Beam模型方法之前,让我们简要回顾一下我们到目前为止所学到的内容。在本章中,我们完成了以下工作:

  • 澄清术语,将“流式处理”的定义聚焦于考虑未限定数据的系统,同时使用更具描述性的术语,如近似/推测结果,来区分经常归为“流式处理”范畴的不同概念。此外,我们强调了大规模数据集的两个重要维度:基数(即有界与无界)和编码(即表格与流式),后者将占据本书下半部分的大部分。
  • 评估了精心设计的批处理和流处理系统的相对能力,认为流处理实际上是批处理的严格超集,并且像Lambda架构这样的概念,建立在流处理劣于批处理的基础上,注定将因为流处理系统的成熟而退役。
  • 提出了两个高层次概念,这些概念对于流处理系统追赶并最终超越批处理至关重要,分别是正确性和关于时间推理的工具。
  • 确定了事件时间和处理时间之间的重要差异,描述了这些差异在分析数据时所带来的困难,并建议从完整性的概念转向简单地适应随时间发生的数据变化的方法。
  • 查看了今天常用的有界和无界数据的主要数据处理方法,通过批处理和流处理引擎大致将无界方法分类为:时间不可知、近似、通过处理时间进行窗口处理和通过事件时间进行窗口处理。

接下来,我们深入探讨Beam模型的细节,从概念上看,我们将数据处理的概念分为四个相关的方面:什么、在哪里、何时和如何。我们还详细研究了在多种情况下处理简单且具体的示例数据集,突出了Beam模型所支持的多种用例,同时提供一些具体的API,以使我们更加接近现实。这些示例将有助于加深本章介绍的事件时间和处理时间的概念,同时探索诸如水位线等新概念。


  1. 为了完整起见,值得一提的是,此定义包括真正的流式处理和微批次实现。对于那些不熟悉微批处理系统的人来说,它们是使用重复执行批处理引擎来处理不受限制的数据的流式处理系统。Spark Streaming是行业中的典型例子。
  2. 熟悉我原始的“流式处理101”文章的读者可能会记得,我强烈鼓励在引用数据集时放弃“流”的术语。这从未流行起来,我最初认为是因为它的流行性和普遍使用。然而,回顾一下,我认为我错了。区分两种不同类型的数据集组成:表和流,实际上是很有价值的。事实上,本书的大部分内容都致力于理解这两者之间的关系。
  3. 如果您不熟悉我所说的“精确一次”,它是指某些数据处理框架提供的特定一致性保证类型。一致性保证通常分为三个主要类别:最多一次处理,最少一次处理和恰好一次处理。请注意,这里使用的名称是指在管道生成的输出中观察到的有效语义,而不是管道可能处理(或尝试处理)任何给定记录的实际次数。因此,有时会使用“实际上一次”这个术语,而不是“精确一次”,因为它更代表事物的基本本质。Reuven在第5章中更详细地介绍了这些概念。
  4. 自“流式处理101”最初发布以来,许多人向我指出,在x轴上放置处理时间并在y轴上放置事件时间更直观。我确实同意,交换两个轴最初会感觉更自然,因为事件时间似乎是因变量,而处理时间是独立变量。然而,由于两个变量都是单调的且密切相关,它们实际上是相互依赖的变量。因此,我认为从技术角度来看,您只需要选择一个轴并坚持使用它即可。数学很令人困惑(尤其是在北美以外的地方,它突然变得复数并对你发起攻击)。
  5. 这个结果真的不应该令人惊讶(但对我来说是,因此我要指出来),因为我们在测量两种偏差/滞后类型时实际上正在创建一个理想线与其成直角的直角三角形。数学很酷。
  6. 我们会在第2章详细介绍对齐的固定窗口,第4章介绍非对齐的固定窗口。
  7. 如果您在学术文献或基于SQL的流处理系统中四处寻找,还会发现第三个时间窗口领域:“基于元组的窗口”(即,大小以元素数计算的窗口)。但是,基于元组的窗口本质上是一种处理时间窗口,其中元素在到达系统时分配单调递增的时间戳。因此,我们不会进一步详细讨论基于元组的窗口。