[酷玩 Spark] Structured Streaming 源码解析系列 ,返回目录请 猛戳这里
「腾讯·广点通」技术团队荣誉出品
本文内容适用范围:
* 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0)
* 2017.05.02 update, Spark 2.1 全系列 √ (已发布:2.1.0, 2.1.1)
本文目录
一、引言:Spark 2 时代!
二、从 Structured Data 到 Structured Streaming
三、Structured Streaming:无限增长的表格
四、StreamExecution:持续查询的运转引擎
1. StreamExecution 的初始状态
2. StreamExecution 的持续查询
3. StreamExecution 的持续查询(增量)
4. 故障恢复
5. Sources 与 Sinks
6. 小结:end-to-end exactly-once guarantees
五、全文总结
六、扩展阅读
参考资料
Spark 1.x 时代里,以 SparkContext(及 RDD API)为基础,在 structured data 场景衍生出了 SQLContext, HiveContext,在 streaming 场景衍生出了 StreamingContext,很是琳琅满目。
Spark 2.x 则咔咔咔精简到只保留一个 SparkSession 作为主程序入口,以 Dataset/DataFrame 为主要的用户 API,同时满足 structured data, streaming data, machine learning, graph 等应用场景,大大减少使用者需要学习的内容,爽爽地又重新实现了一把当年的 "one stack to rule them all" 的理想。
我们这里简单回顾下 Spark 2.x 的 Dataset/DataFrame 与 Spark 1.x 的 RDD 的不同:
RDD[Person]
,那么一行就是一个 Person
,存在内存里也是把 Person
作为一个整体(序列化前的 java object,或序列化后的 bytes)。Person
的 Dataset 或 DataFrame,是二维行+列的数据集,比如一行一个 Person
,有 name:String
, age:Int
, height:Double
三列;在内存里的物理结构,也会显式区分列边界。
Dataset
或 DataFrame
在 API 层面的差别时,我们统一写作 Dataset/DataFrame
[小节注] 其实 Spark 1.x 就有了 Dataset/DataFrame 的概念,但还仅是 SparkSQL 模块的主要 API ;到了 2.0 时则 Dataset/DataFrame 不局限在 SparkSQL、而成为 Spark 全局的主要 API。
使用 Dataset/DataFrame 的行列数据表格来表达 structured data,既容易理解,又具有广泛的适用性:
class Person { String name; int age; double height}
的多个对象可以方便地转化为 Dataset/DataFrame
{name: "Alice", age: 20, height: 1.68}, {name: "Bob", age: 25, height: 1.76}
可以方便地转化为Dataset/DataFrame
Dataset/DataFrame
Spark 2.0 更进一步,使用 Dataset/Dataframe 的行列数据表格来扩展表达 streaming data —— 所以便横空出世了 Structured Streaming 、《Structured Streaming 源码解析系列》—— 与静态的 structured data 不同,动态的 streaming data 的行列数据表格是一直无限增长的(因为 streaming data 在源源不断地产生)!
基于“无限增长的表格”的编程模型 [1],我们来写一个 streaming 的 word count:
对应的 Structured Streaming 代码片段:
val spark = SparkSession.builder().master("...").getOrCreate() // 创建一个 SparkSession 程序入口
val lines = spark.readStream.textFile("some_dir") // 将 some_dir 里的内容创建为 Dataset/DataFrame;即 input table
val words = lines.flatMap(_.split(" "))
val wordCounts = words.groupBy("value").count() // 对 "value" 列做 count,得到多行二列的 Dataset/DataFrame;即 result table
val query = wordCounts.writeStream // 打算写出 wordCounts 这个 Dataset/DataFrame
.outputMode("complete") // 打算写出 wordCounts 的全量数据
.format("console") // 打算写出到控制台
.start() // 新起一个线程开始真正不停写出
query.awaitTermination() // 当前用户主线程挂住,等待新起来的写出线程结束
这里需要说明几点:
现在我们将目光聚焦到 continuous query 的驱动引擎(即整个 Structured Streaming 的驱动引擎) StreamExecution 上来。
我们前文刚解析过,先定义好 Dataset/DataFrame 的产生、变换和写出,再启动 StreamExection 去持续查询。这些 Dataset/DataFrame 的产生、变换和写出的信息就对应保存在 StreamExecution 非常重要的 3 个成员变量中:
sources
: streaming data 的产生端(比如 kafka 等)logicalPlan
: DataFrame/Dataset 的一系列变换(即计算逻辑)sink
: 最终结果写出的接收端(比如 file system 等)StreamExection 另外的重要成员变量是:
currentBatchId
: 当前执行的 idbatchCommitLog
: 已经成功处理过的批次有哪些offsetLog
, availableOffsets
, committedOffsets
: 当前执行需要处理的 source data 的 meta 信息offsetSeqMetadata
: 当前执行的 watermark 信息(event time 相关,本文暂不涉及、另文解析)等我们将 Source, Sink, StreamExecution 及其重要成员变量标识在下图,接下来将逐个详细解析。
一次执行的过程如上图;这里有 6 个关键步骤:
Structured Streaming 在编程模型上暴露给用户的是,每次持续查询看做面对全量数据(而不仅仅是本次执行信收到的数据),所以每次执行的结果是针对全量数据进行计算的结果。
但是在实际执行过程中,由于全量数据会越攒越多,那么每次对全量数据进行计算的代价和消耗会越来越大。
Structured Streaming 的做法是:
所以 Structured Streaming 在编程模型上暴露给用户的是,每次持续查询看做面对全量数据,但在具体实现上转换为增量的持续查询。
通过前面小节的解析,我们知道存储 source offsets 的 offsetLog,和存储计算状态的 StateStore,是全局高可用的。仍然采用前面的示意图,offsetLog 和 StateStore 被特殊标识为紫色,代表高可用。
由于 exectutor 节点的故障可由 Spark 框架本身很好的 handle,不引起可用性问题,我们本节的故障恢复只讨论 driver 故障恢复。
如果在某个执行过程中发生 driver 故障,那么重新起来的 StreamExecution:
这样即可保证每次执行的计算结果,在 sink 这个层面,是 不重不丢 的 —— 即使中间发生过 1 次或以上的失效和恢复。
可以看到,Structured Streaming 层面的 Source,需能 根据 offsets 重放数据 [2]。所以:
Sources | 是否可重放 | 原生内置支持 | 注解 |
---|---|---|---|
HDFS-compatible file system | 已支持 | 包括但不限于 text, json, csv, parquet, orc, ... | |
Kafka | 已支持 | Kafka 0.10.0+ | |
RateStream | 已支持 | 以一定速率产生数据 | |
RDBMS | (待支持) | 预计后续很快会支持 | |
Socket | 已支持 | 主要用途是在技术会议/讲座上做 demo | |
Receiver-based | 不会支持 | 就让这些前浪被拍在沙滩上吧 |
也可以看到,Structured Streaming 层面的 Sink,需能 幂等式写入数据 [3]。所以:
Sinks | 是否幂等写入 | 原生内置支持 | 注解 |
---|---|---|---|
HDFS-compatible file system | 已支持 | 包括但不限于 text, json, csv, parquet, orc, ... | |
ForeachSink (自定操作幂等) | 已支持 | 可定制度非常高的 sink | |
RDBMS | (待支持) | 预计后续很快会支持 | |
Kafka | 已支持 | Kafka 目前不支持幂等写入,所以可能会有重复写入 (但推荐接着 Kafka 使用 streaming de-duplication 来去重) |
|
ForeachSink (自定操作不幂等) | 已支持 | 不推荐使用不幂等的自定操作 | |
Console | 已支持 | 主要用途是在技术会议/讲座上做 demo |
所以在 Structured Streaming 里,我们总结下面的关系[4]:
这里的 end-to-end 指的是,如果 source 选用类似 Kafka, HDFS 等,sink 选用类似 HDFS, MySQL 等,那么 Structured Streaming 将自动保证在 sink 里的计算结果是 exactly-once 的 —— Structured Streaming 终于把过去需要使用者去维护的 sink 去重逻辑接盘过去了!:-)
自 Spark 2.0 开始,处理 structured data 的 Dateset/DataFrame 被扩展为同时处理 streaming data,诞生了 Structured Streaming。
Structured Streaming 以“无限扩展的表格”为编程模型,在 StreamExecution 实际执行中增量执行,并满足 end-to-end exactly-once guarantee.
在 Spark 2.0 时代,Dataset/DataFrame 成为主要的用户 API,同时满足 structured data, streaming data, machine learning, graph 等应用场景,大大减少使用者需要学习的内容,爽爽地又重新实现了一把当年的 "one stack to rule them all" 的理想。
谨以此《Structured Streaming 源码解析系列》和以往的《Spark Streaming 源码解析系列》,向“把大数据变得更简单 (make big data simple) ”的创新者们,表达感谢和敬意。