结构化串流的关键思想是将实时数据流视为不断追加的表。这就产生了一个新的流处理模型,它非常类似于批处理模型.您将把流式计算表示为静态表上的标准批处理查询,而 Spark 将其作为无界输入表上的增量查询运行。让我们更详细地了解一下这个模型。
1、基本概念
将输入数据流视为“输入表”。到达流的每个数据项就像一个新行被追加到 Input Table。
对输入的查询将生成“结果表”。每个触发器间隔(比如说,每1秒钟) ,新行就会附加到 Input Table,最终更新 Result Table。无论何时更新结果表,我们都希望将更改后的结果行写入外部接收器。
“Output”定义为写入外部存储器的内容。输出可以在不同的模式下定义:
- Complete Mode - 整个更新的结果表将写入外部存储。由存储连接器决定如何处理整个表的写入。
- Append Mode - 只有自最后一个触发器以来附加在Result Table中的新行将被写入外部存储。这仅适用于结果表中的现有行预计不会更改的查询。
- Update Mode - 只有自上次触发以来在结果表中更新的行将被写入外部存储器(自 Spark 2.1.1以来可用)。请注意,这不同于完成模式,因为这种模式只输出自上次触发器以来已更改的行。如果查询不包含聚合,那么它将等效于 Append 模式。
请注意,每种模式都适用于某些类型的查询。
为了说明这个模型的用法,让我们在上面的 Quick Example 的context 中理解这个模型。lines DataFrame 是输入表,最后wordCounts DataFrame 是结果表。注意,在流线上生成 wordCounts 的查询与静态 datatrame 完全相同。但是,在启动此查询时,Spark 将不断检查来自套接字连接的新数据。如果有新数据,Spark 将运行一个“增量”查询,该查询将以前的运行计数与新数据结合起来,以计算更新的计数,如下所示。
注意,结构化流不会实现整个表。它从流数据源中读取最新的可用数据,以增量方式处理以更新结果,然后丢弃源数据。它只保留更新结果所需的最小中间状态数据(例如,前面示例中的中间计数)。
该模型与许多其他流处理引擎明显不同。许多流系统要求用户自己维护正在运行的聚合,因此必须考虑容错性和数据一致性(至少一次、最多一次或正好一次)。在这个模型中,Spark 负责在出现新数据时更新结果表,从而减轻用户对结果表的推理。作为一个示例,让我们看看这个模型如何处理基于事件时间的处理和迟到的数据。
2、处理事件时间和延迟数据
事件时间是嵌入在数据本身中的时间。对于许多应用程序,您可能希望对此事件时间进行操作。例如,如果您希望获得物联网设备每分钟生成的事件数量,那么您可能希望使用数据生成时间(即数据中的事件时间) ,而不是 Spark 接收它们的时间。这个事件时间在这个模型中非常自然地表示出来——来自设备的每个事件都是表中的一行,而事件时间是该行中的一个列值。这使得基于窗口的聚合(例如每分钟的事件数)只是事件时间列上的一种特殊类型的分组和聚合——每个时间窗口是一个组,每一行可以属于多个窗口/组。因此,这种基于事件时间窗口的聚合查询可以在静态数据集(例如从收集的设备事件日志)和数据流上一致地定义,使用户的生活更加容易。
此外,根据事件时间,该模型自然地处理比预期晚到达的数据。由于 Spark 正在更新结果表,所以它完全控制在有最新数据时更新旧聚合数据,以及清理旧聚合数据以限制中间状态数据的大小。自从 Spark 2.1,我们支持水印,它允许用户指定后期数据的阈值,并允许引擎相应地清理旧的状态。这些将在后面的窗口操作部分进行更详细的解释。
3、容错语义
提供一次性的端到端语义是结构化流设计的关键目标之一。为了实现这一点,我们设计了结构化流源、接收器和执行引擎来可靠地跟踪处理的确切进度,以便它能够通过重新启动或重新处理来处理任何类型的故障。假定每个流源都有偏移量(类似于 Kafka 偏移量,或者 Kinesis 序列号)来跟踪流中的读取位置。引擎使用检查点和预写日志记录每个触发器中正在处理的数据的偏移范围。流式水槽设计成幂等的,用于处理再处理。通过使用可重复的资源和幂等汇聚,结构化流能够确保在任何失败情况下的端到端精确一次语义。