流式数据处理在当今大数据领域是非常重要,这是有足够充分的理由的,如下:
在讨论可能遇到的不同类型的数据时,精确的术语也是很有用的。通过两个重要且正交的维度对数据可以唯一确定——Cardinality(基数)和 Constitution(结构)。
数据集的基数(Cardinality) 决定其大小,其最主要的方面是数据集是有限的,还是无限的。这有两个描述数据集粗略基数的术语:
当然,如果我们取交易系统中 2021 年 5 月 16 日的交易数据,那么此时交易数据就变成了有界数据。显而易见,有界数据是无界数据的子集。
另一方面,数据集的结构(Constitution),决定了它的物理表现形式。因此,结构定义数据的交互方式。有两种非常重要的结构:
相比 Batch,Stream 需要做两件事:
接下来,我们先厘清时间域(Time Domain) 的基本概念,之后在进一步分析变化的事件时间上的无界、无序数据是什么。
为了确切地讨论无界数据,还需要对时间域有一个清晰的认识。对于任何数据处理系统,我们通常会关心两种时间:
如果大家对 Flink 有所了解,那么会发现 Flink 还引入了 Ingestion Time(摄入时间),它是指数据到达 Flink 的时间。很少使用这种时间。
当然,不是所有的数据处理,都需要考虑时间因素。但是,大多数场景是需要考虑时间的,比如平台每小时销售额等。理想情况下,Event Time 和 Processing Time 应该是一直相等的。但现实却并非如此,造成二者差异的因素有以下:
现实中 Event Time 和 Processing Time 的差异大概如下图:
图中黑色虚线表示理想情况下,Processing Time 和 Event Time 是相等的。橘红色的曲线表示实际上 Processing Time 和 Event Time 的差异。其中系统刚开始时的 Processing Time 有些延迟,数据处理的中间阶段二者趋于一致,行将结束之际又出现延迟。
关于延迟/滞后的重要结论是:因为整个 Event Time 和 Processing Time 之间的映射关系不是静态的,所以我们不能仅仅根据 Processing Time 去分析。为了处理无界数据,数据处理系统提供了一个窗口的概念,我们在下文进行阐述。
如果你关心正确性和利用 Event Time 来处理数据,你就不能使用 Processing Time 定义数据的临时分界线。由于这两种时间没有可预测的关系,所以某些 Event Time 的数据可能会划分到错误的 Processing Time 窗口。
不幸的是,即使按照 Event Time 开窗,也不见得一定正确。在无界数据中,无序和不可预知的滞后导致了 Event Time 窗口的数据完整性问题:由于在处理时间和事件时间之间缺乏可预测的映射关系,如何确定何时观察到了给定 Event Time X 的所有数据呢?对于现实中的很多数据源,你都无法确定。
我们与其尝试将无界数据整理成最终变得完整的有限批次,不如让我们接受这些不确定性,设计一套工具。当新数据将到达,旧数据可能会被撤回或更新,我们构建的任何系统都应该能够自己处理这些事实,完整性的概念是对特定和适当用例的一种优化,而不是跨语义的必要性他们都是。
在讨论这种方案实现之前,我们先看看通用数据处理模式。
有界数据就是通常意义下的 Batch,处理有界数据概念上很直观,也被我们所熟悉。它是指将一个充满熵的数据集,经过某个数据处理引擎,如 MapReduce,最终产生一种新的结构化数据集。
批处理系统虽然设计时并未考虑无界数据,但是它已经被用于处理无界数据了。如你所料,它将无界数据分割成适合批处理的有界数据的集合。
大多数情况下,将输入的无界数据用固定大小的窗口分割为相互独立的有界数据,然后用批处理系统重复计算。尤其是对于日志这样的数据源,可以自然而然地将其按时间拆成一个树状结构,比如天级日志。
但是,实际上绝大多数系统还要解决数据完整性的问题。比如,如果由于网络分区导致数据延迟达到该怎么办?如果数据是在全球范围内收集的,并且必须在处理前传输到公共位置如何解决?这些都意味着我们必须要采用一些方法来解决,比如,直到延迟的数据都已经收集完之后再处理,或者一旦窗口中有延迟数据到达就重新处理整个批次。
固定窗口的批处理方法用于处理会话(Session)这种更复杂的窗口策略时,就会失效。会话是指一个连续的活动周期,由一个不活动的间隔所终止。类似于我们平时访问某些系统时,如果过一段时间不操作,就会重新登录。使用一般的批处理系统计算会话时,会出现一个会话被分到不同的窗口中,如图 4 中红色标记所示。我们可以通过增加批次大小来减少分割次数,但这会增加延迟的成本。另一个方案是添加额外的逻辑,来拼接之前的会话,但这就增加了复杂度。
由此可见,用任何一种传统的批处理系统计算会话都是不理想的。一个更好的方法是,在流上建立会话,在下文我们将会谈到。
与大多数基于批处理的无界数据处理方法相反,流式系统(Streaming Systems)是为无界数据而生的。正如我们之前谈到的,大多数分布式数据源不仅仅是无界数据,而且还具有以下特性:
根据无界数据的特点,可以将其处理方式划分为 4 类:
Time-Agnostic 的处理方式用于根本与时间无关的场景,所有相关的逻辑都是数据驱动。这种处理流式系统除了数据传输,没有什么特别需要支持的。下面让我们看几个例子:
过滤是最基本的时间无关处理。当一条数据达到,我们只需考虑它是否是我们感兴趣的,然后过滤掉不感兴趣的数据。因为这种事情在任何时间只与数据自身有关,所以与数据源是无界的、无序的、以及事件时间延迟都无关。
Inner Join 是另一个时间无关的例子。当 Join 两个无界数据源时,我们只关注 Join 的结果,计算逻辑中没有时间元素。当一个数据源的数据达到时,我们可以把它缓存起来,只有当另一个数据源的数据到达时,才去生成 Join 的结果。但是,对于没有发生的 Join 的数据,可能需要采取一些垃圾回收策略,这就和时间有关了。然而,对于很少或者没有未完成 Join 的用例,这种事情就不是问题了。
反观另一个语义 Outer Join,它却涉及数据完整性的问题。当我们观察到 Join 的一方到达后,如何知道另一方是否到达呢?事实上,我们无法确定,这就需要引入一些超时的概念,因此也就引入了时间元素。其实,时间元素本质上是一种窗口形式,后面的文章我们会进一步探讨。
近似算法也是时间无关的,例如,Top-N,K-means 等。它们消费无界数据,然后生成结果数据。近似算法具有以下优缺点:
接下来的两种是关于窗口化的变种。在之前我们已经有过简单的接触,窗口化只是获取数据源(无界或有界),并沿时间边界将其切割成有限块进行处理的概念。
现在我们先看看三种不同的窗口化策略:
我们前面讨论过两种时间域:Processing Time 和 Event Time。而窗口对于这两种时间域都是有意义的。现在我们就来看看它们的异同,首先从基于 Processing Time 的窗口开始吧。
当用 Processing Time 开窗时,系统实质上会将输入数据缓冲到窗口中,直到经过了一段的 Processing Time 为止。例如,对于 5 分钟的固定窗口,系统将按 Processing Time 缓存 5 分钟的数据,然后将这 5 分钟内接收到的数据视为一个窗口,并将其下发到下游。
基于 Processing Time 的窗口有以下几个优势:
基于 Processing Time 的窗口也有一个最大的劣势是:如果所讨论的数据具有关联的事件时间,这些数据必须以 Event Time 顺序到达,基于 Processing Time 的窗口无法反映这些事件实际发生的时间的事实。
当需要观察数据源中的有限一部分,以反映那些事件实际发生的时间时,可以使用 Event Time 窗口。在 2016 年之前,大多数使用的数据处理系统都缺乏对它的原生支持,虽然任何强一致性系统经过一些修改时能够解决的,比如 Hadoop 和 Spark Streaming 1.x。
图 10 中的黑色箭头指向的两个数据,它们达到的 Processing Time 窗口和它们所属的 Event Time 窗口是不一样的。因此,如果在某个关注 Event Time 的场景下,却使用了 Processing Time 窗口来计算,那么得到的结果就会是错误的。如我们所料,使用 Event Time 窗口可以保证数据事件时间的正确性。
关于无界数据源上的事件时间窗口的另一个好处是,可以创建动态大小的窗口,例如会话,而不会出现在固定窗口上生成会话时产生的任意拆分。
任何事情都犹如硬币的两面,Event Time 窗口的语义固然强大,但是它也有两个明显的缺陷:
本文介绍了很多内容:澄清术语,介绍完整性和时间工具两个重要概念,阐明 Processing Time 和 Event Time 的关系,分析四种常用的数据处理方法。