《Streaming Systems》第一章 Streaming 101

Streaming 101 的标题来自于作者曾经发表的两篇博客文章:”Streaming 101“ 和 “Streaming 102”。

第一章包括了对流处理中基本概念的描述,讨论了流式系统的能力、两种重要的时间概念,以及一些常见的数据处理模式。

流式数据处理是如今大数据中的一大重要话题,理由有以下几点:

  • 企业希望能够更及时地了解和洞悉他们的数据,切换到流式处理是降低延迟的一个很好的方法
  • 使用专为此类永无止境的大量数据设计的系统,可以更容易地控制在现代企业中越来越普遍的海量、无界数据集
  • 当数据到达时进行处理可以使工作量在时间上的分布更加均匀,从而可以提供更加一致和可预测的资源消耗量

术语:什么是流?

对于流进行准确定义的一大难题在于:很多事物都应该被描述为它们是什么,但它们最终被俗称为在历史上是如何被实现的(即流处理引擎)。术语上准确性的缺失导致了在某些情况下,流式系统的能力被错误地限制成在历史上被描述为“流”的系统所具备的能力,比如只能产生用于估计或预测性的结果。

考虑到经过良好设计的流式系统具有和批处理系统一样产生正确、一致、可重复结果的能力,这里我们更倾向于将术语“流”设定为一个非常具体的含义:

流式系统:一种在设计时考虑到无界数据集的数据处理引擎。

准确的术语在讨论不同类型的数据时也大有益处,从作者角度来看,有两个重要且正交的维度来定义一个给定的数据集:cardinality(基数)constitution(结构)

Cardinality 描述了数据集的规模。对于描述粗粒度的 cardinality,有两种不同的数据集:有界数据无界数据

Constitution 描述了数据的物理表现形式。constitution 定义了问题中跟数据交互的方式,简单来说,有两种主要的 constitution:TableStream

缺陷被过分夸大的流式系统

在历史上,流式系统一度被贬低为只能提供低延迟、不准确或是预测性的结果,通常跟一个能力更强并能提供最终准确结果的批处理系统结合在一起,也就是通常说的Lambda架构。

Lambda 架构的维护是一大难题,你需要构建、准备和维护两套独立的 pipeline,并且最终要以某种方式将两套 pipeline 的结果合并到一起。

随着之后数年在强一致性流式引擎上的研究,Lambda 架构逐渐不再有吸引力。Jay Kreps 发表了《Questioning the Lambda Architecture》的博文,他解决了在使用像 Kafka 这样的可重放系统与流式系统连接背景下的可重复性问题,并且提出了 Kappa 架构,基本含义为使用适合当前作业的且经过精心设计的系统只运行单个 pipeline。

这里,作者希望更进一步,他认为经过精心设计的流式系统实际上提供了批处理功能的严格超集。取模的结果可能是效率上的提升,我们不再需要如今还存在着的批处理系统。感谢 Apache Flink 社区的人们将这个想法铭记于心,并构建了一个即使在批处理模式下也在幕后全程进行着流式处理的系统。

其实,为了能够击败批处理系统,只需要做到两点:

1. 正确性(Correctness)

这使你可以与批处理并驾齐驱。正确性可以归结为一致性存储。流式系统需要有能够随着时间推移持久化 checkpoint 状态的方法。考虑到机器故障,这需要精心的设计来保证一致性。再次重申非常重要的一点:正确性需要 exactly-once 处理语义,exactly-once 处理语义需要强一致性。

2. 推断时间的工具(Tools for reasoning about time)

这使你可以超越批处理。推断时间的工具对于处理无界无序且处理时间偏差不断变化的数据是必不可少的。

事件时间 vs 处理时间

在任何数据处理系统中,一般都有两种我们所关心的时间:

事件时间 Event time:事件真实发生时的时间

处理时间 Processing time:事件被在系统中被观察到的时间

不是所有的案例都关心事件时间,但绝大多数都会。在理想情况下,事件时间和处理时间应该总是相等的,事件在发生时就立刻被处理。但在真实世界中,事件时间和处理时间之间的偏差不仅是非零的,而且是关于输入源、执行引擎和硬件特征相关的高度可变的函数。影响事件时间和处理时间之间偏差级别的因素包括以下几点:

  • 共享资源的限制,如网络拥塞,网络分区,或在非专用环境中的CPU共享
  • 软件原因,如分布式系统逻辑、竞争等
  • 数据自身的特征,比如键值分布,吞吐量变化,或其他无法预知的变化(如飞机乘客在整个航班飞行过程中离线使用手机后退出飞行模式)

结果是,如果将真实世界中事件时间和处理时间之间的过程关系画到图中,可能如下图所示:

Figure 1-1

数据处理模式

有界数据

处理有界数据在概念上非常直接,在下图中,我们在左图数据集上运行数据处理引擎,比如 MapReduce,得到右图中具有新结构且有更多内在价值的数据集。

Figure 1-2

无界数据:批处理

批处理引擎虽然在设计时没有显式考虑无界数据,但从批处理系统被构思出来以来,它们也可以用于处理无界数据集。处理方法无非是将无界数据切割成适合批处理的有界数据集合。

固定窗口

最常见的方式,将输入数据划分到固定大小的窗口,再分别处理这些有界的窗口,如下图所示。

Figure 1-3

但实际上,大部分系统仍然需要解决完整性(completeness)的问题(考虑到网络问题,事件写入日志可能会有延迟,事件可能来自移动设备等等),这意味一些缓解措施可能是必要的,比如推迟处理过程直到确认所有数据都已经被收集到,或者在数据延迟到达时重新处理整个批中的数据等。

会话

会话一般被定义为一段时间内的活动,当一段时间不出现活动时即终止。当用传统批处理引擎计算会话时,总是会出现会话被分割到不同 batch 中的情况,如下图中的红色标记所示。我们可以通过增加 batch size 的方式来减少这种情况,但代价是会增大延迟。另一种选择是通过增加额外的逻辑来拼接会话,但代价是使问题变得更加复杂。

Figure 1-4

无界数据:流处理

相比于基于批的方式处理无界数据的方法,流式系统是转为无界数据而设计的。对于许多现实世界中的分布式输入源,数据不仅仅是无限的,同时还有如下特点:

  • 在事件时间上高度无序
  • 不断变化的事件时间偏差

对于处理具有这些特征的数据,方法大致可以分为四类:不考虑时间(Time-agnostic
)、近似估计(Approximation algorithms)、根据处理时间划分窗口以及根据事件时间划分窗口。

不考虑时间

该方法被用来处理时间无关紧要的场景,所有相关逻辑都是数据驱动的。批处理系统同样适用于该场景。具体案例有:

  1. 过滤

Figure 1-5

  1. Inner joins 内连接

当将两个无界数据源做内连接时,如果结果仅仅关心同时从两个源中到达的元素,那么这里的逻辑并不包含时间元素。只需要将某一边数据缓存到持久化状态中,当另一边也出现该数据后发送 join 结果即可。(实际上,这里可能需要一些垃圾收集策略,这可能会是基于时间的)

Figure 1-6

但如果将语义切换到外连接,那么就会引入数据完整性的问题:看到某一边的某个数据之后,如何知道另一边会不会再次出现这条数据?实际情况是无法判断,所以这里需要引入超时概念,也就引入了时间元素。

近似估算算法

第二大类方法是估算算法,比如估算 Top-N流式 k-means 等等。这些算法以无界数据为输入,输出近似希望得到的结果。

估算算法的优势是低开销,且适用于无界数据;劣势是只适用于很少的情况,并且算法本身非常复杂。

值得注意的是这些算法在设计中通常是有时间元素的,且通常是处理时间。这对于那些在近似值上提供可证明的误差界限的算法非常重要。 如果这些错误界限是基于数据按序到达的,那么当为算法提供具有可变事件时间偏差的无序数据时,就基本上没有任何意义。

窗口

窗口是从有界或无界数据源获取数据,沿时间边界将其分割成有限大小的块并进行处理的概念。 下图展示了三种不同模式的窗口。

Figure 1-8

  1. 滚动窗口
    滚动窗口将时间划分为固定长度的片段,如下图,窗口在整个数据集上均匀分布,并且是对齐的窗口,在某些情况下可能希望对数据的不同子集(如不同 key)上的窗口做一些偏移处理,以使得窗口结束时的负载能够均匀地分布到不同时间上,这是非对齐窗口(在第六章会详细说明)的一个实例。
    Figure 1-9
  2. 滑动窗口
    滚动窗口的一般化形式,通过一个固定的总长度和固定的周期来定义。如果周期小于总长度,窗口会互相重叠,周期与总长度相等便是滚动窗口,如果周期大于总长度,便得到只处理部分子集数据的抽样窗口。滑动窗口通常是对齐的,但在一些特定案例中为了提升性能也可以是非对齐的。
  3. 会话窗口
    会话窗口是动态窗口的一个实例,会话由一系列事件组成,当不活动时间超出设定的超时时间后会话结束。会话将时间上相关的一系列行为聚集到一起,通常被用来分析用户行为。会话窗口有趣的地方在于它们的长度取决于相关的真实数据,它们也是经典的非对齐窗口。

处理时间窗口

处理时间窗口有一些很好的性质:

  • 简单性
  • 判断窗口完整性的方法非常直接
  • 如果你希望根据数据在数据源中被观察到的时间来处理信息,处理时间窗口正是你所需要的。许多监控的场景正好属于此类。

处理时间窗口的一大缺陷也很明显:如果问题中的数据有与其相关的事件时间,那么这些数据必须以事件时间的顺序到达,处理时间窗口才能反映出它们真实发生的时间。但不幸的是,顺序的事件时间数据在大多数真实世界的分布式输入源中并不常见。

一个简单的例子,手机 app 收集用户数据并用于后续处理,如果手机设备在某段时间离线,在这段时间内收集的数据需要在恢复在线后才会上传,这意味着数据在事件时间上可能误差若干分钟,小时,天甚至周。使用处理时间窗口在这样的数据集上不可能得到任何有用的结论。

另一个例子,考虑一个从各大洲收集数据的全球性服务,如果在带宽受限的跨大陆线路上出现的网络问题减小了带宽/增大了延迟,部分数据到达时间就会出现明显延迟。这时处理时间窗口不再能代表事件真实发生的时间,只能代表事件到达处理管道的时间,可能包含了旧数据和新数据的任意组合。

事件时间窗口

事件时间窗口是窗口的黄金标准。在2016年之前,大多数数据处理系统缺少对事件时间窗口的原生支持。而在今天,多数系统都能够在一定程度上提供原生对事件时间窗口的支持。

下图是对无界数据源进行一个小时滚动窗口划分的样例,数据会根据事件时间划分到对应窗口。

Figure 1-10

事件时间窗口可以是动态大小的,下图是对无界数据源进行会话窗口划分的样例。与批处理中从固定大小窗口中生成会话相比,这里不会再出现会话被分割的现象。

Figure 1-11

由于事件时间窗口在处理时间中总是会比窗口的实际时长存在更长时间,因此事件时间窗口有两个显著的缺陷:

  1. 缓存
    由于更长的窗口存在时间,更多的数据需要被缓存。这并不是一个很严重的问题,因为在数据处理系统依赖的资源中,持久化存储的成本通常是最低的。并且很多聚合操作比如求和求平均数并不需要缓存完整的输入数据集合,只需要进行增量计算,这只需要很小的中间状态。
  2. 完整性
    考虑到我们并没有一个好办法来得知我们是否已经得到了一个窗口中的全部数据,因此我们也不知道应该在何时发出窗口数据的处理结果。对很多不同类型的输入,一些系统可以通过水位线给出一个合理的窗口完成时间的启发式预测结果。但对于正确性要求极高的场景,唯一可靠的方法是让用户决定他们希望何时发出窗口的处理结果以及如何在后续修正这些结果。

《Streaming Systems》第一章 Streaming 101

http://huangxiao.info/2021/12/07/streaming-systems-chapter1/

作者

Shawn Huang

发布于

2021-12-07

更新于

2021-12-09

许可协议

评论