Introduction
我们正处于大数据时代,组织不断收集大量数据。然而,这种数据泛滥的价值取决于及时提取可操作见解的能力。因此,对连续应用程序的需求日益增长,这些应用程序可以从海量数据提取流中获得实时可操作的见解。
然而,构建生产级连续应用程序可能具有挑战性,因为开发人员需要克服许多障碍,包括:
-
提供端到端的可靠性和正确性保证 - 长期运行的数据处理系统必须通过确保输出与批处理的结果一致来适应故障。此外,必须持续监控异常活动(例如上游组件故障,流量峰值等)并自动减轻这些活动,以确保实时提供高度可用的洞察力。
-
执行复杂的转换 - 数据以无数种格式(CSV,JSON,Avro等)到达,在使用之前通常必须进行重组,转换和扩充。这种重组要求批处理系统中的所有传统工具都可用,但没有通常需要的额外延迟。
-
处理迟到或无序数据 - 在处理物理世界时,迟到或无序的数据是生活中的事实。因此,必须在新信息到达时连续(和准确)修改聚合和其他复杂计算。
-
与其他系统集成 - 信息源自各种来源(Kafka,HDFS,S3等),必须集成这些来源以查看完整的图片。
Structured Streaming以Spark SQL 为基础, 建立在上述基础之上,借用其强力API提供无缝的查询接口,同时最优化的执行低延迟持续的更新结果。
流数据ETL操作的需要
ETL: Extract, Transform, and Load
ETL操作可将非结构化数据转化为可以高效查询的Table。具体而言需要可以执行以下操作:
-
过滤,转换和清理数据
-
转化为更高效的存储格式,如JSON(易于阅读)转换为Parquet(查询高效)
-
数据按重要列来分区(更高效查询)
传统上,ETL定期执行批处理任务。例如实时转储原始数据,然后每隔几小时将其转换为结构化表格,以实现高效查询,但高延迟非常高。在许多情况下这种延迟是不可接受的。
幸运的是,Structured Streaming 可轻松将这些定期批处理任务转换为实时数据。此外,该引擎提供保证与定期批处理作业相同的容错和数据一致性,同时提供更低的端到端延迟。
使用结构化流转换原始日志
val cloudTrailSchema = new StructType() .add("Records", ArrayType(new StructType() .add("additionalEventData", StringType) .add("apiVersion", StringType) .add("awsRegion", StringType) // ...
val rawRecords = spark.readStream .schema(cloudTrailSchema) .json("s3n://mybucket/AWSLogs/*/CloudTrail/*/2017/*/*")
这里的rawRecords为Dataframe,可理解为无限表格
这允许我们将批处理和流数据视为表。由于表和DataFrames / Datasets在语义上是同义的,因此可以对批处理和流数据应用相同的类似批处理的DataFrame / Dataset查询。在这种情况下,我们将转换原始JSON数据,以便使用Spark SQL的内置支持来操作复杂的嵌套模式更容易查询。
val cloudtrailEvents = rawRecords .select(explode($"records") as 'record) .select( unix_timestamp( $"record.eventTime", "yyyy-MM-dd'T'hh:mm:ss").cast("timestamp") as 'timestamp, $"record.*")
在这里,我们explode(拆分)从每个文件加载的记录数组到单独的记录中。我们还将每个记录中的字符串事件时间字符串解析为Spark的时间戳类型,并将嵌套列展平以便于查询。请注意,如果cloudtrailEvents是一组固定文件上的批处理DataFrame,那么我们就会编写相同的查询,并且我们只会将结果写为一次parsed.write.parquet("/cloudtrail")。相反,我们将启动一个连续运行的StreamingQuery,以便在新数据到达时对其进行转换。
val streamingETLQuery = cloudtrailEvents .withColumn("date", $"timestamp".cast("date") // derive the date .writeStream .trigger(ProcessingTime("10 seconds")) // check for files every 10s .format("parquet") // write as Parquet partitioned by date .partitionBy("date") .option("path", "/cloudtrail") .option("checkpointLocation", "/cloudtrail.checkpoint/") .start()
StreamingQuery将会连续运行,当新数据到达时并会对其进行转换
这里我们为StreamingQuery指定以下配置:
-
从时间戳列中导出日期
-
每10秒检查一次新文件(即触发间隔)
-
将已解析的DataFrame中的转换数据写为路径中的Parquet格式表/cloudtrail。
-
按日期对Parquet表进行分区,以便我们以后可以有效地查询数据的时间片; 监控应用程序的关键要求。
-
在/checkpoints/cloudtrail容错路径中保存检查点信息
从概念上讲,rawRecordsDataFrame是仅附加的输入表,cloudtrailEventsDataFrame是转换的结果表。换句话说,当新行附加到input(rawRecords)时,结果表(cloudtrailEvents)将具有新的转换行。在这种特殊情况下,每隔10秒,Spark SQL引擎就会触发对新文件的检查。当它找到新数据(即输入表中的新行)时,它会转换数据以在结果表中生成新行,然后将其写为Parquet文件。
此外,在运行此流式查询时,您可以使用Spark SQL同时查询Parquet表。流式查询以事务方式写入Parquet数据,使得并发交互式查询处理将始终看到最新数据的一致视图。这种强有力的保证称为前缀完整性,它使结构化流媒体管道与更大的连续应用程序很好地集成。
从故障中恢复以获得完全一次的容错保证
长时间运行的管道必须能够容忍机器故障。使用结构化流,实现容错就像为查询指定检查点位置一样简单。在前面的代码片段中,我们在以下行中执行了此操作。
.option("checkpointLocation", "/cloudtrail.checkpoint/")
此检查点目录是每个查询,并且在查询处于活动状态时,Spark会不断将已处理数据的元数据写入检查点目录。即使整个群集出现故障,也可以使用相同的检查点目录在新群集上重新启动查询,并始终进行恢复。更具体地说,在新集群上,Spark使用元数据来启动新查询,其中失败的一个停止,从而确保端到端的一次性保证和数据一致性
此外,只要输入源和输出架构保持不变,同样的机制允许您在重新启动之间升级查询。从Spark 2.1开始,我们使用JSON对检查点数据进行编码,以实现面向未来的兼容性。因此,即使在更新Spark版本后,您也可以重新启动查询。在所有情况下,您将获得相同的容错和一致性保证。
将实时数据与历史/批次数据相结合
许多应用程序需要将历史/批处理数据与实时数据相结合。例如,除了传入的审计日志之外,我们可能已经有大量积压的日志等待转换。理想情况下,我们希望尽快实现这两者,以交互方式查询最新数据,并且还可以访问历史数据以供将来分析。使用大多数现有系统设置此类管道通常很复杂,因为您必须设置多个进程:用于转换历史数据的批处理作业,用于转换实时数据的流式管道,以及可能需要另一个步骤来组合结果。
结构化流媒体消除了这一挑战。您可以配置上述查询,以便在处理新数据文件到达时对其进行优先级排序,同时使用空间群集容量来处理旧文件。首先,我们将latestFirst文件源的选项设置为true,以便首先处理新文件。然后,我们设置maxFilesPerTrigger限制每次处理的文件数。这会调整查询以更频繁地更新下游数据仓库,以便最新数据可用于尽快查询。我们可以一起定义rawLogsDataFrame,如下所示:
val rawJson = spark.readStream
.schema(cloudTrailSchema)
.option("latestFirst", "true")
.option("maxFilesPerTrigger", "20")
.json("s3n://mybucket/AWSLogs/*/CloudTrail/*/2017/01/*")
通过这种方式,我们可以编写单个查询,轻松地将实时数据与历史数据相结合,同时确保低延迟,高效率和数据一致性。
结论
Apache Spark中的结构化流是编写流式ETL管道的最佳框架,如上所述,Databricks可以轻松地在生产中大规模运行它们。我们分享了步骤的高级概述 - 提取,转换,加载和最终查询 - 设置流式ETL生产管道。我们还讨论并演示了Structured Streaming如何克服在生产中解决和设置高容量和低延迟流媒体管道的挑战。