本文所有内容是基于spark 2.4.3版本官方文档
Structured Streaming 是Spark流处理引擎在2.*版本后加入的模块, 是基于微批(Micro-Batch)的流处理,其最低延时至少100ms。在2.3引入了Continuous Processing实验性流处理模式,可达到端到端最低1毫秒的延时。
val spark = SparkSession
.builder
.appName("StructuredNetworkWordCount")
.getOrCreate()
// Create DataFrame representing the stream of input lines from connection to localhost:9999
val lines = spark.readStream
.format("socket")
.option("host", "localhost")
.option("port", 9999)
.load()
// Split the lines into words
val words = lines.as[String].flatMap(_.split(" "))
// Generate running word count
val wordCounts = words.groupBy("value").count()
// Start running the query that prints the running counts to the console
val query = wordCounts.writeStream
.outputMode("complete")
.format("console")
.start()
// prevent the process from exiting while the query is active.
query.awaitTermination()
一、基本概念
- Streaming数据流转
输入的数据流可以看作一个输入表,到达的每一条数据都可以当作在输入表中新增一行数据。对输入表的查询会产生一个结果表,每一个trigger interval,输入表中新增的数据都会更新结果表,同时会将改变的结果行写到外部存储(external sink),在这个过程中,会保存需要用于更新结果表最少的中间状态数据。 - 处理Event-time和延迟数据
Event-time时嵌入在数据内的时间,对于多数应用,需要处理的时数据产生的时间而非spark接收数据的时间。event-time可以看作数据行中的一列,这样可以对event-time列进行基于窗口的分组聚合操作 -- 每一个时间窗口都是一个分组,每个数据行都属于多个窗口/分组,这样基于窗口事件事件聚合查询可以在静态数据集和数据流上具有一致的定义。
当出现延迟数据时,Spark拥有完全的权控制对先前聚合结果的进行更新,也能清除过期聚合结果来限制中间状态数据的量。spark2.1之后,支持使用wartermarking来制定数据延迟的阈值,处理引擎也可以据此清除过期中间状态。 - 容错语义
实现端到端exactly-once语义时Structured Streaming设计的重要目标,输入源、输出端和执行引擎在设计上都能可靠地追踪流处理的进程,进而通过重启或重新处理解决各种失败情况
二、主要特性
- streaming DataFrames/Datasets的创建
Spark Structured Streaming内置的数据源有:File source、Kafka source、Socket source、Rate source,其中Socket source不提供端到端容错保证,因为该数据源无法数据重放
示例中的Streaming DataFrames是非强类型的,意味着不会再编译时检查DataFrame的schema,只会在查询提交后运行时做检查。类似map,flatMap等需要在编译时确认类型的操作,需要将Streaming DataFrames转换成强类型的Streaming Datasets后再操作。val spark: SparkSession = ... // Read text from socket val socketDF = spark .readStream .format("socket") .option("host", "localhost") .option("port", 9999) .load() socketDF.isStreaming // Returns True for DataFrames that have streaming sources socketDF.printSchema // Read all the csv files written atomically in a directory val userSchema = new StructType().add("name", "string").add("age", "integer") val csvDF = spark .readStream .option("sep", ";") .schema(userSchema) // Specify schema of the csv files .csv("/path/to/directory") // Equivalent to format("csv").load("/path/to/directory")
- streaming DataFrames/Datasets的schema推断和分区发现
基于File source的Structured Streaming默认不会自行推断schema,而是需要用户明确提供。该限制用来保证即使再出现故障的情况下也能保证查询使用schema的一致性。在有ad-hoc需要时,可以通过设置spark.sql.streaming.schemaInference=true开启schema推断
分区发现是指如果schema中存在key字段,当出现/key=value/子目录时,可以自动地将目录放到分区列表中。组成分区schema的目录在开始查询时必须存在并且保持静态(1.key存在 2.key不可变 3.value可变)
- 基本操作-选取、映射、聚合
streaming Dataframes可以使用distinct()和dropDuplicates(colNames)进行去重,区别在于distinct根据每一条数据进行完整内容的比对和去重,而dropDuplicates可以根据指定的字段进行去重。//定义schema case class DeviceData(device: String, deviceType: String, signal: Double, time: DateTime) val df: DataFrame = ... // streaming DataFrame with IOT device data with schema { device: string, deviceType: string, signal: double, time: string } val ds: Dataset[DeviceData] = df.as[DeviceData] // streaming Dataset with IOT device data // Select the devices which have signal more than 10 df.select("device").where("signal > 10") // using untyped APIs ds.filter(_.signal > 10).map(_.device) // using typed APIs // Running count of the number of updates for each device type df.groupBy("deviceType").count() // using untyped API // Running average signal for each device type import org.apache.spark.sql.expressions.scalalang.typed ds.groupByKey(_.deviceType).agg(typed.avg(_.signal)) // using typed API //create temp view df.createOrReplaceTempView("updates") spark.sql("select count(*) from updates") // returns another streaming DF
streaming Datasets支持大多数通用DataSets操作,其不支持的操作有:
· streaming Datasets不支持多留聚合
· streaming Datasets不支持Limit和take(n)操作
· streaming Datasets不支持Distinct操作
· streaming Datasets不支持排序,排序必须跟在聚合操作后并使用complete输出模式
· streaming Datasets少数几种outer join
· streaming Datasets不能直接count(),用ds.groupBy().count()可以返回一个包含运行时计数的streaming Datasets
· streaming Datasets不支持foreach(),需用ds.writeStream.foreach(...) 代替
· streaming Datasets不支持show(), 需用console sink代替
- Window
基于事件时间的滑动窗口的聚合与分组聚合类似,区别是滑动窗口只作用在落在窗口内的行数据,分组聚合作用在所有行import spark.implicits._ val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String } // Group the data by window and word and compute the count of each group val windowedCounts = words.groupBy( window($"timestamp", "10 minutes", "5 minutes"), $"word" ).count()
WarterMarking
数据流是基于时间的窗口操作,但每个窗口的数据不一定会及时的在窗口时间内到来,因此需要窗口数据作为中间状态,当属于该窗口的数据到来时更新状态。但系统能保存的中间状态时有限的,不可能无限地等待数据,因此spark2.1后引入了WarterMarking让系统可以知道在什么时候可以触发窗口计算并丢弃窗口的中间状态。Watermark是一种平衡处理延时和完整性的灵活机制。
在系统每次触发数据落地(trigger)时,系统会基于( WaterMark= 当前窗口最大可见event time - 允许延迟时间)作为当前窗口可处理数据event_time最小的时间,早于这个时间的窗口状态会被认为过期并清除,早于这个时间的数据可能会也可能不会被丢弃。
WarterMark触发Window计算有2个条件:(a).watermark时间>=window最大可见event time (b).窗口内有数据import spark.implicits._ val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String } // Group the data by window and word and compute the count of each group val windowedCounts = words .withWatermark("timestamp", "10 minutes") .groupBy( window($"timestamp", "10 minutes", "5 minutes"), $"word") .count()
-
Structured Streaming的Join操作
Structured Streaming支持streaming DataSets/DataFrames 连接 static Structured Streaming或者streaming DataSets/DataFrames。
stream-static 连接是无状态的,因此不需要管理状态。stream-stream 连接的主要挑战是Stream是不完整的数据集,很难匹配join的输入,每个流输入的一行数据都可能要匹配另一个流还没出现的数据。因此必须为把所有流过去的数据缓存为流状态,这样就可以把过去的输入和未来的输入相应地匹配起来生成连接结果。同样地,使用wartermarking来处理延迟、乱序的数据以及限制中间状态的量。在join操作中需要有下面的设定
1.定义两个输入流控制延迟的WaterMark,告知引擎每个流数据延迟处理的范围
2.定义对两个输入流事件时间的限制,这样引擎可以知道两个流新老数据连接的最大时间差import org.apache.spark.sql.functions.expr val impressions = spark.readStream. ... val clicks = spark.readStream. ... // Apply watermarks on event-time columns val impressionsWithWatermark = impressions.withWatermark("impressionTime", "2 hours") val clicksWithWatermark = clicks.withWatermark("clickTime", "3 hours") // Join with event-time constraints impressionsWithWatermark.join( clicksWithWatermark, expr(""" clickAdId = impressionAdId AND clickTime >= impressionTime AND clickTime <= impressionTime + interval 1 hour """) )
-
Trigger操作
trigger操作用于设置流数据生成微批和处理微批的时间间隔,当前支持的不同类型trigger如下:
1.不指定:如果没有显式指定,也就是采用默认的微批处理模式,即只要前一个微批处理结束就会立即处理下一个微批(在处理期间积累的数据)。
2.固定时间间隔:按固定时间间隔生成微批。如果前一个微批在间隔内完成,那下一个微批要等到间隔结束才生成并处理;如果前一个微批超过间隔完成,那么下一个微批会在前一个结束后立即生成并处理;如果没有可用的下一个微批的时不做任何处理。
3.一次性:所有的数据当作一个微批一次性处理。此类型适用于周期性的启停一个作业进行处理的场景,从数据层面类似定时执行的etl作业,其优势在于自行管理每次执行的数据,而etl需要用户指定;执行作业具有原子性,而etl一旦失败需要清理已写入数据;通过合理配置watermark可以通过dropDuplicates实现跨多个执行作业去重,etl无法实现;如果接受更高的延时,周期按小时或按天作业相比全天运行的流作业节省更多资源和成本。
4.连续性(Continuous): 持续处理,更低延时。属于实验性功能,不多做介绍import org.apache.spark.sql.streaming.Trigger // Default trigger (runs micro-batch as soon as it can) df.writeStream .format("console") .start() // ProcessingTime trigger with two-seconds micro-batch interval df.writeStream .format("console") .trigger(Trigger.ProcessingTime("2 seconds")) .start() // One-time trigger df.writeStream .format("console") .trigger(Trigger.Once()) .start() // Continuous trigger with one-second checkpointing interval df.writeStream .format("console") .trigger(Trigger.Continuous("1 second")) .start()
-
实现任意的状态操作
前面提到的状态主要是指对流进行分组聚合产生的状态,很多情况下我们可能需要保存更复杂的状态,比如我们可能希望在数据流中保存会话状态。Spark2.2之后,可通过使用mapGroupsWithState或者更强大的flatMapGroupsWithState操作来实现。该操作可以在每次trigger时,将自定义函数作用在每个分组上(groupByKey)来生成、更新、清除任意自定义的状态(只会作用在当前trigger中出现的分组)。想进一步了解可参考GroupState(mapGroupsWithState/flatMapGroupsWithState) -
输出端和输出模式
有以下几种内置的输出端:File sink, kafka sink, Foreach sink, Console sink(调试用), Memory sink(调试用)//file sink writeStream .format("parquet") // can be "orc", "json", "csv", etc. .option("path", "path/to/destination/dir") .start() //kafka sink writeStream .format("kafka") .option("kafka.bootstrap.servers", "host1:port1,host2:port2") .option("topic", "updates") .start() //foreach sink writeStream .foreach(...) .start() //write sink writeStream .format("console") .start() //memory sink writeStream .format("memory") .queryName("tableName") .start()
到输出端的输出模式有一下三种模式:
Complet Mode - 仅支持aggregation查询,每次trigger都会将更新后全量的结果表写入外部存储
Append Mode - 默认模式,根据watermark延迟输出结果表新增的行数据到外部存储,并清理过期状态
Update Mode - 每次trigger都会将结果表中更新的行会写入外部存储(2.1.1版本后可用) -
基于检查点的故障恢复
为了防止系统故障或者意外关闭,spark使用checkpoint和WAL机制恢复故障前的查询状态继续运行,实现exactly-once语义。从checkpoint进行故障恢复时,下面的变更时不允许的:
1.变更输入源的数量或类型
2.变更输入源订阅的topics/files
3.变更输出端的文件目录或topic
4.变更映射输出结果的schema是否允许要看输出端是否允许这种变更
checkpoint持久化的时候会保存Scala/Java/Python对象(如果有)序列化后的数据,如果应用升级变更了对象数据结构,从checkpoint中恢复状态数据可能会导致错误。这种情况在重启应用时要么删除先前的checkpoint目录,要么更改目录。aggDF .writeStream .outputMode("complete") .option("checkpointLocation", "path/to/HDFS/dir") .format("memory") .start()
-
监控
待补充