上一篇
Spark Structured Streaming系列-窗口管理详解
经过上面几篇操作,在配置完输入,并针对DataFrame或者DataSet做了一些查询、窗口等操作后,想要把结果保存起来。就可以使用DataSet.writeStream()方法,配置输出需要配置下面的内容:
-
format : 配置输出的格式
-
output mode:输出的格式
-
query name:查询的名称,类似tempview的名字
-
trigger interval:触发的间隔时间,如果前一个batch处理超时了,那么不会立即执行下一个batch,而是等下一个trigger时间在执行。
-
checkpoint location:为保证数据的可靠性,可以设置检查点保存输出的结果。
output Mode
详细的来看看这个输出模式的配置,它与普通的Spark的输出不同,只有三种类型:
-
complete,把所有的DataFrame的内容输出,这种模式只能在做agg聚合操作的时候使用,比如ds.group.count,之后可以使用它。整个Result Table输出,适合有聚合操作时使用。
-
append,普通的dataframe在做完map或者filter之后可以使用。这种模式会把新的batch的数据输出出来。将新数据append到Result Table中,并输出新数据。适合只涉及select, where, map, flatMap, filter, join等操作的查询。
-
update,把此次新增的数据输出,并更新整个dataframe。有点类似之前的streaming的state处理。更新Result Table,且只有更新部分才会输出。
输出的类型
Structed Streaming提供了几种输出的类型:Console、Memory、File和Foreach。
-
file,保存成csv或者parquet
noAggDF
.writeStream
.format("parquet")
.option("checkpointLocation", "path/to/checkpoint/dir")
.option("path", "path/to/destination/dir")
.start()
还有一些其他可以控制的参数:
maxFilesPerTrigger 每个batch最多的文件数,默认是没有限制。比如我设置了这个值为1,那么同时增加了5个文件,这5个文件会每个文件作为一波数据,更新streaming dataframe。
latestFirst 是否优先处理最新的文件,默认是false。如果设置为true,那么最近被更新的会优先处理。这种场景一般是在监听日志文件的时候使用。
fileNameOnly 是否只监听固定名称的文件。
-
console,直接输出到控制台。一般做测试的时候用这个比较方便。
noAggDF
.writeStream
.format("console")
.start()
-
memory,可以保存在内容,供后面的代码使用
aggDF
.writeStream
.queryName("aggregates")
.outputMode("complete")
.format("memory")
.start()
spark.sql("select * from aggregates").show()
-
foreach,参数是一个foreach的方法,用户可以实现这个方法实现一些自定义的功能。
writeStream.foreach(...).start()
这个foreach的功能很强大,下面是influxdb参考实例,需要注意一点所有foreach方法使用的对象需要Serializable,代码序列化后运行在各个节点。
public class InfluxdbForeachWriter extends
ForeachWriter<Row> implements Serializable {
private final static Logger logger =
LoggerFactory.getLogger(InfluxdbForeachWriter.class);
private InfluxDB influxDB;
private InfluxdbConfig influxdbConfig;
public InfluxdbForeachWriter(InfluxdbConfig influxdbConfig) {
this.influxdbConfig = influxdbConfig;
}
@Override
public boolean open(long l, long l1) {
influxDB = InfluxDBFactory.connect(influxdbConfig.getUrl(),
influxdbConfig.getUsername(), influxdbConfig.getPassword());
influxDB.setLogLevel(InfluxDB.LogLevel.NONE);
return true;
}
@Override
public void process(Row row) {
influxDB.write(influxdbConfig.getDatabase(),
influxdbConfig.getRetentionPolicy(),
InfluxDbUtil.builderPoint(influxdbConfig, row));
}
@Override
public void close(Throwable throwable) {
}
}
Sink 解析
Structured Streaming 非常显式地提出了输入(Source)、执行(StreamExecution)、输出(Sink)的 3 个组件,并且在每个组件显式地做到 fault-tolerant,由此得到整个 streaming 程序的 end-to-end exactly-once guarantees.
具体到源码上,Sink 是一个抽象的接口 trait Sink [1],只有一个方法:
trait Sink {
def addBatch(batchId: Long, data: DataFrame): Unit
}
这个仅有的 addBatch() 方法支持了 Structured Streaming 实现 end-to-end exactly-once 处理所一定需要的功能。我们将马上解析这个 addBatch() 方法。
相比而言,前作 Spark Streaming 并没有对输出进行特别的抽象,而只是在 DStreamGraph [2] 里将一些 dstreams 标记为了 output。当需要 exactly-once 特性时,程序员可以根据当前批次的时间标识,来 自行维护和判断 一个批次是否已经执行过。
进化到 Structured Streaming 后,显式地抽象出了 Sink,并提供了一些原生幂等的 Sink 实现:
-
已支持
-
HDFS-compatible file system,具体实现是 FileStreamSink extends Sink
-
Foreach sink,具体实现是 ForeachSink extends Sink
-
Kafka sink,具体实现是 KafkaSink extends Sink
-
-
预计后续很快会支持
-
RDBMS
-
Sink:方法与功能
在 Structured Streaming 里,由 StreamExecution 作为持续查询的驱动器,分批次不断地:
-
在每个 StreamExecution 的批次最开始,StreamExecution 会向 Source 询问当前 Source 的最新进度,即最新的 offset
-
这个 Offset 给到 StreamExecution 后会被 StreamExecution 持久化到自己的 WAL 里
-
由 Source 根据 StreamExecution 所要求的 start offset、end offset,提供在 (start, end] 区间范围内的数据
-
StreamExecution 触发计算逻辑 logicalPlan 的优化与编译
-
把计算结果写出给 Sink
-
具体是由 StreamExecution 调用 Sink.addBatch(batchId: Long, data: DataFrame)
-
注意这时才会由 Sink 触发发生实际的取数据操作,以及计算过程
-
通常 Sink 直接可以直接把 data: DataFrame 的数据写出,并在完成后记录下 batchId: Long
-
在故障恢复时,分两种情况讨论:
(i) 如果上次执行在本步 结束前即失效,那么本次执行里 sink 应该完整写出计算结果
-
-
(ii) 如果上次执行在本步 结束后才失效,那么本次执行里 sink 可以重新写出计算结果(覆盖上次结果),也可以跳过写出计算结果(因为上次执行已经完整写出过计算结果了)
-
在数据完整写出到 Sink 后,StreamExecution 通知 Source 可以废弃数据;然后把成功的批次 id 写入到 batchCommitLog
参考文档:
Github: org/apache/spark/sql/execution/streaming/Sink.scala
Github: org/apache/spark/streaming/DStreamGraph.scala