上一篇 

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 作为持续查询的驱动器,分批次不断地:

Spark Structured Streaming系列-数据输出详解_DataFrame

    1. 在每个 StreamExecution 的批次最开始,StreamExecution 会向 Source 询问当前 Source 的最新进度,即最新的 offset

    2. 这个 Offset 给到 StreamExecution 后会被 StreamExecution 持久化到自己的 WAL 里

    3. 由 Source 根据 StreamExecution 所要求的 start offset、end offset,提供在 (start, end] 区间范围内的数据

    4. StreamExecution 触发计算逻辑 logicalPlan 的优化与编译

    5. 把计算结果写出给 Sink

      1. 具体是由 StreamExecution 调用 Sink.addBatch(batchId: Long, data: DataFrame)

      2. 注意这时才会由 Sink 触发发生实际的取数据操作,以及计算过程

      3. 通常 Sink 直接可以直接把 data: DataFrame 的数据写出,并在完成后记录下 batchId: Long

      4. 在故障恢复时,分两种情况讨论:

        (i) 如果上次执行在本步 结束前即失效,那么本次执行里 sink 应该完整写出计算结果

(ii) 如果上次执行在本步 结束后才失效,那么本次执行里 sink 可以重新写出计算结果(覆盖上次结果),也可以跳过写出计算结果(因为上次执行已经完整写出过计算结果了)

  1. 在数据完整写出到 Sink 后,StreamExecution 通知 Source 可以废弃数据;然后把成功的批次 id 写入到 batchCommitLog

 

参考文档:

Github: org/apache/spark/sql/execution/streaming/Sink.scala

Github: org/apache/spark/streaming/DStreamGraph.scala