本文所有内容是基于spark 2.4.3版本官方文档
Structured Streaming provides fast, scalable, fault-tolerant, end-to-end exactly-once stream processing without the user having to reason about streaming
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()
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是非强类型的,意味着不会再编译时检查DataFrame的schema,只会在查询提交后运行时做检查。类似map,flatMap等需要在编译时确认类型的操作,需要将Streaming DataFrames转换成强类型的Streaming Datasets后再操作。//定义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 Dataframes可以使用distinct()和dropDuplicates(colNames)进行去重,区别在于distinct根据每一条数据进行完整内容的比对和去重,而dropDuplicates可以根据指定的字段进行去重。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()
监控
待补充