readAll()`
325 | - 由 `FileBasedWriteAheadLogReader` 具体实现
326 |
327 | ## (3) 重放
328 |
329 | 如果上游支持重放,比如 Apache Kafka,那么就可以选择不用热备或者冷备来另外存储数据了,而是在失效时换一个 executor 进行数据重放即可。
330 |
331 | 具体的,[Spark Streaming 从 Kafka 读取方式有两种](http://spark.apache.org/docs/latest/streaming-kafka-integration.html):
332 |
333 | - 基于 `Receiver` 的
334 | - 这种是将 Kafka Consumer 的偏移管理交给 Kafka —— 将存在 ZooKeeper 里,失效后由 Kafka 去基于 offset 进行重放
335 | - 这样可能的问题是,Kafka 将同一个 offset 的数据,重放给两个 batch 实例 —— 从而只能保证 at least once 的语义
336 | - Direct 方式,不基于 `Receiver`
337 | - 由 Spark Streaming 直接管理 offset —— 可以给定 offset 范围,直接去 Kafka 的硬盘上读数据,使用 Spark Streaming 自身的均衡来代替 Kafka 做的均衡
338 | - 这样可以保证,每个 offset 范围属于且只属于一个 batch,从而保证 exactly-once
339 |
340 | 这里我们以 Direct 方式为例,详解一下 Spark Streaming 在源头数据实效后,是如果从上游重放数据的。
341 |
342 | 这里的实现分为两个层面:
343 |
344 | - `DirectKafkaInputDStream`:负责侦测最新 offset,并将 offset 分配至唯一个 batch
345 | - 会在每次 batch 生成时,依靠 `latestLeaderOffsets()` 方法去侦测最新的 offset
346 | - 然后与上一个 batch 侦测到的 offset 相减,就能得到一个 offset 的范围 `offsetRange`
347 | - 把这个 offset 范围内的数据,唯一分配到本 batch 来处理
348 | - `KafkaRDD`:负责去读指定 offset 范围内的数据,并基于此数据进行计算
349 | - 会生成一个 Kafka 的 `SimpleConsumer` —— `SimpleConsumer` 是 Kafka 最底层、直接对着 Kafka 硬盘上的文件读数据的类
350 | - 如果 `Task` 失败,导致任务重新下发,那么 offset 范围仍然维持不变,将直接重新生成一个 Kafka 的 `SimpleConsumer` 去读数据
351 |
352 | 所以看 Direct 的方式,归根结底是由 Spark Streaming 框架来负责整个 offset 的侦测、batch 分配、实际读取数据;并且这些分 batch 的信息都是 checkpoint 到可靠存储(一般是 HDFS)了。这就没有用到 Kafka 使用 ZooKeeper 来均衡 consumer 和记录 offset 的功能,而是把 Kafka 直接当成一个底层的文件系统来使用了。
353 |
354 | 当然,我们讲上游重放并不只局限于 Kafka,而是说凡是支持消息重放的上游都可以 —— 比如,HDFS 也可以看做一个支持重放的可靠上游 —— FileInputDStream 就是利用重放的方式,保证了 executor 失效后的源头数据的可读性。
355 |
356 | ## (4) 忽略
357 |
358 | 最后,如果应用的实时性需求大于准确性,那么一块数据丢失后我们也可以选择忽略、不恢复失效的源头数据。
359 |
360 | 假设我们有 r1, r2, r3 这三个 `Receiver`,而且每 5 秒产生一个 Block,每 15 秒产生一个 batch。那么,每个 batch 有 `15 s ÷ 5 block/s/receiver × 3 receiver = 9 block`。现在假设 r1 失效,随之也丢失了 3 个 block。
361 |
362 | 那么上层应用如何进行忽略?有两种粒度的做法。
363 |
364 | ### 粗粒度忽略
365 |
366 | 粗粒度的做法是,如果计算任务试图读取丢失的源头数据时出错,会导致部分 task 计算失败,会进一步导致整个 batch 的 job 失败,最终在 driver 端以 `SparkException` 的形式报出来 —— 此时我们 catch 住这个 `SparkException`,就能够屏蔽这个 batch 的 job 失败了。
367 |
368 | 粗粒度的这个做法实现起来非常简单,问题是会忽略掉整个 batch 的计算结果。虽然我们还有 6 个 block 是好的,但所有 9 个的数据都会被忽略。
369 |
370 | ### 细粒度忽略
371 |
372 | 细粒度的做法是,只将忽略部分局限在丢失的 3 个 block 上,其它部分 6 部分继续保留。目前原生的 Spark Streaming 还不能完全做到,但我们对 Spark Streaming 稍作修改,就可以做到了。
373 |
374 | 细粒度基本思路是,在一个计算的 task 发现作为源数据的 block 失效后,不是直接报错,而是另外生成一个空集合作为“修正”了的源头数据,然后继续 task 的计算,并将成功。
375 |
376 | 如此一来,仅局限在发生数据丢失的 3 个块数据才会进行“忽略”的过程,6 个好的块数据将正常进行计算。最后整个 job 是成功的。
377 |
378 | 当然这里对 Spark Streaming 本身的改动,还需要考虑一些细节,比如只在 Spark Streaming 里生效、不要影响到 Spark Core、SparkSQL,再比如 task 通常都是会失效重试的,我们希望前几次现场重试,只在最后一次重试仍不成功的时候再进行忽略。
379 |
380 | 我们把修改的代码,以及使用方法放在这里了,请随用随取。
381 |
382 | ## 总结
383 |
384 | 我们上面分四个小节介绍了 Spark Streaming 对源头数据的高可用的保障方式,我们用一个表格来总结一下:
385 |
386 |
387 |
388 | |
389 | 图示 |
390 | 优点 |
391 | 缺点 |
392 |
393 |
394 | (1) 热备 |
395 |  |
396 | 无 recover time |
397 | 需要占用双倍资源 |
398 |
399 |
400 | (2) 冷备 |
401 |  |
402 | 十分可靠 |
403 | 存在 recover time |
404 |
405 |
406 | (3) 重放 |
407 |  |
408 | 不占用额外资源 |
409 | 存在 recover time |
410 |
411 |
412 | (4) 忽略 |
413 |  |
414 | 无 recover time |
415 | 准确性有损失 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/proflin/CoolplaySpark/issues/11),返回目录请 [猛戳这里](readme.md))
424 |
--------------------------------------------------------------------------------
/Spark Streaming 源码解析系列/4.2 Driver 端长时容错详解.md:
--------------------------------------------------------------------------------
1 | ## Driver 端长时容错详解
2 |
3 | ***[酷玩 Spark] Spark Streaming 源码解析系列*** ,返回目录请 [猛戳这里](readme.md)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本系列内容适用范围:
9 |
10 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
11 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
12 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
13 | ```
14 |
15 |
16 |
17 | 阅读本文前,请一定先阅读 [Spark Streaming 实现思路与模块概述](0.1 Spark Streaming 实现思路与模块概述.md) 一文,其中概述了 Spark Streaming 的 4 大模块的基本作用,有了全局概念后再看本文对 `模块 4:长时容错` 细节的解释。
18 |
19 | ## 引言
20 |
21 | 之前的详解我们详解了完成 Spark Streamimg 基于 Spark Core 所新增功能的 3 个模块,接下来我们看一看第 4 个模块将如何保障 Spark Streaming 的长时运行 —— 也就是,如何与前 3 个模块结合,保障前 3 个模块的长时运行。
22 |
23 | 通过前 3 个模块的关键类的分析,我们可以知道,保障模块 1 和 2 需要在 driver 端完成,保障模块 3 需要在 executor 端和 driver 端完成。
24 |
25 | 
26 |
27 | 本文我们详解 driver 端的保障。具体的,包括两部分:
28 |
29 | - (1) ReceivedBlockTracker 容错
30 | - 采用 WAL 冷备方式
31 | - (2) DStream, JobGenerator 容错
32 | - 采用 Checkpoint 冷备方式
33 |
34 | ## (1) ReceivedBlockTracker 容错详解
35 |
36 | 前面我们讲过,块数据的 meta 信息上报到 `ReceiverTracker`,然后交给 `ReceivedBlockTracker` 做具体的管理。`ReceivedBlockTracker` 也采用 WAL 冷备方式进行备份,在 driver 失效后,由新的 `ReceivedBlockTracker` 读取 WAL 并恢复 block 的 meta 信息。
37 |
38 | `WriteAheadLog` 的方式在单机 RDBMS、NoSQL/NewSQL 中都有广泛应用,前者比如记录 transaction log 时,后者比如 HBase 插入数据可以先写到 HLog 里。
39 |
40 | `WriteAheadLog` 的特点是顺序写入,所以在做数据备份时效率较高,但在需要恢复数据时又需要顺序读取,所以需要一定 recovery time。
41 |
42 | `WriteAheadLog` 及其基于 rolling file 的实现 `FileBasedWriteAheadLog` 我们在 [Executor 端长时容错详解](4.1 Executor 端长时容错详解.md) 详解过了,下面我们主要看 `ReceivedBlockTracker` 如何使用 WAL。
43 |
44 | `ReceivedBlockTracker` 里有一个 `writeToLog()` 方法,会将具体的 log 信息写到 rolling log 里。我们看代码有哪些地方用到了 `writeToLog()`:
45 |
46 | ```scala
47 |
48 | def addBlock(receivedBlockInfo: ReceivedBlockInfo): Boolean = synchronized {
49 | ...
50 | // 【在收到了 Receiver 报上来的 meta 信息后,先通过 writeToLog() 写到 WAL】
51 | writeToLog(BlockAdditionEvent(receivedBlockInfo))
52 | // 【再将 meta 信息索引起来】
53 | getReceivedBlockQueue(receivedBlockInfo.streamId) += receivedBlockInfo
54 | ...
55 | }
56 |
57 | def allocateBlocksToBatch(batchTime: Time): Unit = synchronized {
58 | ...
59 | // 【在收到了 JobGenerator 的为最新的 batch 划分 meta 信息的要求后,先通过 writeToLog() 写到 WAL】
60 | writeToLog(BatchAllocationEvent(batchTime, allocatedBlocks))
61 | // 【再将 meta 信息划分到最新的 batch 里】
62 | timeToAllocatedBlocks(batchTime) = allocatedBlocks
63 | ...
64 | }
65 |
66 | def cleanupOldBatches(cleanupThreshTime: Time, waitForCompletion: Boolean): Unit = synchronized {
67 | ...
68 | // 【在收到了 JobGenerator 的清除过时的 meta 信息要求后,先通过 writeToLog() 写到 WAL】
69 | writeToLog(BatchCleanupEvent(timesToCleanup))
70 | // 【再将过时的 meta 信息清理掉】
71 | timeToAllocatedBlocks --= timesToCleanup
72 | // 【再将 WAL 里过时的 meta 信息对应的 log 清理掉】
73 | writeAheadLogOption.foreach(_.clean(cleanupThreshTime.milliseconds, waitForCompletion))
74 | }
75 | ```
76 |
77 | 通过上面的代码可以看到,有 3 种消息 —— `BlockAdditionEvent`, `BatchAllocationEvent`, `BatchCleanupEvent` —— 会被保存到 WAL 里。
78 |
79 | 也就是,如果我们从 WAL 中恢复,能够拿到这 3 种消息,然后从头开始重做这些 log,就能重新构建出 `ReceivedBlockTracker` 的状态成员:
80 |
81 | 
82 |
83 | ## (2) DStream, JobGenerator 容错详解
84 |
85 | 另外,需要定时对 `DStreamGraph` 和 `JobScheduler` 做 Checkpoint,来记录整个 `DStreamGraph` 的变化、和每个 batch 的 job 的完成情况。
86 |
87 | 注意到这里采用的是完整 checkpoint 的方式,和之前的 WAL 的方式都不一样。Checkpoint 通常也是落地到可靠存储如 HDFS。Checkpoint 发起的间隔默认的是和 batchDuration 一致;即每次 batch 发起、提交了需要运行的 job 后就做 Checkpoint,另外在 job 完成了更新任务状态的时候再次做一下 Checkpoint。
88 |
89 | 具体的,`JobGenerator.doCheckpoint()` 实现是,`new` 一个当前状态的 `Checkpoint`,然后通过 `CheckpointWriter` 写出去:
90 |
91 | ```scala
92 | // 来自 JobGenerator
93 |
94 | private def doCheckpoint(time: Time, clearCheckpointDataLater: Boolean) {
95 | if (shouldCheckpoint && (time - graph.zeroTime).isMultipleOf(ssc.checkpointDuration)) {
96 | logInfo("Checkpointing graph for time " + time)
97 | ssc.graph.updateCheckpointData(time)
98 | // 【new 一个当前状态的 Checkpoint,然后通过 CheckpointWriter 写出去】
99 | checkpointWriter.write(new Checkpoint(ssc, time), clearCheckpointDataLater)
100 | }
101 | }
102 | ```
103 |
104 | 然后我们看 `JobGenerator.doCheckpoint()` 在哪里被调用:
105 |
106 | ```scala
107 | // 来自 JobGenerator
108 |
109 | private def processEvent(event: JobGeneratorEvent) {
110 | logDebug("Got event " + event)
111 | event match {
112 | ...
113 | // 【是异步地收到 DoCheckpoint 消息后,在一个线程池里执行 doCheckpoint() 方法】
114 | case DoCheckpoint(time, clearCheckpointDataLater) =>
115 | doCheckpoint(time, clearCheckpointDataLater)
116 | ...
117 | }
118 | }
119 | ```
120 |
121 | 所以进一步看,到底哪里发送过 `DoCheckpoint` 消息:
122 |
123 | ```scala
124 | // 来自 JobGenerator
125 |
126 | private def generateJobs(time: Time) {
127 | SparkEnv.set(ssc.env)
128 | Try {
129 | jobScheduler.receiverTracker.allocateBlocksToBatch(time) // 【步骤 (1)】
130 | graph.generateJobs(time) // 【步骤 (2)】
131 | } match {
132 | case Success(jobs) =>
133 | val streamIdToInputInfos = jobScheduler.inputInfoTracker.getInfo(time) // 【步骤 (3)】
134 | jobScheduler.submitJobSet(JobSet(time, jobs, streamIdToInputInfos)) // 【步骤 (4)】
135 | case Failure(e) =>
136 | jobScheduler.reportError("Error generating jobs for time " + time, e)
137 | }
138 | eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = false)) // 【步骤 (5)】
139 | }
140 |
141 | // 来自 JobScheduler
142 | private def clearMetadata(time: Time) {
143 | ssc.graph.clearMetadata(time)
144 |
145 | if (shouldCheckpoint) {
146 | // 【一个 batch 做完,需要 clean 元数据时】
147 | eventLoop.post(DoCheckpoint(time, clearCheckpointDataLater = true))
148 | }
149 | ...
150 | }
151 | ```
152 |
153 | 原来是两处会发送 `DoCheckpoint` 消息:
154 |
155 | - 第 1 处就是经典的 `JobGenerator.generateJob()` 的第 (5) 步
156 | - 是在第 (4) 步提交了 `JobSet` 给 `JobScheduler` 异步执行后,就马上执行第 (5) 步来发送 `DoCheckpoint` 消息(如下图)
157 | - 
158 | - 第 2 处是 `JobScheduler` 成功执行完了提交过来的 `JobSet` 后,就可以清除此 batch 的相关信息了
159 | - 这时是先 clear 各种信息
160 | - 然后发送 `DoCheckpoint` 消息,触发 `doCheckpoint()`,就会记录下来我们已经做完了一个 batch
161 |
162 | 解决了什么时候 `doCheckpoint()`,现在唯一的问题就是 `Checkpoint` 都会包含什么内容了。
163 |
164 | ## Checkpoint 详解
165 |
166 | 我们看看 `Checkpoint` 的具体内容,整个列表如下:
167 |
168 | ```scala
169 | 来自 Checkpoint
170 |
171 | val checkpointTime: Time
172 | val master: String = ssc.sc.master
173 | val framework: String = ssc.sc.appName
174 | val jars: Seq[String] = ssc.sc.jars
175 | val graph: DStreamGraph = ssc.graph // 【重要】
176 | val checkpointDir: String = ssc.checkpointDir
177 | val checkpointDuration: Duration = ssc.checkpointDuration
178 | val pendingTimes: Array[Time] = ssc.scheduler.getPendingTimes().toArray // 【重要】
179 | val delaySeconds: Int = MetadataCleaner.getDelaySeconds(ssc.conf)
180 | val sparkConfPairs: Array[(String, String)] = ssc.conf.getAll
181 | ```
182 |
183 |
184 |
185 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/proflin/CoolplaySpark/issues/12),返回目录请 [猛戳这里](readme.md))
186 |
--------------------------------------------------------------------------------
/Spark Streaming 源码解析系列/Q&A 什么是 end-to-end exactly-once.md:
--------------------------------------------------------------------------------
1 | ## [Q] 什么是 end-to-end exactly-once ?
2 | [A] 一般我们把上游数据源 (Source) 看做一个 end,把下游数据接收 (Sink) 看做另一个 end:
3 |
4 | ```
5 | Source --> Spark Streaming --> Sink
6 | [end] [end]
7 |
8 | ```
9 |
10 | 目前的 Spark Streaming 处理过程**自身**是 exactly-once 的,而且对上游这个 end 的数据管理做得也不错(比如在 direct 模式里自己保存 Kafka 的偏移),但对下游除 HDFS 外的如 HBase, MySQL, Redis 等诸多 end 还不太友好,需要 user code 来实现幂等逻辑、才能保证 end-to-end 的 exactly-once。
11 |
12 | 而在 Spark 2.0 引入的 Structured Streaming 里,将把常见的下游 end 也管理起来(比如通过 batch id 来原生支持幂等),那么不需要 user code 做什么就可以保证 end-to-end 的 exactly-once 了,请见下面一张来自 databricks 的 slide[1]:
13 |
14 | 
15 |
16 |
17 | - [1] Reynold Xin (Databricks), *"the Future of Real-time in Spark"*, 2016.02, http://www.slideshare.net/rxin/the-future-of-realtime-in-spark.
18 |
19 | --
20 |
21 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/25),返回目录请 [猛戳这里](readme.md))
22 |
--------------------------------------------------------------------------------
/Spark Streaming 源码解析系列/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark Streaming 源码解析系列/img.png
--------------------------------------------------------------------------------
/Spark Streaming 源码解析系列/q&a.imgs/end-to-end exactly-once.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark Streaming 源码解析系列/q&a.imgs/end-to-end exactly-once.png
--------------------------------------------------------------------------------
/Spark Streaming 源码解析系列/readme.md:
--------------------------------------------------------------------------------
1 | ## Spark Streaming 源码解析系列
2 |
3 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
4 |
5 | ```
6 | 本系列内容适用范围:
7 |
8 | * 2022.02.10 update, Spark 3.1.3 √
9 | * 2021.10.13 update, Spark 3.2 全系列 √
10 | * 2021.01.07 update, Spark 3.1 全系列 √
11 | * 2020.06.18 update, Spark 3.0 全系列 √
12 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
13 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
14 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
15 | ```
16 |
17 | - *概述*
18 | - [0.1 Spark Streaming 实现思路与模块概述](0.1%20Spark%20Streaming%20实现思路与模块概述.md)
19 | - *模块 1:DAG 静态定义*
20 | - [1.1 DStream, DStreamGraph 详解](1.1%20DStream%2C%20DStreamGraph%20详解.md)
21 | - [1.2 DStream 生成 RDD 实例详解](1.2%20DStream%20生成%20RDD%20实例详解.md)
22 | - *模块 2:Job 动态生成*
23 | - [2.1 JobScheduler, Job, JobSet 详解](2.1%20JobScheduler%2C%20Job%2C%20JobSet%20详解.md)
24 | - [2.2 JobGenerator 详解](2.2%20JobGenerator%20详解.md)
25 | - *模块 3:数据产生与导入*
26 | - [3.1 Receiver 分发详解](3.1%20Receiver%20分发详解.md)
27 | - [3.2 Receiver, ReceiverSupervisor, BlockGenerator, ReceivedBlockHandler 详解](3.2%20Receiver%2C%20ReceiverSupervisor%2C%20BlockGenerator%2C%20ReceivedBlockHandler%20详解.md)
28 | - [3.3 ReceiverTraker, ReceivedBlockTracker 详解](3.3%20ReceiverTraker%2C%20ReceivedBlockTracker%20详解.md)
29 | - *模块 4:长时容错*
30 | - [4.1 Executor 端长时容错详解](4.1%20Executor%20端长时容错详解.md)
31 | - [4.2 Driver 端长时容错详解](4.2%20Driver%20端长时容错详解.md)
32 | - *StreamingContext*
33 | - 5.1 StreamingContext 详解
34 | - *一些资源和 Q&A*
35 | - [Spark 资源集合](https://github.com/lw-lin/CoolplaySpark/tree/master/Spark%20%E8%B5%84%E6%BA%90%E9%9B%86%E5%90%88) (包括 Spark Summit 视频,Spark 中文微信群等资源集合)

36 | - [(Q&A) 什么是 end-to-end exactly-once?](Q%26A%20什么是%20end-to-end%20exactly-once.md)
37 |
38 | ## 致谢
39 |
40 | - Github @wongxingjun 同学指出 3 处 typo,并提 Pull Request 修正(PR 已合并)
41 | - Github @endymecy 同学指出 2 处 typo,并提 Pull Request 修正(PR 已合并)
42 | - Github @Lemonjing 同学指出几处 typo,并提 Pull Request 修正(PR 已合并)
43 | - Github @xiaoguoqiang 同学指出 1 处 typo,并提 Pull Request 修正(PR 已合并)
44 | - Github 张瀚 (@AntikaSmith) 同学指出 1 处 问题(已修正)
45 | - Github Tao Meng (@mtunique) 同学指出 1 处 typo,并提 Pull Request 修正(PR 已合并)
46 | - Github @ouyangshourui 同学指出 1 处问题,并提 Pull Request 修正(PR 已合并)
47 | - Github @jacksu 同学指出 1 处问题,并提 Pull Request 修正(PR 已合并)
48 | - Github @klion26 同学指出 1 处 typo(已修正)
49 | - Github @397090770 同学指出 1 处配图笔误(已修正)
50 | - Github @ubtaojiang1982 同学指出 1 处 typo(已修正)
51 | - Github @marlin5555 同学指出 1 处配图遗漏信息(已修正)
52 | - Weibo @wyggggo 同学指出 1 处 typo(已修正)
53 |
54 | ## Spark Streaming 史前史(1)
55 |
56 | 作为跑在商业硬件上的大数据处理框架,Apache Hadoop 在诞生后的几年内(2005~今)火的一塌糊涂,几乎成为了业界处理大数据的事实上的标准工具:
57 |
58 | 
59 |
60 | ## Spark Streaming 史前史(2)
61 |
62 | 不过大家逐渐发现还需要有单独针对流式数据(其特点是源数据实时性高,要求处理延迟低)的处理需求;于是自 2010 年起又流行起了很多通用流数据处理框架,这种与 Hadoop 等批处理框架配合使用的“批+实时”的双引擎架构又成为了当前事实上的标准:
63 |
64 | 
65 |
66 | ps: 前段时间跟一位前 Googler(很巧他是 MillWheel 的第一批用户)一起吃饭时,了解到 MillWheel 原来是 2010 年左右开发的,据说极其极其好用。
67 |
68 | ## Spark Streaming 诞生
69 |
70 | 
71 |
72 | 
73 |
74 | 本系列文章,就来详解发布于 2013 年的 Spark Streaming。
75 |
76 | ## 知识共享
77 |
78 | 
79 |
80 | 除非另有注明,本《Spark Streaming 源码解析系列》系列文章使用 [CC BY-NC(署名-非商业性使用)](https://creativecommons.org/licenses/by-nc/4.0/) 知识共享许可协议。
81 |
--------------------------------------------------------------------------------
/Spark 样例工程/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | */.idea
3 | */target
4 | *.class
--------------------------------------------------------------------------------
/Spark 样例工程/README.md:
--------------------------------------------------------------------------------
1 | 一个简单 hello world 的样例工程。从这里可以:
2 | - 点击上面的 `spark_hello_world` 目录,直接查看 scala 源代码;
3 | - 点击上面的 `spark_hello_world.zip` 下载本地并运行。
4 |
--------------------------------------------------------------------------------
/Spark 样例工程/spark_hello_world.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 样例工程/spark_hello_world.zip
--------------------------------------------------------------------------------
/Spark 样例工程/spark_hello_world/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.github.lw_lin.spark
8 | spark_hello_world
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 2.10
13 | 1.6.0
14 |
15 |
16 |
17 |
18 | org.apache.spark
19 | spark-core_${scala.version}
20 | ${spark.version}
21 |
22 |
23 | org.apache.spark
24 | spark-sql_${scala.version}
25 | ${spark.version}
26 |
27 |
28 | org.apache.spark
29 | spark-streaming_${scala.version}
30 | ${spark.version}
31 |
32 |
33 |
34 |
35 | src/main/java
36 |
37 |
38 | net.alchim31.maven
39 | scala-maven-plugin
40 | 3.2.0
41 |
42 |
43 | scala-compile
44 |
45 | compile
46 |
47 |
48 |
49 | scala-test-compile
50 |
51 | testCompile
52 |
53 |
54 |
55 |
56 | incremental
57 |
58 | -target:jvm-1.6
59 | -encoding
60 | UTF-8
61 |
62 |
63 | -source
64 | 1.6
65 | -target
66 | 1.6
67 |
68 |
69 |
70 |
71 | maven-compiler-plugin
72 |
73 |
74 | default-compile
75 | none
76 |
77 |
78 | default-testCompile
79 | none
80 |
81 |
82 |
83 |
84 | org.apache.maven.plugins
85 | maven-assembly-plugin
86 | 2.2-beta-5
87 |
88 |
89 | jar-with-dependencies
90 |
91 |
92 |
93 |
94 | package
95 |
96 | single
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | src/main/resources
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/Spark 样例工程/spark_hello_world/src/main/scala/com/github/lw_lin/spark/SparkHelloWorld.scala:
--------------------------------------------------------------------------------
1 | package com.github.lw_lin.spark
2 |
3 | import org.apache.spark.{SparkContext, SparkConf}
4 |
5 | /**
6 | * This program can be downloaded at:
7 | * https://github.com/lw-lin/CoolplaySpark/tree/master/Spark%20%E6%A0%B7%E4%BE%8B%E5%B7%A5%E7%A8%8B
8 | */
9 | object SparkHelloWorld {
10 |
11 | def main(args: Array[String]) {
12 | val conf = new SparkConf()
13 | conf.setAppName("SparkHelloWorld")
14 | conf.setMaster("local[2]")
15 | val sc = new SparkContext(conf)
16 |
17 | val lines = sc.parallelize(Seq("hello world", "hello tencent"))
18 | val wc = lines.flatMap(_.split(" ")).map(word => (word, 1)).reduceByKey(_ + _)
19 | wc.foreach(println)
20 |
21 | Thread.sleep(10 * 60 * 1000) // 挂住 10 分钟; 这时可以去看 SparkUI: http://localhost:4040
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Spark 资源集合/README.md:
--------------------------------------------------------------------------------
1 | # Spark+AI Summit 资源 (2019 最新)
2 |
3 | 
4 | - [2019.04.23~25] 官方日程 => [官方日程](https://databricks.com/sparkaisummit/north-america/schedule-static)
5 | - [2019.04.23~25] PPT 合集 => [PPT 合集 from 示说网](https://mp.weixin.qq.com/s/CSTqXHCpJPvlkVAeaY1mIw)
6 | - [2019.04.23~25] 视频集合 => [墙内地址@百度云](https://pan.baidu.com/s/10HmEy1zbVnfsZQrllTwl8A)
7 |
8 |
9 | # Spark 中文微信交流群
10 |
11 | 
12 |
13 |
14 | # Spark 资源
15 |
16 | - [Databricks 的博客](https://databricks.com/blog)
17 | - Spark 背后的大数据公司的博客,包括 Spark 技术剖析、Spark 案例、行业动态等
18 | - [Apache Spark JIRA issues](https://issues.apache.org/jira/issues/?jql=project+%3D+SPARK)
19 | - 开发人员经常关注一下,可以知道未来 3 ~ 6 个月 Spark 的发展方向
20 | - [Structured Streaming 源码解析系列](https://github.com/lw-lin/CoolplaySpark/tree/master/Structured%20Streaming%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E7%B3%BB%E5%88%97)
21 | - 作者会按照最新 Spark 版本持续更新和修订
22 | - [Spark Streaming 源码解析系列](https://github.com/lw-lin/CoolplaySpark/tree/master/Spark%20Streaming%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%E7%B3%BB%E5%88%97)
23 | - 作者会按照最新 Spark 版本持续更新和修订
24 |
25 |
26 | # 各种资源持续更新 ing
27 |
28 | *欢迎大家提供资源索引(在本 repo 直接发 issue 即可),thanks!*
29 |
30 |
--------------------------------------------------------------------------------
/Spark 资源集合/resources/spark_ai_summit_2019_san_francisco.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/spark_ai_summit_2019_san_francisco.png
--------------------------------------------------------------------------------
/Spark 资源集合/resources/spark_summit_east_2017.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/spark_summit_east_2017.png
--------------------------------------------------------------------------------
/Spark 资源集合/resources/spark_summit_europe_2016.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/spark_summit_europe_2016.jpg
--------------------------------------------------------------------------------
/Spark 资源集合/resources/spark_summit_europe_2016.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/spark_summit_europe_2016.png
--------------------------------------------------------------------------------
/Spark 资源集合/resources/spark_summit_europe_2017.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/spark_summit_europe_2017.png
--------------------------------------------------------------------------------
/Spark 资源集合/resources/wechat_sh_meetup.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/wechat_sh_meetup.PNG
--------------------------------------------------------------------------------
/Spark 资源集合/resources/wechat_sh_meetup_small.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/wechat_sh_meetup_small.PNG
--------------------------------------------------------------------------------
/Spark 资源集合/resources/wechat_spark_streaming.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/wechat_spark_streaming.PNG
--------------------------------------------------------------------------------
/Spark 资源集合/resources/wechat_spark_streaming_small.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/wechat_spark_streaming_small.PNG
--------------------------------------------------------------------------------
/Spark 资源集合/resources/wechat_spark_streaming_small_.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Spark 资源集合/resources/wechat_spark_streaming_small_.PNG
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.1 Structured Streaming 实现思路与实现概述.md:
--------------------------------------------------------------------------------
1 | # Structured Streaming 实现思路与实现概述 #
2 |
3 | ***[酷玩 Spark] Structured Streaming 源码解析系列*** ,返回目录请 [猛戳这里](.)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本文内容适用范围:
9 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
10 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
11 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
12 | ```
13 |
14 | 本文目录
15 |
16 |
17 | 一、引言:Spark 2 时代!
18 | 二、从 Structured Data 到 Structured Streaming
19 | 三、Structured Streaming:无限增长的表格
20 | 四、StreamExecution:持续查询的运转引擎
21 | 1. StreamExecution 的初始状态
22 | 2. StreamExecution 的持续查询
23 | 3. StreamExecution 的持续查询(增量)
24 | 4. 故障恢复
25 | 5. Sources 与 Sinks
26 | 6. 小结:end-to-end exactly-once guarantees
27 | 五、全文总结
28 | 六、扩展阅读
29 | 参考资料
30 |
31 |
32 | ## 一、引言:Spark 2 时代!
33 |
34 | 
35 |
36 | Spark 1.x 时代里,以 SparkContext(及 RDD API)为基础,在 structured data 场景衍生出了 SQLContext, HiveContext,在 streaming 场景衍生出了 StreamingContext,很是琳琅满目。
37 |
38 | 
39 |
40 | Spark 2.x 则咔咔咔精简到只保留一个 SparkSession 作为主程序入口,以 Dataset/DataFrame 为主要的用户 API,同时满足 structured data, streaming data, machine learning, graph 等应用场景,大大减少使用者需要学习的内容,爽爽地又重新实现了一把当年的 "one stack to rule them all" 的理想。
41 |
42 | 
43 |
44 | 我们这里简单回顾下 Spark 2.x 的 Dataset/DataFrame 与 Spark 1.x 的 RDD 的不同:
45 |
46 | - Spark 1.x 的 RDD 更多意义上是一个一维、只有行概念的数据集,比如 `RDD[Person]`,那么一行就是一个 `Person`,存在内存里也是把 `Person` 作为一个整体(序列化前的 java object,或序列化后的 bytes)。
47 | - Spark 2.x 里,一个 `Person` 的 Dataset 或 DataFrame,是二维行+列的数据集,比如一行一个 `Person`,有 `name:String`, `age:Int`, `height:Double` 三列;在内存里的物理结构,也会显式区分列边界。
48 | - Dataset/DataFrame 在 API 使用上有区别:Dataset 相比 DataFrame 而言是 type-safe 的,能够在编译时对 AnalysisExecption 报错(如下图示例):
49 | - Dataset/DataFrame 存储方式无区别:两者在内存中的存储方式是完全一样的、是按照二维行列(UnsafeRow)来存的,所以在没必要区分 `Dataset` 或 `DataFrame` 在 API 层面的差别时,我们统一写作 `Dataset/DataFrame`
50 |
51 | > [小节注] 其实 Spark 1.x 就有了 Dataset/DataFrame 的概念,但还仅是 SparkSQL 模块的主要 API ;到了 2.0 时则 Dataset/DataFrame 不局限在 SparkSQL、而成为 Spark 全局的主要 API。
52 |
53 | ## 二、从 Structured Data 到 Structured Streaming
54 |
55 | 使用 Dataset/DataFrame 的行列数据表格来表达 structured data,既容易理解,又具有广泛的适用性:
56 |
57 | - Java 类 `class Person { String name; int age; double height}` 的多个对象可以方便地转化为 `Dataset/DataFrame`
58 | - 多条 json 对象比如 `{name: "Alice", age: 20, height: 1.68}, {name: "Bob", age: 25, height: 1.76}` 可以方便地转化为 `Dataset/DataFrame`
59 | - 或者 MySQL 表、行式存储文件、列式存储文件等等等都可以方便地转化为 `Dataset/DataFrame`
60 |
61 | Spark 2.0 更进一步,使用 Dataset/Dataframe 的行列数据表格来扩展表达 streaming data —— 所以便横空出世了 Structured Streaming 、《Structured Streaming 源码解析系列》—— 与静态的 structured data 不同,动态的 streaming data 的行列数据表格是一直无限增长的(因为 streaming data 在源源不断地产生)!
62 |
63 | 
64 |
65 | ## 三、Structured Streaming:无限增长的表格
66 |
67 | 基于“无限增长的表格”的编程模型 [1],我们来写一个 streaming 的 word count:
68 |
69 | 
70 |
71 | 对应的 Structured Streaming 代码片段:
72 |
73 | ```scala
74 | val spark = SparkSession.builder().master("...").getOrCreate() // 创建一个 SparkSession 程序入口
75 |
76 | val lines = spark.readStream.textFile("some_dir") // 将 some_dir 里的内容创建为 Dataset/DataFrame;即 input table
77 | val words = lines.flatMap(_.split(" "))
78 | val wordCounts = words.groupBy("value").count() // 对 "value" 列做 count,得到多行二列的 Dataset/DataFrame;即 result table
79 |
80 | val query = wordCounts.writeStream // 打算写出 wordCounts 这个 Dataset/DataFrame
81 | .outputMode("complete") // 打算写出 wordCounts 的全量数据
82 | .format("console") // 打算写出到控制台
83 | .start() // 新起一个线程开始真正不停写出
84 |
85 | query.awaitTermination() // 当前用户主线程挂住,等待新起来的写出线程结束
86 | ```
87 |
88 | 这里需要说明几点:
89 |
90 | - Structured Streaming 也是先纯定义、再触发执行的模式,即
91 | - 前面大部分代码是 ***纯定义*** Dataset/DataFrame 的产生、变换和写出
92 | - 后面位置再真正 ***start*** 一个新线程,去触发执行之前的定义
93 | - 在新的执行线程里我们需要 ***持续地*** 去发现新数据,进而 ***持续地*** 查询最新计算结果至写出
94 | - 这个过程叫做 ***continous query(持续查询)***
95 |
96 | ## 四、StreamExecution:持续查询的运转引擎
97 |
98 | 现在我们将目光聚焦到 ***continuous query*** 的驱动引擎(即整个 Structured Streaming 的驱动引擎) StreamExecution 上来。
99 |
100 | ### 1. StreamExecution 的初始状态
101 |
102 | 我们前文刚解析过,先定义好 Dataset/DataFrame 的产生、变换和写出,再启动 StreamExection 去持续查询。这些 Dataset/DataFrame 的产生、变换和写出的信息就对应保存在 StreamExecution 非常重要的 3 个成员变量中:
103 |
104 | - `sources`: streaming data 的产生端(比如 kafka 等)
105 | - `logicalPlan`: DataFrame/Dataset 的一系列变换(即计算逻辑)
106 | - `sink`: 最终结果写出的接收端(比如 file system 等)
107 |
108 | StreamExection 另外的重要成员变量是:
109 |
110 | - `currentBatchId`: 当前执行的 id
111 | - `batchCommitLog`: 已经成功处理过的批次有哪些
112 | - `offsetLog`, `availableOffsets`, `committedOffsets`: 当前执行需要处理的 source data 的 meta 信息
113 | - `offsetSeqMetadata`: 当前执行的 watermark 信息(event time 相关,本文暂不涉及、另文解析)等
114 |
115 | 我们将 Source, Sink, StreamExecution 及其重要成员变量标识在下图,接下来将逐个详细解析。
116 |
117 | 
118 |
119 | ### 2. StreamExecution 的持续查询
120 |
121 | 
122 |
123 | 一次执行的过程如上图;这里有 6 个关键步骤:
124 |
125 | 1. StreamExecution 通过 Source.getOffset() 获取最新的 offsets,即最新的数据进度;
126 | 2. StreamExecution 将 offsets 等写入到 offsetLog 里
127 | - 这里的 offsetLog 是一个持久化的 WAL (Write-Ahead-Log),是将来可用作故障恢复用
128 | 3. StreamExecution 构造本次执行的 LogicalPlan
129 | - (3a) 将预先定义好的逻辑(即 StreamExecution 里的 logicalPlan 成员变量)制作一个副本出来
130 | - (3b) 给定刚刚取到的 offsets,通过 Source.getBatch(offsets) 获取本执行新收到的数据的 Dataset/DataFrame 表示,并替换到 (3a) 中的副本里
131 | - 经过 (3a), (3b) 两步,构造完成的 LogicalPlan 就是针对本执行新收到的数据的 Dataset/DataFrame 变换(即整个处理逻辑)了
132 | 4. 触发对本次执行的 LogicalPlan 的优化,得到 IncrementalExecution
133 | - 逻辑计划的优化:通过 Catalyst 优化器完成
134 | - 物理计划的生成与选择:结果是可以直接用于执行的 RDD DAG
135 | - 逻辑计划、优化的逻辑计划、物理计划、及最后结果 RDD DAG,合并起来就是 IncrementalExecution
136 | 5. 将表示计算结果的 Dataset/DataFrame (包含 IncrementalExecution) 交给 Sink,即调用 Sink.add(ds/df)
137 | 6. 计算完成后的 commit
138 | - (6a) 通过 Source.commit() 告知 Source 数据已经完整处理结束;Source 可按需完成数据的 garbage-collection
139 | - (6b) 将本次执行的批次 id 写入到 batchCommitLog 里
140 |
141 | ### 3. StreamExecution 的持续查询(增量)
142 |
143 | 
144 |
145 | Structured Streaming 在编程模型上暴露给用户的是,每次持续查询看做面对全量数据(而不仅仅是本次执行信收到的数据),所以每次执行的结果是针对全量数据进行计算的结果。
146 |
147 | 但是在实际执行过程中,由于全量数据会越攒越多,那么每次对全量数据进行计算的代价和消耗会越来越大。
148 |
149 | Structured Streaming 的做法是:
150 |
151 | - 引入全局范围、高可用的 StateStore
152 | - 转全量为增量,即在每次执行时:
153 | - 先从 StateStore 里 restore 出上次执行后的状态
154 | - 然后加入本执行的新数据,再进行计算
155 | - 如果有状态改变,将把改变的状态重新 save 到 StateStore 里
156 | - 为了在 Dataset/DataFrame 框架里完成对 StateStore 的 restore 和 save 操作,引入两个新的物理计划节点 —— StateStoreRestoreExec 和 StateStoreSaveExec
157 |
158 | 所以 Structured Streaming 在编程模型上暴露给用户的是,每次持续查询看做面对全量数据,但在具体实现上转换为增量的持续查询。
159 |
160 | ### 4. 故障恢复
161 |
162 | 通过前面小节的解析,我们知道存储 source offsets 的 offsetLog,和存储计算状态的 StateStore,是全局高可用的。仍然采用前面的示意图,offsetLog 和 StateStore 被特殊标识为紫色,代表高可用。
163 |
164 | 
165 |
166 | 由于 exectutor 节点的故障可由 Spark 框架本身很好的 handle,不引起可用性问题,我们本节的故障恢复只讨论 driver 故障恢复。
167 |
168 | 如果在某个执行过程中发生 driver 故障,那么重新起来的 StreamExecution:
169 |
170 | - 读取 WAL offsetlog 恢复出最新的 offsets 等;相当于取代正常流程里的 (1)(2) 步
171 | - 读取 batchCommitLog 决定是否需要重做最近一个批次
172 | - 如果需要,那么重做 (3a), (3b), (4), (5), (6a), (6b) 步
173 | - 这里第 (5) 步需要分两种情况讨论
174 | - (i) 如果上次执行在 (5) ***结束前即失效***,那么本次执行里 sink 应该完整写出计算结果
175 | - (ii) 如果上次执行在 (5) ***结束后才失效***,那么本次执行里 sink 可以重新写出计算结果(覆盖上次结果),也可以跳过写出计算结果(因为上次执行已经完整写出过计算结果了)
176 |
177 | 这样即可保证每次执行的计算结果,在 sink 这个层面,是 ***不重不丢*** 的 —— 即使中间发生过 1 次或以上的失效和恢复。
178 |
179 | ### 5. Sources 与 Sinks
180 |
181 | 可以看到,Structured Streaming 层面的 Source,需能 ***根据 offsets 重放数据*** [2]。所以:
182 |
183 | | Sources | 是否可重放 | 原生内置支持 | 注解 |
184 | | :-----------------------------: | :------------------------------: | :-----: | :--------------------------------------: |
185 | | **HDFS-compatible file system** |  | 已支持 | 包括但不限于 text, json, csv, parquet, orc, ... |
186 | | **Kafka** |  | 已支持 | Kafka 0.10.0+ |
187 | | **RateStream** |  | 已支持 | 以一定速率产生数据 |
188 | | **RDBMS** |  | *(待支持)* | 预计后续很快会支持 |
189 | | **Socket** |  | 已支持 | 主要用途是在技术会议/讲座上做 demo |
190 | | **Receiver-based** |  | 不会支持 | 就让这些前浪被拍在沙滩上吧 |
191 |
192 | 也可以看到,Structured Streaming 层面的 Sink,需能 ***幂等式写入数据*** [3]。所以:
193 |
194 | | Sinks | 是否幂等写入 | 原生内置支持 | 注解 |
195 | | :-----------------------------: | :------------------------------: | :-----: | :--------------------------------------: |
196 | | **HDFS-compatible file system** |  | 已支持 | 包括但不限于 text, json, csv, parquet, orc, ... |
197 | | **ForeachSink** (自定操作幂等) |  | 已支持 | 可定制度非常高的 sink |
198 | | **RDBMS** |  | *(待支持)* | 预计后续很快会支持 |
199 | | **Kafka** |  | 已支持 | Kafka 目前不支持幂等写入,所以可能会有重复写入
(但推荐接着 Kafka 使用 streaming de-duplication 来去重) |
200 | | **ForeachSink** (自定操作不幂等) |  | 已支持 | 不推荐使用不幂等的自定操作 |
201 | | **Console** |  | 已支持 | 主要用途是在技术会议/讲座上做 demo |
202 |
203 | ### 6. 小结:end-to-end exactly-once guarantees
204 |
205 | 所以在 Structured Streaming 里,我们总结下面的关系[4]:
206 |
207 | 
208 |
209 | 这里的 end-to-end 指的是,如果 source 选用类似 Kafka, HDFS 等,sink 选用类似 HDFS, MySQL 等,那么 Structured Streaming 将自动保证在 sink 里的计算结果是 exactly-once 的 —— Structured Streaming 终于把过去需要使用者去维护的 sink 去重逻辑接盘过去了!:-)
210 |
211 | ## 五、全文总结
212 |
213 | 自 Spark 2.0 开始,处理 structured data 的 Dateset/DataFrame 被扩展为同时处理 streaming data,诞生了 Structured Streaming。
214 |
215 | Structured Streaming 以“无限扩展的表格”为编程模型,在 StreamExecution 实际执行中增量执行,并满足 end-to-end exactly-once guarantee.
216 |
217 | 在 Spark 2.0 时代,Dataset/DataFrame 成为主要的用户 API,同时满足 structured data, streaming data, machine learning, graph 等应用场景,大大减少使用者需要学习的内容,爽爽地又重新实现了一把当年的 "one stack to rule them all" 的理想。
218 |
219 | > 谨以此《Structured Streaming 源码解析系列》和以往的《Spark Streaming 源码解析系列》,向“把大数据变得更简单 (make big data simple) ”的创新者们,表达感谢和敬意。
220 |
221 | ## 六、扩展阅读
222 |
223 | 1. Spark Summit East 2016: [The Future of Real-time in Spark](https://spark-summit.org/east-2016/events/keynote-day-3/)
224 | 2. Blog: [Continuous Applications: Evolving Streaming in Apache Spark 2.0](https://databricks.com/blog/2016/07/28/continuous-applications-evolving-streaming-in-apache-spark-2-0.html)
225 | 3. Blog: [Structured Streaming In Apache Spark: A new high-level API for streaming](https://databricks.com/blog/2016/07/28/structured-streaming-in-apache-spark.html)
226 |
227 | ## 参考资料
228 |
229 | 1. [Structured Streaming Programming Guide](http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html)
230 | 2. [Github: org/apache/spark/sql/execution/streaming/Source.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/Source.scala)
231 | 3. [Github: org/apache/spark/sql/execution/streaming/Sink.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/Sink.scala)
232 | 4. [A Deep Dive into Structured Streaming](http://www.slideshare.net/databricks/a-deep-dive-into-structured-streaming?qid=51953136-8233-4d5d-a1c2-ce30051f16d1&v=&b=&from_search=1)
233 |
234 | ## 知识共享
235 |
236 | 
237 |
238 | 除非另有注明,本《Structured Streaming 源码解析系列》系列文章使用 [CC BY-NC(署名-非商业性使用)](https://creativecommons.org/licenses/by-nc/4.0/) 知识共享许可协议。
239 |
240 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/29),返回目录请 [猛戳这里](.))
241 |
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/010.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/010.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/015.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/015.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/030.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/030.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/040.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/040.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/050.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/050.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/070.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/070.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/100.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/110.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/110.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/120.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/170.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/170.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/checked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/checked.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/1.imgs/negative.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/1.imgs/negative.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/2.1 Structured Streaming 之 Source 解析.md:
--------------------------------------------------------------------------------
1 | # Structured Streaming 之 Source 解析 #
2 |
3 | ***[酷玩 Spark] Structured Streaming 源码解析系列*** ,返回目录请 [猛戳这里](.)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本文内容适用范围:
9 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
10 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
11 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
12 | ```
13 |
14 |
15 |
16 | 阅读本文前,请一定先阅读 [Structured Streaming 实现思路与实现概述](1.1%20Structured%20Streaming%20实现思路与实现概述.md) 一文,其中概述了 Structured Streaming 的实现思路(包括 StreamExecution, Source, Sink 等在 Structured Streaming 里的作用),有了全局概念后再看本文的细节解释。
17 |
18 | ## 引言
19 |
20 | Structured Streaming 非常显式地提出了输入(Source)、执行(StreamExecution)、输出(Sink)的 3 个组件,并且在每个组件显式地做到 fault-tolerant,由此得到整个 streaming 程序的 end-to-end exactly-once guarantees.
21 |
22 | 具体到源码上,Source 是一个抽象的接口 [trait Source](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/Source.scala) [1],包括了 Structured Streaming 实现 end-to-end exactly-once 处理所一定需要提供的功能(我们将马上详细解析这些方法):
23 |
24 | ```scala
25 | trait Source {
26 | /* 方法 (1) */ def schema: StructType
27 | /* 方法 (2) */ def getOffset: Option[Offset]
28 | /* 方法 (3) */ def getBatch(start: Option[Offset], end: Offset): DataFrame
29 | /* 方法 (4) */ def commit(end: Offset) : Unit = {}
30 | /* 方法 (5) */ def stop(): Unit
31 | }
32 | ```
33 |
34 | 相比而言,前作 Spark Streaming 的输入 InputDStream 抽象 [2] 并不强制要求可靠和可重放,因而也存在一些不可靠输入源(如 Receiver-based 输入源),在失效情形下丢失源头输入数据;这时即使 Spark Streaming 框架本身能够重做,但由于源头数据已经不存在了,也会导致计算本身不是 exactly-once 的。当然,Spark Streaming 对可靠的数据源如 HDFS, Kafka 等的计算给出的 guarantee 还是 exactly-once。
35 |
36 | 进化到 Structured Streaming 后,只保留对 ***可靠数据源*** 的支持:
37 |
38 | - 已支持
39 | - Kafka,具体实现是 KafkaSource extends Source
40 | - HDFS-compatible file system,具体实现是 FileStreamSource extends Source
41 | - RateStream,具体实现是 RateStreamSource extends Source
42 | - 预计后续很快会支持
43 | - RDBMS
44 |
45 | ## Source:方法与功能
46 |
47 | 在 Structured Streaming 里,由 StreamExecution 作为持续查询的驱动器,分批次不断地:
48 |
49 | 
50 |
51 | 1. 在每个 StreamExecution 的批次最开始,StreamExecution 会向 Source 询问当前 Source 的最新进度,即最新的 offset
52 | - 这里是由 StreamExecution 调用 Source 的 `def getOffset: Option[Offset]`,即方法 (2)
53 | - Kafka (KafkaSource) 的具体 `getOffset()` 实现 ,会通过在 driver 端的一个长时运行的 consumer 从 kafka brokers 处获取到各个 topic 最新的 offsets(注意这里不存在 driver 或 consumer 直接连 zookeeper),比如 `topicA_partition1:300, topicB_partition1:50, topicB_partition2:60`,并把 offsets 返回
54 | - HDFS-compatible file system (FileStreamSource) 的具体 `getOffset()` 实现,是先扫描一下最新的一组文件,给一个递增的编号并持久化下来,比如 `2 -> {c.txt, d.txt}`,然后把编号 `2` 作为最新的 offset 返回
55 | 2. 这个 Offset 给到 StreamExecution 后会被 StreamExecution 持久化到自己的 WAL 里
56 | 3. 由 Source 根据 StreamExecution 所要求的 start offset、end offset,提供在 `(start, end]` 区间范围内的数据
57 | - 这里是由 StreamExecution 调用 Source 的 `def getBatch(start: Option[Offset], end: Offset): DataFrame`,即方法 (3)
58 | - 这里的 start offset 和 end offset,通常就是 Source 在上一个执行批次里提供的最新 offset,和 Source 在这个批次里提供的最新 offset;但需要注意区间范围是 ***左开右闭*** !
59 | - 数据的返回形式的是一个 DataFrame(这个 DataFrame 目前只包含数据的描述信息,并没有发生实际的取数据操作)
60 | 4. StreamExecution 触发计算逻辑 logicalPlan 的优化与编译
61 | 5. 把计算结果写出给 Sink
62 | - 注意这时才会由 Sink 触发发生实际的取数据操作,以及计算过程
63 | 6. 在数据完整写出到 Sink 后,StreamExecution 通知 Source 可以废弃数据;然后把成功的批次 id 写入到 batchCommitLog
64 | - 这里是由 StreamExecution 调用 Source 的 `def commit(end: Offset): Unit`,即方法 (4)
65 | - `commit()` 方法主要是帮助 Source 完成 garbage-collection,如果外部数据源本身即具有 garbage-collection 功能,如 Kafka,那么在 Source 的具体 `commit()` 实现上即可为空、留给外部数据源去自己管理
66 |
67 | 到此,是解析了 Source 的方法 (2) (3) (4) 在 StreamExecution 的具体批次执行中,所需要实现的语义和被调用的过程。
68 |
69 | 另外还有方法 (1) 和 (5):
70 |
71 | - 方法 (1) `def schema: StructType`
72 | - 返回一个本 Source 数据的 schema 描述,即每列数据的名称、类型、是否可空等
73 | - 本方法在 Structured Streaming 开始真正执行每个批次开始前调用,不在每个批次执行时调用
74 | - 方法 (5) `def stop(): Unit`
75 | - 当一个持续查询结束时,Source 会被调用此方法
76 |
77 | ## Source 的具体实现:HDFS-compatible file system, Kafka, Rate
78 |
79 | 我们总结一下截至目前,Source 已有的具体实现:
80 |
81 | | Sources | 是否可重放 | 原生内置支持 | 注解 |
82 | | :-----------------------------: | :------------------------------: | :----: | :--------------------------------------: |
83 | | **HDFS-compatible file system** |  | 已支持 | 包括但不限于 text, json, csv, parquet, orc, ... |
84 | | **Kafka** |  | 已支持 | Kafka 0.10.0+ |
85 | | **RateStream** |  | 已支持 | 以一定速率产生数据 |
86 | | **Socket** |  | 已支持 | 主要用途是在技术会议/讲座上做 demo |
87 |
88 | 这里我们特别强调一下,虽然 Structured Streaming 也内置了 `socket` 这个 Source,但它并不能可靠重放、因而也不符合 Structured Streaming 的结构体系。它的主要用途只是在技术会议/讲座上做 demo,不应用于线上生产系统。
89 |
90 | ## 扩展阅读
91 |
92 | 1. [Structured Streaming + Kafka Integration Guide (Kafka broker version 0.10.0 or higher)](https://spark.apache.org/docs/latest/structured-streaming-kafka-integration.html)
93 |
94 | ## 参考资料
95 |
96 | 1. [Github: org/apache/spark/sql/execution/streaming/Source.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/Source.scala)
97 | 2. [Github: org/apache/spark/streaming/dstream/InputDStream.scala](https://github.com/apache/spark/blob/master/streaming/src/main/scala/org/apache/spark/streaming/dstream/InputDStream.scala)
98 |
99 |
100 |
101 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/31),返回目录请 [猛戳这里](.))
102 |
103 |
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/2.2 Structured Streaming 之 Sink 解析.md:
--------------------------------------------------------------------------------
1 | # Structured Streaming 之 Sink 解析 #
2 |
3 | ***[酷玩 Spark] Structured Streaming 源码解析系列*** ,返回目录请 [猛戳这里](.)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本文内容适用范围:
9 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
10 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
11 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
12 | ```
13 |
14 |
15 |
16 | 阅读本文前,请一定先阅读 [Structured Streaming 实现思路与实现概述](1.1%20Structured%20Streaming%20实现思路与实现概述.md) 一文,其中概述了 Structured Streaming 的实现思路(包括 StreamExecution, Source, Sink 等在 Structured Streaming 里的作用),有了全局概念后再看本文的细节解释。
17 |
18 | ## 引言
19 |
20 | Structured Streaming 非常显式地提出了输入(Source)、执行(StreamExecution)、输出(Sink)的 3 个组件,并且在每个组件显式地做到 fault-tolerant,由此得到整个 streaming 程序的 end-to-end exactly-once guarantees.
21 |
22 | 具体到源码上,Sink 是一个抽象的接口 [trait Sink](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/Sink.scala) [1],只有一个方法:
23 |
24 | ```scala
25 | trait Sink {
26 | def addBatch(batchId: Long, data: DataFrame): Unit
27 | }
28 | ```
29 |
30 | 这个仅有的 `addBatch()` 方法支持了 Structured Streaming 实现 end-to-end exactly-once 处理所一定需要的功能。我们将马上解析这个 `addBatch()` 方法。
31 |
32 | 相比而言,前作 Spark Streaming 并没有对输出进行特别的抽象,而只是在 DStreamGraph [2] 里将一些 dstreams 标记为了 output。当需要 exactly-once 特性时,程序员可以根据当前批次的时间标识,来 ***自行维护和判断*** 一个批次是否已经执行过。
33 |
34 | 进化到 Structured Streaming 后,显式地抽象出了 Sink,并提供了一些原生幂等的 Sink 实现:
35 |
36 | - 已支持
37 | - HDFS-compatible file system,具体实现是 FileStreamSink extends Sink
38 | - Foreach sink,具体实现是 ForeachSink extends Sink
39 | - Kafka sink,具体实现是 KafkaSink extends Sink
40 | - 预计后续很快会支持
41 | - RDBMS
42 |
43 | ## Sink:方法与功能
44 |
45 | 在 Structured Streaming 里,由 StreamExecution 作为持续查询的驱动器,分批次不断地:
46 |
47 | 
48 |
49 | 1. 在每个 StreamExecution 的批次最开始,StreamExecution 会向 Source 询问当前 Source 的最新进度,即最新的 offset
50 | 2. 这个 Offset 给到 StreamExecution 后会被 StreamExecution 持久化到自己的 WAL 里
51 | 3. 由 Source 根据 StreamExecution 所要求的 start offset、end offset,提供在 `(start, end]` 区间范围内的数据
52 | 4. StreamExecution 触发计算逻辑 logicalPlan 的优化与编译
53 | 5. 把计算结果写出给 Sink
54 | - 具体是由 StreamExecution 调用 `Sink.addBatch(batchId: Long, data: DataFrame)`
55 | - 注意这时才会由 Sink 触发发生实际的取数据操作,以及计算过程
56 | - 通常 Sink 直接可以直接把 `data: DataFrame` 的数据写出,并在完成后记录下 `batchId: Long`
57 | - 在故障恢复时,分两种情况讨论:
58 | - (i) 如果上次执行在本步 ***结束前即失效***,那么本次执行里 sink 应该完整写出计算结果
59 | - (ii) 如果上次执行在本步 ***结束后才失效***,那么本次执行里 sink 可以重新写出计算结果(覆盖上次结果),也可以跳过写出计算结果(因为上次执行已经完整写出过计算结果了)
60 | 6. 在数据完整写出到 Sink 后,StreamExecution 通知 Source 可以废弃数据;然后把成功的批次 id 写入到 batchCommitLog
61 |
62 | ## Sink 的具体实现:HDFS-API compatible FS, Foreach
63 |
64 | ### (a) 具体实现: HDFS-API compatible FS
65 |
66 | 通常我们使用如下方法方法写出到 HDFS-API compatible FS:
67 |
68 | ```scala
69 | writeStream
70 | .format("parquet") // parquet, csv, json, text, orc ...
71 | .option("checkpointLocation", "path/to/checkpoint/dir")
72 | .option("path", "path/to/destination/dir")
73 | ```
74 |
75 | 那么我们看这里 `FileStreamSink` 具体的 `addBatch()` 实现是:
76 |
77 | ```scala
78 | // 来自:class FileStreamSink extends Sink
79 | // 版本:Spark 2.1.0
80 | override def addBatch(batchId: Long, data: DataFrame): Unit = {
81 | /* 首先根据持久化的 fileLog 来判断这个 batchId 是否已经写出过 */
82 | if (batchId <= fileLog.getLatest().map(_._1).getOrElse(-1L)) {
83 | /* 如果 batchId 已经完整写出过,则本次跳过 addBatch */
84 | logInfo(s"Skipping already committed batch $batchId")
85 | } else {
86 | /* 本次需要具体执行写出 data */
87 | /* 初始化 FileCommitter -- FileCommitter 能正确处理 task 推测执行、task 失败重做等情况 */
88 | val committer = FileCommitProtocol.instantiate(
89 | className = sparkSession.sessionState.conf.streamingFileCommitProtocolClass,
90 | jobId = batchId.toString,
91 | outputPath = path,
92 | isAppend = false)
93 |
94 | committer match {
95 | case manifestCommitter: ManifestFileCommitProtocol =>
96 | manifestCommitter.setupManifestOptions(fileLog, batchId)
97 | case _ => // Do nothing
98 | }
99 |
100 | /* 获取需要做 partition 的 columns */
101 | val partitionColumns: Seq[Attribute] = partitionColumnNames.map { col =>
102 | val nameEquality = data.sparkSession.sessionState.conf.resolver
103 | data.logicalPlan.output.find(f => nameEquality(f.name, col)).getOrElse {
104 | throw new RuntimeException(s"Partition column $col not found in schema ${data.schema}")
105 | }
106 | }
107 |
108 | /* 真正写出数据 */
109 | FileFormatWriter.write(
110 | sparkSession = sparkSession,
111 | queryExecution = data.queryExecution,
112 | fileFormat = fileFormat,
113 | committer = committer,
114 | outputSpec = FileFormatWriter.OutputSpec(path, Map.empty),
115 | hadoopConf = hadoopConf,
116 | partitionColumns = partitionColumns,
117 | bucketSpec = None,
118 | refreshFunction = _ => (),
119 | options = options)
120 | }
121 | }
122 | ```
123 |
124 | ### (b) 具体实现: Foreach
125 |
126 | 通常我们使用如下方法写出到 foreach sink:
127 |
128 | ```scala
129 | writeStream
130 | /* 假设进来的每条数据是 String 类型的 */
131 | .foreach(new ForeachWriter[String] {
132 | /* 每个 partition 即每个 task 会在开始时调用此 open() 方法 */
133 | /* 注意对于同一个 partitionId/version,此方法可能被先后调用多次,如 task 失效重做时 */
134 | /* 注意对于同一个 partitionId/version,此方法也可能被同时调用,如推测执行时 */
135 | override def open(partitionId: Long, version: Long): Boolean = {
136 | println(s"open($partitionId, $version)")
137 | true
138 | }
139 | /* 此 partition 内即每个 task 内的每条数据,此方法都被调用 */
140 | override def process(value: String): Unit = println(s"process $value")
141 | /* 正常结束或异常结束时,此方法被调用。但一些异常情况时,此方法不一定被调用。 */
142 | override def close(errorOrNull: Throwable): Unit = println(s"close($errorOrNull)")
143 | })
144 | ```
145 |
146 | 那么我们看这里 `ForeachSink` 具体的 `addBatch()` 实现是:
147 |
148 | ```scala
149 | // 来自:class ForeachSink extends Sink with Serializable
150 | // 版本:Spark 2.1.0
151 | override def addBatch(batchId: Long, data: DataFrame): Unit = {
152 | val encoder = encoderFor[T].resolveAndBind(
153 | data.logicalPlan.output,
154 | data.sparkSession.sessionState.analyzer)
155 | /* 是 rdd 的 foreachPartition,即是 task 级别 */
156 | data.queryExecution.toRdd.foreachPartition { iter =>
157 | /* partition/task 级别的 open */
158 | if (writer.open(TaskContext.getPartitionId(), batchId)) {
159 | try {
160 | while (iter.hasNext) {
161 | /* 对每条数据调用 process() 方法 */
162 | writer.process(encoder.fromRow(iter.next()))
163 | }
164 | } catch {
165 | case e: Throwable =>
166 | /* 异常时调用 close() 方法 */
167 | writer.close(e)
168 | throw e
169 | }
170 | /* 正常写完调用 close() 方法 */
171 | writer.close(null)
172 | } else {
173 | /* 不写数据、直接调用 close() 方法 */
174 | writer.close(null)
175 | }
176 | }
177 | }
178 | ```
179 |
180 | 所以我们看到,foreach sink 需要使用者提供 writer,所以这里的可定制度就非常高。
181 |
182 | 但是仍然需要注意,由于 foreach 的 writer 可能被 open() 多次,可能有多个 task 同时调用一个 writer。所以推荐 writer 一定要写成幂等的,如果 writer 不幂等、那么 Structured Streaming 框架本身也没有更多的办法能够保证 end-to-end exactly-once guarantees 了。
183 |
184 | ### (c) 具体实现: Kafka
185 |
186 | Spark 2.1.1 版本开始加入了 KafkaSink,使得 Spark 也能够将数据写入到 kafka 中。
187 |
188 | 通常我们使用如下方法写出到 kafka sink:
189 |
190 | ```scala
191 | writeStream
192 | .format("kafka")
193 | .option("checkpointLocation", ...)
194 | .outputMode(...)
195 | .option("kafka.bootstrap.servers", ...) // 写出到哪个集群
196 | .option("topic", ...) // 写出到哪个 topic
197 | ```
198 |
199 | 那么我们看这里 `KafkaSink` 具体的 `addBatch()` 实现是:
200 |
201 | ```scala
202 | // 来自:class KafkaSink extends Sink
203 | // 版本:Spark 2.1.1, 2.2.0
204 | override def addBatch(batchId: Long, data: DataFrame): Unit = {
205 | if (batchId <= latestBatchId) {
206 | logInfo(s"Skipping already committed batch $batchId")
207 | } else {
208 | // 主要是通过 KafkaWriter.write() 来做写出;
209 | // 在 KafkaWriter.write() 里,主要是继续通过 KafkaWriteTask.execute() 来做写出
210 | KafkaWriter.write(sqlContext.sparkSession,
211 | data.queryExecution, executorKafkaParams, topic)
212 | latestBatchId = batchId
213 | }
214 | }
215 | ```
216 |
217 | 那么我们继续看这里 `KafkaWriteTask` 具体的 `execute()` 实现是:
218 |
219 | ```scala
220 | // 来自:class KafkaWriteTask
221 | // 版本:Spark 2.1.1, 2.2.0
222 | def execute(iterator: Iterator[InternalRow]): Unit = {
223 | producer = new KafkaProducer[Array[Byte], Array[Byte]](producerConfiguration)
224 | while (iterator.hasNext && failedWrite == null) {
225 | val currentRow = iterator.next()
226 | // 这里的 projection 主要是构建 projectedRow,使得:
227 | // 其第 0 号元素是 topic
228 | // 其第 1 号元素是 key 的 binary 表示
229 | // 其第 2 号元素是 value 的 binary 表示
230 | val projectedRow = projection(currentRow)
231 | val topic = projectedRow.getUTF8String(0)
232 | val key = projectedRow.getBinary(1)
233 | val value = projectedRow.getBinary(2)
234 | if (topic == null) {
235 | throw new NullPointerException(s"null topic present in the data. Use the " +
236 | s"${KafkaSourceProvider.TOPIC_OPTION_KEY} option for setting a default topic.")
237 | }
238 | val record = new ProducerRecord[Array[Byte], Array[Byte]](topic.toString, key, value)
239 | val callback = new Callback() {
240 | override def onCompletion(recordMetadata: RecordMetadata, e: Exception): Unit = {
241 | if (failedWrite == null && e != null) {
242 | failedWrite = e
243 | }
244 | }
245 | }
246 | producer.send(record, callback)
247 | }
248 | }
249 | ```
250 |
251 | 这里我们需要说明的是,由于 Spark 本身会失败重做 —— 包括单个 task 的失败重做、stage 的失败重做、整个拓扑的失败重做等 —— 那么同一条数据可能被写入到 kafka 一次以上。由于 kafka 目前还不支持 transactional write,所以多写入的数据不能被撤销,会造成一些重复。当然 kafka 自身的高可用写入(比如写入 broker 了的数据的 ack 消息没有成功送达 producer,导致 producer 重新发送数据时),也有可能造成重复。
252 |
253 | 在 kafka 支持 transactional write 之前,可能需要下游实现下去重机制。比如如果下游仍然是 Structured Streaming,那么可以使用 streaming deduplication 来获得去重后的结果。
254 |
255 | ## 总结
256 |
257 | 我们总结一下截至目前,Sink 已有的具体实现:
258 |
259 | | Sinks | 是否幂等写入 | 原生内置支持 | 注解 |
260 | | :-----------------------------: | :------------------------------: | :----: | :--------------------------------------: |
261 | | **HDFS-compatible file system** |  | 已支持 | 包括但不限于 text, json, csv, parquet, orc, ... |
262 | | **ForeachSink** (自定操作幂等) |  | 已支持 | 可定制度非常高的 sink |
263 | | **Kafka** |  | 已支持 | Kafka 目前不支持幂等写入,所以可能会有重复写入
(但推荐接着 Kafka 使用 streaming de-duplication 来去重) |
264 | | **ForeachSink** (自定操作不幂等) |  | 已支持 | 不推荐使用不幂等的自定操作 |
265 |
266 | 这里我们特别强调一下,虽然 Structured Streaming 也内置了 `console` 这个 Source,但其实它的主要用途只是在技术会议/讲座上做 demo,不应用于线上生产系统。
267 |
268 | ## 参考资料
269 |
270 | 1. [Github: org/apache/spark/sql/execution/streaming/Sink.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/Sink.scala)
271 | 2. [Github: org/apache/spark/streaming/DStreamGraph.scala](https://github.com/apache/spark/blob/master/streaming/src/main/scala/org/apache/spark/streaming/DStreamGraph.scala)
272 |
273 |
274 |
275 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/32),返回目录请 [猛戳这里](.))
276 |
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/3.1 Structured Streaming 之状态存储解析.md:
--------------------------------------------------------------------------------
1 | # Structured Streaming 之状态存储解析 #
2 |
3 | ***[酷玩 Spark] Structured Streaming 源码解析系列*** ,返回目录请 [猛戳这里](.)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本文内容适用范围:
9 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
10 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
11 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
12 | ```
13 |
14 |
15 |
16 | 阅读本文前,请一定先阅读 [Structured Streaming 实现思路与实现概述](1.1%20Structured%20Streaming%20实现思路与实现概述.md) 一文,其中概述了 Structured Streaming 的实现思路(包括 StreamExecution, StateStore 等在 Structured Streaming 里的作用),有了全局概念后再看本文的细节解释。
17 |
18 | ## 引言
19 |
20 | 我们知道,持续查询的驱动引擎 StreamExecution 会持续不断地驱动每个批次的执行。
21 |
22 | 对于不需要跨批次的持续查询,如 `map()`, `filter()` 等,每个批次之间的执行相互独立,不需要状态支持。而比如类似 `count()` 的聚合式持续查询,则需要跨批次的状态支持,这样本批次的执行只需依赖上一个批次的结果,而不需要依赖之前所有批次的结果。这也即增量式持续查询,能够将每个批次的执行时间稳定下来,避免越后面的批次执行时间越长的情形。
23 |
24 | 这个增量式持续查询的思路和实现,我们在 [Structured Streaming 实现思路与实现概述](1.1 Structured Streaming 实现思路与实现概述.md) 解析过:
25 |
26 | 
27 |
28 | 而在这里面的 StateStore,即是 Structured Streaming 用于保存跨批次状态结果的模块组件。本文解析 StateStore 模块。
29 |
30 | ## StateStore 模块的总体思路
31 |
32 | 
33 |
34 | StateStore 模块的总体思路:
35 | - 分布式实现
36 | - 跑在现有 Spark 的 driver-executors 架构上
37 | - driver 端是轻量级的 coordinator,只做协调工作
38 | - executor 端负责状态的实际分片的读写
39 | - 状态分片
40 | - 因为一个应用里可能会包含多个需要状态的 operator,而且 operator 本身也是分 partition 执行的,所以状态存储的分片以 `operatorId`+`partitionId` 为切分依据
41 | - 以分片为基本单位进行状态的读入和写出
42 | - 每个分片里是一个 key-value 的 store,key 和 value 的类型都是 `UnsafeRow`(可以理解为 SparkSQL 里的 Object 通用类型),可以按 key 查询、或更新
43 | - 状态分版本
44 | - 因为 StreamExection 会持续不断地执行批次,因而同一个 operator 同一个 partition 的状态也是随着时间不断更新、产生新版本的数据
45 | - 状态的版本是与 StreamExecution 的进展一致,比如 StreamExection 的批次 id = 7 完成时,那么所有 version = 7 的状态即已经持久化
46 | - 批量读入和写出分片
47 | - 对于每个分片,读入时
48 | - 根据 operator + partition + version, 从 HDFS 读入数据,并缓存在内存里
49 | - 对于每个分片,写出时
50 | - 累计当前版本(即 StreamExecution 的当前批次)的多行的状态修改,一次性写出到 HDFS 一个修改的流水 log,流水 log 写完即标志本批次的状态修改完成
51 | - 同时应用修改到内存中的状态缓存
52 |
53 | 关于 StateStore 的 operator, partiton, version 有一个图片可帮助理解:
54 |
55 | 
56 |
57 | ## StateStore:(a)迁移、(b)更新和查询、(c)维护、(d)故障恢复
58 |
59 |
60 |
61 | ### (a) StateStore 在不同的节点之间如何迁移
62 |
63 | 在 StreamExecution 执行过程中,随时在 operator 实际执行的 executor 节点上唤起一个状态存储分片、并读入前一个版本的数据即可(如果 executor 上已经存在一个分片,那么就直接重用,不用唤起分片、也不用读入数据了)。
64 |
65 | 我们上节讲过,持久化的状态是在 HDFS 上的。那么如上图所示:
66 |
67 | - `executor a`, 唤起了 `operator = 1, partition = 1` 的状态存储分片,从 HDFS 里位于本机的数据副本 load 进来 `version = 5` 的数据;
68 | - 一个 executor 节点可以执行多个 operator,那么也就可以在一个 executor 上唤起多个状态存储分片(分别对应不同的 operator + partition),如图示 `executor b`;
69 | - 在一些情况下,需要从其他节点的 HDFS 数据副本上 load 状态数据,如图中 `executor c` 需要从 `executor b` 的硬盘上 load 数据;
70 | - 另外还有的情况是,同一份数据被同时 load 到不同的 executor 上,如 `executor d` 和 `executor a` 即是读入了同一份数据 —— 推测执行时就容易产生这种情况 —— 这时也不会产生问题,因为 load 进来的是同一份数据,然后在两个节点上各自修改,最终只会有一个节点能够成功提交对状态的修改。
71 |
72 | ### (b) StateStore 的更新和查询
73 |
74 | 我们前面也讲过,在一个状态存储分片里,是 key-value 的 store。这个 key-value 的 store 支持如下操作:
75 |
76 | ```scala
77 | /* == CRUD 增删改查 =============================== */
78 |
79 | // 查询一条 key-value
80 | def get(key: UnsafeRow): Option[UnsafeRow]
81 |
82 | // 新增、或修改一条 key-value
83 | def put(key: UnsafeRow, value: UnsafeRow): Unit
84 |
85 | // 删除一条符合条件的 key-value
86 | def remove(condition: UnsafeRow => Boolean): Unit
87 | // 根据 key 删除 key-value
88 | def remove(key: UnsafeRow): Unit
89 |
90 | /* == 批量操作相关 =============================== */
91 |
92 | // 提交当前执行批次的所有修改,将刷出到 HDFS,成功后版本将自增
93 | def commit(): Long
94 |
95 | // 放弃当前执行批次的所有修改
96 | def abort(): Unit
97 |
98 | // 当前状态分片、当前版本的所有 key-value 状态
99 | def iterator(): Iterator[(UnsafeRow, UnsafeRow)]
100 |
101 | // 当前状态分片、当前版本比上一个版本的所有增量更新
102 | def updates(): Iterator[StoreUpdate]
103 | ```
104 |
105 | 使用 StateStore 的代码可以这样写(现在都是 Structured Streaming 内部实现在使用 StateStore,上层用户无需面对这些细节):
106 |
107 | ```scala
108 | // 在最开始,获取正确的状态分片(按需重用已有分片或读入新的分片)
109 | val store = StateStore.get(StateStoreId(checkpointLocation, operatorId, partitionId), ..., version, ...)
110 |
111 | // 开始进行一些更改
112 | store.put(...)
113 | store.remove(...)
114 |
115 | // 更改完成,批量提交缓存在内存里的更改到 HDFS
116 | store.commit()
117 |
118 | // 查看当前状态分片的所有 key-value / 刚刚更新了的 key-value
119 | store.iterator()
120 | store.updates()
121 | ```
122 |
123 | ### (c) StateStore 的维护
124 |
125 | 我们看到,前面 StateStore 在写出状态的更新时,是写出的修改流水 log。
126 |
127 | StateStore 本身也带了 maintainess 即维护模块,会周期性的在后台将过去的状态和最近若干版本的流水 log 进行合并,并把合并后的结果重新写回到 HDFS:`old_snapshot + delta_a + delta_b + … => lastest_snapshot`。
128 |
129 | 这个过程跟 HBase 的 major/minor compact 差不多,但还没有区别到 major/minor 的粒度。
130 |
131 | ### (d) StateStore 的故障恢复
132 |
133 | StateStore 的所有状态以 HDFS 为准。如果某个状态分片在更新过程中失败了,那么还没有写出的更新会不可见。
134 |
135 | 恢复时也是从 HDFS 读入最近可见的状态,并配合 StreamExecution 的执行批次重做。从另一个角度说,就是大家 —— 输入数据、及状态存储 —— 先统一往后会退到本执行批次刚开始时的状态,然后重新计算。当然这里重新计算的粒度是 Spark 的单个 task,即一个 partition 的输入数据 + 一个 partition 的状态存储。
136 |
137 | 从 HDFS 读入最近可见的状态时,如果有最新的 snapshot,也就用最新的 snapshot,如果没有,就读入稍旧一点的 snapshot 和新的 deltas,先做一下最新状态的合并。
138 |
139 | ## 总结
140 |
141 | 在 Structured Streaming 里,StateStore 模块提供了 ***分片的***、***分版本的***、***可迁移的***、***高可用*** key-value store。
142 |
143 | 基于这个 StateStore 模块,StreamExecution 实现了 ***增量的*** 持续查询、和很好的故障恢复以维护 ***end-to-end exactly-once guarantees***。
144 |
145 | ## 扩展阅读
146 |
147 | 1. [Github: org/apache/spark/sql/execution/streaming/state/StateStore.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/StateStore.scala)
148 | 2. [Github: org/apache/spark/sql/execution/streaming/state/HDFSBackedStateStoreProvider.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/state/HDFSBackedStateStoreProvider.scala)
149 |
150 |
151 |
152 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/33),返回目录请 [猛戳这里](.))
153 |
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/3.imgs/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/3.imgs/100.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/3.imgs/200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/3.imgs/200.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.1 Structured Streaming 之 Event Time 解析.md:
--------------------------------------------------------------------------------
1 | # Structured Streaming 之 Event Time 解析 #
2 |
3 | ***[酷玩 Spark] Structured Streaming 源码解析系列*** ,返回目录请 [猛戳这里](.)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本文内容适用范围:
9 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
10 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
11 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
12 | ```
13 |
14 |
15 |
16 | 阅读本文前,请一定先阅读 [Structured Streaming 实现思路与实现概述](1.1%20Structured%20Streaming%20实现思路与实现概述.md) 一文,其中概述了 Structured Streaming 的实现思路,有了全局概念后再看本文的细节解释。
17 |
18 | ## Event Time !
19 |
20 | Spark Streaming 时代有过非官方的 event time 支持尝试 [1],而在进化后的 Structured Streaming 里,添加了对 event time 的原生支持。
21 |
22 | 我们来看一段官方 programming guide 的例子 [2]:
23 |
24 | ```scala
25 | import spark.implicits._
26 |
27 | val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
28 |
29 | // Group the data by window and word and compute the count of each group
30 | // Please note: we'll revise this example in
31 | val windowedCounts = words.groupBy(
32 | window($"timestamp", "10 minutes", "5 minutes"),
33 | $"word"
34 | ).count()
35 | ```
36 |
37 | 这里的执行过程如下图。
38 |
39 | 
40 |
41 | - 我们有一系列 arriving 的 records
42 | - 首先是一个对着时间列 `timestamp` 做长度为`10m`,滑动为`5m` 的 *window()* 操作
43 | - 例如上图右上角的虚框部分,当达到一条记录 `12:22|dog` 时,会将 `12:22` 归入两个窗口 `12:15-12:25`、`12:20-12:30`,所以产生两条记录:`12:15-12:25|dog`、`12:20-12:30|dog`,对于记录 `12:24|dog owl` 同理产生两条记录:`12:15-12:25|dog owl`、`12:20-12:30|dog owl`
44 | - 所以这里 *window()* 操作的本质是 *explode()*,可由一条数据产生多条数据
45 | - 然后对 *window()* 操作的结果,以 `window` 列和 `word` 列为 key,做 *groupBy().count()* 操作
46 | - 这个操作的聚合过程是增量的(借助 StateStore)
47 | - 最后得到一个有 `window`, `word`, `count` 三列的状态集
48 |
49 | ## 处理 Late Data
50 |
51 | 还是沿用前面 *window()* + *groupBy().count()* 的例子,但注意有一条迟到的数据 `12:06|cat` :
52 |
53 | 
54 |
55 | 可以看到,在这里的 late data,在 State 里被正确地更新到了应在的位置。
56 |
57 | ## OutputModes
58 |
59 | 我们继续来看前面 *window()* + *groupBy().count()* 的例子,现在我们考虑将结果输出,即考虑 OutputModes:
60 |
61 | #### (a) Complete
62 |
63 | Complete 的输出是和 State 是完全一致的:
64 |
65 | 
66 |
67 | #### (b) Append
68 |
69 | Append 的语义将保证,一旦输出了某条 key,未来就不会再输出同一个 key。
70 |
71 | 
72 |
73 | 所以,在上图 `12:10` 这个批次直接输出 `12:00-12:10|cat|1`, `12:05-12:15|cat|1` 将是错误的,因为在 `12:20` 将结果更新为了 `12:00-12:10|cat|2`,但是 Append 模式下却不会再次输出 `12:00-12:10|cat|2`,因为前面输出过了同一条 key `12:00-12:10|cat` 的结果`12:00-12:10|cat|1`。
74 |
75 | 为了解决这个问题,在 Append 模式下,Structured Streaming 需要知道,某一条 key 的结果什么时候不会再更新了。当确认结果不会再更新的时候(下一篇文章专门详解依靠 watermark 确认结果不再更新),就可以将结果进行输出。
76 |
77 | 
78 |
79 | 如上图所示,如果我们确定 `12:30` 这个批次以后不会再有对 `12:00-12:10` 这个 window 的更新,那么我们就可以把 `12:00-12:10` 的结果在 `12:30` 这个批次输出,并且也会保证后面的批次不会再输出 `12:00-12:10` 的 window 的结果,维护了 Append 模式的语义。
80 |
81 | #### (c) Update
82 |
83 | Update 模式已在 Spark 2.1.1 及以后版本获得正式支持。
84 |
85 | 
86 |
87 | 如上图所示,在 Update 模式中,只有本执行批次 State 中被更新了的条目会被输出:
88 |
89 | - 在 12:10 这个执行批次,State 中全部 2 条都是新增的(因而也都是被更新了的),所以输出全部 2 条;
90 | - 在 12:20 这个执行批次,State 中 2 条是被更新了的、 4 条都是新增的(因而也都是被更新了的),所以输出全部 6 条;
91 | - 在 12:30 这个执行批次,State 中 4 条是被更新了的,所以输出 4 条。这些需要特别注意的一点是,如 Append 模式一样,本执行批次中由于(通过 watermark 机制)确认 `12:00-12:10` 这个 window 不会再被更新,因而将其从 State 中去除,但没有因此产生输出。
92 |
93 | ## 总结
94 |
95 | 本文解析了 Structured Streaming 原生提供的对 event time 的支持,包括 window()、groupBy() 增量聚合、对 late date 的支持、以及在 Complete, Append, Update 模式下的输出结果。
96 |
97 | ## 扩展阅读
98 |
99 | 1. [Github: org/apache/spark/sql/catalyst/analysis/Analyzer.scala#TimeWindowing](https://github.com/apache/spark/blob/v2.1.1/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala#L2232)
100 | 2. [Github: org/apache/spark/sql/catalyst/expressions/TimeWindow](https://github.com/apache/spark/blob/master/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/TimeWindow.scala)
101 |
102 | ## 参考资料
103 |
104 | 1. https://github.com/cloudera/spark-dataflow
105 | 2. [Structured Streaming Programming Guide - Window Operations on Event Time](http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#window-operations-on-event-time)
106 |
107 |
108 |
109 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/34),返回目录请 [猛戳这里](.))
110 |
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.2 Structured Streaming 之 Watermark 解析.md:
--------------------------------------------------------------------------------
1 | # 4.2 Structured Streaming 之 Watermark 解析 #
2 |
3 | ***[酷玩 Spark] Structured Streaming 源码解析系列*** ,返回目录请 [猛戳这里](.)
4 |
5 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
6 |
7 | ```
8 | 本文内容适用范围:
9 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
10 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
11 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
12 | ```
13 |
14 |
15 |
16 | 阅读本文前,请一定先阅读 [Structured Streaming 之 Event Time 解析](4.1%20Structured%20Streaming%20之%20Event%20Time%20解析.md),其中解析了 Structured Streaming 的 Event Time 及为什么需要 Watermark。
17 |
18 | ## 引言
19 |
20 | 
21 |
22 | 我们在前文 [Structured Streaming 之 Event Time 解析](4.1%20Structured%20Streaming%20之%20Event%20Time%20解析.md) 中的例子,在:
23 |
24 | - (a) 对 event time 做 *window()* + *groupBy().count()* 即利用状态做跨执行批次的聚合,并且
25 | - (b) 输出模式为 Append 模式
26 |
27 | 时,需要知道在 `12:30` 结束后不会再有对 `window 12:00-12:10` 的更新,因而可以在 `12:30` 这个批次结束时,输出 `window 12:00-12:10` 的 1 条结果。
28 |
29 | ## Watermark 机制
30 |
31 | 对上面这个例子泛化一点,是:
32 |
33 | - (a+) 在对 event time 做 *window()* + *groupBy().aggregation()* 即利用状态做跨执行批次的聚合,并且
34 | - (b+) 输出模式为 Append 模式或 Update 模式
35 |
36 | 时,Structured Streaming 将依靠 watermark 机制来限制状态存储的无限增长、并(对 Append 模式)尽早输出不再变更的结果。
37 |
38 | 换一个角度,如果既不是 Append 也不是 Update 模式,或者是 Append 或 Update 模式、但不需状态做跨执行批次的聚合时,则不需要启用 watermark 机制。
39 |
40 | 具体的,我们启用 watermark 机制的方式是:
41 |
42 | ```scala
43 | val words = ... // streaming DataFrame of schema { timestamp: Timestamp, word: String }
44 |
45 | // Group the data by window and word and compute the count of each group
46 | val windowedCounts = words
47 | .withWatermark("timestamp", "10 minutes") // 注意这里的 watermark 设置!
48 | .groupBy(
49 | window($"timestamp", "10 minutes", "5 minutes"),
50 | $"word")
51 | .count()
52 | ```
53 |
54 | 这样即告诉 Structured Streaming,以 `timestamp` 列的最大值为锚点,往前推 10min 以前的数据不会再收到。这个值 —— 当前的最大 timestamp 再减掉 10min —— 这个随着 timestamp 不断更新的 Long 值,就是 watermark。
55 |
56 | 
57 |
58 | 所以,在之前的这里图示中:
59 |
60 | - 在 `12:20` 这个批次结束后,锚点变成了 `12:20|dog owl` 这条记录的 event time `12:20` ,watermark 变成了 `12:20 - 10min = 12:10`;
61 | - 所以,在 `12:30` 批次结束时,即知道 event time `12:10` 以前的数据不再收到了,因而 window `12:00-12:10` 的结果也不会再被更新,即可以安全地输出结果 `12:00-12:10|cat|2`;
62 | - 在结果 `12:00-12:10|cat|2` 输出以后,State 中也不再保存 window `12:00-12:10` 的相关信息 —— 也即 State Store 中的此条状态得到了清理。
63 |
64 | ## 图解 Watermark 的进展
65 |
66 | 下图中的这个来自官方的例子 [1],直观的解释了 watermark 随着 event time 的进展情况(对应的相关参数仍与前面的例子一致):
67 |
68 | 
69 |
70 | ## 详解 Watermark 的进展
71 |
72 | ### (a) Watermark 的保存和恢复
73 |
74 | 我们知道,在每次 StreamExecution 的每次增量执行(即 IncrementalExecution)开始后,首先会在 driver 端持久化相关的 source offsets 到 offsetLog 中,即下图中的步骤 (1)。实际在这个过程中,也将系统当前的 watermark 等值保存了进去。
75 |
76 | 
77 |
78 | 这样,在故障恢复时,可以从 offsetLog 中恢复出来的 watermark 值;当然在初次启动、还没有 offsetLog 时,watermark 的值会初始化为 0。
79 |
80 | ### (b) Watermark 用作过滤条件
81 |
82 | 在每次 StreamExecution 的每次增量执行(即 IncrementalExecution)开始时,将 driver 端的 watermark 最新值(即已经写入到 offsetLog 里的值)作为过滤条件,加入到整个执行的 logicalPlan 中。
83 |
84 | 具体的是在 Append 和 Complete 模式下,且需要与 StateStore 进行交互时,由如下代码设置过滤条件:
85 |
86 | ```scala
87 | /** Generate a predicate that matches data older than the watermark */
88 | private lazy val watermarkPredicate: Option[Predicate] = {
89 | val optionalWatermarkAttribute =
90 | keyExpressions.find(_.metadata.contains(EventTimeWatermark.delayKey))
91 |
92 | optionalWatermarkAttribute.map { watermarkAttribute =>
93 | // If we are evicting based on a window, use the end of the window. Otherwise just
94 | // use the attribute itself.
95 | val evictionExpression =
96 | if (watermarkAttribute.dataType.isInstanceOf[StructType]) {
97 | LessThanOrEqual(
98 | GetStructField(watermarkAttribute, 1),
99 | Literal(eventTimeWatermark.get * 1000))
100 | } else {
101 | LessThanOrEqual(
102 | watermarkAttribute,
103 | Literal(eventTimeWatermark.get * 1000))
104 | }
105 |
106 | logInfo(s"Filtering state store on: $evictionExpression")
107 | newPredicate(evictionExpression, keyExpressions)
108 | }
109 | }
110 | ```
111 |
112 | 总的来讲,就是进行 `event time 的字段 <= watermark` 的过滤。
113 |
114 | 所以在 Append 模式下,把 StateStore 里符合这个过滤条件的状态进行输出,因为这些状态将来不会再更新了;在 Update 模式下,把符合这个过滤条件的状态删掉,因为这些状态将来不会再更新了。
115 |
116 | ### (c) Watermark 的更新
117 |
118 | 在单次增量执行的过程中,按照每个 partition 即每个 task,在处理每一条数据时,同时收集 event time 的(统计)数字:
119 |
120 | ```scala
121 | // 来自 EventTimeWatermarkExec
122 | case class EventTimeStats(var max: Long, var min: Long, var sum: Long, var count: Long) {
123 | def add(eventTime: Long): Unit = {
124 | this.max = math.max(this.max, eventTime)
125 | this.min = math.min(this.min, eventTime)
126 | this.sum += eventTime
127 | this.count += 1
128 | }
129 |
130 | def merge(that: EventTimeStats): Unit = {
131 | this.max = math.max(this.max, that.max)
132 | this.min = math.min(this.min, that.min)
133 | this.sum += that.sum
134 | this.count += that.count
135 | }
136 |
137 | def avg: Long = sum / count
138 | }
139 | ```
140 |
141 | 那么每个 partition 即每个 task,收集到了 event time 的 `max`, `min`, `sum`, `count` 值。在整个 job 结束时,各个 partition 即各个 task 的 `EventTimeStats` ,收集到 driver 端。
142 |
143 | 在 driver 端,在每次增量执行结束后,把收集到的所有的 eventTimeStats 取最大值,并进一步按需更新 watermark(本次可能更新,也可能不更新):
144 |
145 | ```scala
146 | // 来自 StreamExecution
147 | lastExecution.executedPlan.collect {
148 | case e: EventTimeWatermarkExec if e.eventTimeStats.value.count > 0 =>
149 | logDebug(s"Observed event time stats: ${e.eventTimeStats.value}")
150 | /* 所收集的 eventTimeStats 的 max 值,减去之前 withWatermark() 时指定的 delayMS 值 */
151 | /* 结果保存为 newWatermarkMs */
152 | e.eventTimeStats.value.max - e.delayMs
153 | }.headOption.foreach { newWatermarkMs =>
154 | /* 比较 newWatermarkMs 与当前的 batchWatermarkMs */
155 | if (newWatermarkMs > offsetSeqMetadata.batchWatermarkMs) {
156 | /* 将当前的 batchWatermarkMs 的更新为 newWatermarkMs */
157 | logInfo(s"Updating eventTime watermark to: $newWatermarkMs ms")
158 | offsetSeqMetadata.batchWatermarkMs = newWatermarkMs
159 | } else {
160 | /* 当前的 batchWatermarkMs 不需要更新 */
161 | logDebug(
162 | s"Event time didn't move: $newWatermarkMs < " +
163 | s"${offsetSeqMetadata.batchWatermarkMs}")
164 | }
165 | }
166 | ```
167 |
168 | 所以我们看,在单次增量执行过程中,具体的是在做 `(b) Watermark 用作过滤条件` 的过滤过程中,watermark 维持不变。
169 |
170 | 直到在单次增量执行结束时,根据收集到的 eventTimeStats,才更新一个 watermark。更新后的 watermark 会被保存和故障时恢复,这个过程是我们在 `(a) Watermark 的保存和恢复` 中解析的。
171 |
172 | ## 关于 watermark 的一些说明
173 |
174 | 关于 Structured Streaming 的目前 watermark 机制,我们有几点说明:
175 |
176 | 1. 再次强调,(a+) 在对 event time 做 *window()* + *groupBy().aggregation()* 即利用状态做跨执行批次的聚合,并且 (b+) 输出模式为 Append 模式或 Update 模式时,才需要 watermark,其它时候不需要;
177 | 2. watermark 的本质是要帮助 StateStore 清理状态、不至于使 StateStore 无限增长;同时,维护 Append 正确的语义(即判断在何时某条结果不再改变、从而将其输出);
178 | 3. 目前版本(Spark 2.2)的 watermark 实现,是依靠最大 event time 减去一定 late threshold 得到的,尚未支持 Source 端提供的 watermark;
179 | - 未来可能的改进是,从 Source 端即开始提供关于 watermark 的特殊信息,传递到 StreamExecution 中使用 [2],这样可以加快 watermark 的进展,从而能更早的得到输出数据
180 | 4. Structured Streaming 对于 watermark 的承诺是:(a) watermark 值不后退(包括正常运行和发生故障恢复时);(b) watermark 值达到后,大多时候会在下一个执行批次输出结果,但也有可能延迟一两个批次(发生故障恢复时),上层应用不应该对此有依赖。
181 |
182 | ## 扩展阅读
183 |
184 | 1. [Github: org/apache/spark/sql/execution/streaming/StatefulAggregate.scala](https://github.com/apache/spark/blob/master/sql/core/src/main/scala/org/apache/spark/sql/execution/streaming/StatefulAggregate.scala)
185 | 2. [Flink Doc: Generating Timestamps / Watermarks](https://ci.apache.org/projects/flink/flink-docs-master/dev/event_timestamps_watermarks.html)
186 |
187 | ## 参考资料
188 |
189 | 1. [Structured Streaming Programming Guide](http://spark.apache.org/docs/latest/structured-streaming-programming-guide.html)
190 | 2. [Design Doc: Structured Streaming Watermarks for handling late data and dropping old aggregates](https://docs.google.com/document/d/1z-Pazs5v4rA31azvmYhu4I5xwqaNQl6ZLIS03xhkfCQ/edit)
191 |
192 |
193 |
194 | (本文完,参与本文的讨论请 [猛戳这里](https://github.com/lw-lin/CoolplaySpark/issues/35),返回目录请 [猛戳这里](.))
195 |
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/100.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/150.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/200.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/210.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/210.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/220.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/220.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/230.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/230.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/300.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/300.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/4.imgs/300_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/Structured Streaming 源码解析系列/4.imgs/300_large.png
--------------------------------------------------------------------------------
/Structured Streaming 源码解析系列/README.md:
--------------------------------------------------------------------------------
1 | ## Structured Streaming 源码解析系列
2 |
3 | [「腾讯广告」](http://e.qq.com)技术团队(原腾讯广点通技术团队)荣誉出品
4 |
5 | ```
6 | 本文内容适用范围:
7 | * 2018.11.02 update, Spark 2.4 全系列 √ (已发布:2.4.0)
8 | * 2018.02.28 update, Spark 2.3 全系列 √ (已发布:2.3.0 ~ 2.3.2)
9 | * 2017.07.11 update, Spark 2.2 全系列 √ (已发布:2.2.0 ~ 2.2.3)
10 | ```
11 |
12 | - *一、概述*
13 | - [1.1 Structured Streaming 实现思路与实现概述](1.1%20Structured%20Streaming%20实现思路与实现概述.md)
14 | - *二、Sources 与 Sinks*
15 | - [2.1 Structured Streaming 之 Source 解析](2.1%20Structured%20Streaming%20之%20Source%20解析.md)
16 | - [2.2 Structured Streaming 之 Sink 解析](2.2%20Structured%20Streaming%20之%20Sink%20解析.md)
17 | - *三、状态存储*
18 | - [3.1 Structured Streaming 之状态存储解析](3.1%20Structured%20Streaming%20之状态存储解析.md)
19 | - *四、Event Time 与 Watermark*
20 | - [4.1 Structured Streaming 之 Event Time 解析](4.1%20Structured%20Streaming%20之%20Event%20Time%20解析.md)
21 | - [4.2 Structured Streaming 之 Watermark 解析](4.2%20Structured%20Streaming%20之%20Watermark%20解析.md)
22 | - *#、一些资源和 Q&A*
23 | - [Spark 资源集合](https://github.com/lw-lin/CoolplaySpark/tree/master/Spark%20%E8%B5%84%E6%BA%90%E9%9B%86%E5%90%88) (包括 Spark Streaming 中文微信群、Spark Summit 视频等资源集合)

24 | - [Q&A] Structured Streaming 与 Spark Streaming 的区别
25 |
26 | ## 致谢
27 |
28 | - Github [@wongxingjun](http://github.com/wongxingjun) 同学指出 2 处 typo,并提 Pull Request 修正(PR 已合并)
29 | - Github [@wangmiao1981](http://github.com/wangmiao1981) 同学指出几处 typo,并提 Pull Request 修正(PR 已合并)
30 | - Github [@zilutang](http://github.com/zilutang) 同学指出 1 处 typo,并提 Pull Request 修正(PR 已合并)
31 |
32 | > 谨以此《Structured Streaming 源码解析系列》和以往的《Spark Streaming 源码解析系列》,向“把大数据变得更简单 (make big data simple) ”的创新者们,表达感谢和敬意。
33 |
34 | ## 知识共享
35 |
36 | 
37 |
38 | 除非另有注明,本《Structured Streaming 源码解析系列》系列文章使用 [CC BY-NC(署名-非商业性使用)](https://creativecommons.org/licenses/by-nc/4.0/) 知识共享许可协议。
39 |
--------------------------------------------------------------------------------
/coolplay_spark_logo_cn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/coolplay_spark_logo_cn.png
--------------------------------------------------------------------------------
/coolplay_spark_logo_cn_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lw-lin/CoolplaySpark/d4880cfb051d3e03ba8d8189eb3699000628ebdc/coolplay_spark_logo_cn_small.png
--------------------------------------------------------------------------------