SparkStreaming 之 DStream 底层结构剖析

摘要:DStream 是 Spark Streaming 的核心抽象,但很多人用了很久也没搞清楚它到底是什么。这篇把 DStream 拆开------它本质是"RDD 的时间序列",本身不存数据;讲清它内部的 DStreamGraph、Lineage 血缘,以及 getOrCompute 怎么按时间点把 DStream 变成 RDD,顺带说清有状态操作为什么要强制开 checkpoint。

关键词:Spark Streaming, DStream, RDD 序列, DStreamGraph, getOrCompute, checkpoint


一、DStream 到底是个什么东西

先给一个判断,省得绕:DStream 是"RDD 的时间序列",它自己一个字节的数据都不存

回想上一篇文章里 batchInterval 的概念------Spark Streaming 每 2 秒(举例)切一个 batch,每个 batch 就是一个 RDD。DStream 就是把这些按时间排好的 RDD 串起来的抽象:

复制代码
RDD @ t0   RDD @ t1   RDD @ t2   ...   RDD @ tn
(batch 0)  (batch 1)  (batch 2)        (batch n)

所以你写的 ds.map(...)ds.reduceByKey(...) 这些操作,本质上是在描述"每个时间片的 RDD 该怎么变换",而不是立刻去算。这一点和 RDD 的惰性求值是一脉相承的------DStream 的转换操作构建的是依赖链,只有遇到 output 操作(printsaveAsTextFilesforeachRDD)才真正触发计算。


二、DStream 的内部结构

一个 DStream 内部有三块关键的东西。

2.1 DStreamGraph:DStream 的 DAG

和 RDD 有 DAG 一样,DStream 也有自己的图结构 DStreamGraph,里面按角色分三类节点:

  • InputDStream :数据入口,比如 SocketInputDStreamKafkaInputDStream,对应上一篇讲的 Receiver。
  • TransformedDStream :中间转换,map/flatMap/filter/join/window 都会产生这种节点。
  • ForEachDStream :输出节点,foreachRDDprint 属于这一类。

DStreamGraph 维护这张依赖图,每个 batch 时间点到了,就顺着图把每个 DStream 对应的 RDD 算出来。

2.2 Lineage:血缘

DStream 同样记录完整的血缘------"我是从哪个父 DStream、经过什么转换来的"。这条链子的用途和 RDD 的 lineage 一样:容错。

不过 DStream 的血缘有个 RDD 没有的要求:涉及有状态操作或需要故障恢复时,必须开 checkpoint。因为 DStream 的血缘是逻辑上的转换链,跨很多 batch 之后这条链会非常长,光靠血缘重算既不现实,也没法恢复"累计到现在的状态"。checkpoint 会把依赖链和状态持久化到可靠存储(HDFS),Driver 挂了能从 checkpoint 拉起来接着算。

scala 复制代码
ssc.checkpoint("hdfs://namenode:8020/checkpoint/streaming-app")

// 有状态操作,不开 checkpoint 会直接抛异常
val state = lines.updateStateByKey(updateFunc)

三、转换操作的两个分类维度

理解 DStream 的操作,可以按两个维度切,这直接影响性能和要不要 checkpoint。

维度一:无状态 vs 有状态

无状态 有状态
代表算子 map / filter / union / join updateStateByKey / window
是否跨 batch 只看当前 batch 跨 batch 累计状态
checkpoint 不需要 必须开

无状态转换每个 RDD 独立处理,干净利落。有状态转换要在 batch 之间记住状态,这个状态就是靠 checkpoint 落盘保住的。

维度二:无 shuffle vs 有 shuffle

map/flatMap/filter 是窄依赖,一个分区内独立算,不碰网络;join/groupByKey/reduceByKey 是宽依赖,要跨分区 shuffle,代价高一个量级。这和 RDD 的判断完全一致------DStream 的每个操作,最终都会落到对应 RDD 的某个操作上。


四、getOrCompute:DStream 怎么变成 RDD

这是 DStream 最核心的一个方法,把"按时间生成 RDD"这件事讲清楚。

JobGenerator 每个 batch 触发一次,传入时间点 t,然后对每个 DStream 调用 getOrCompute(t)

  1. 查缓存 :先看 generatedRDDs 里有没有 t 对应的 RDD,有就直接返回。
  2. 没命中就 compute(t) :调用当前 DStream 的 compute,它会先递归地让父 DStream 生成 t 时刻的 RDD,再应用当前这一步的转换。
  3. 缓存返回 :算出来的 RDD 存进 generatedRDDs,避免重复计算。

为什么要缓存?因为同一个 DStream 在同一个时间点可能被多个下游引用------比如一个流既要做实时统计,又要写一份原始数据。不缓存的话,同一个 RDD 会被算两遍,浪费整条链路的计算。


五、transform 和 foreachRDD:打通 RDD API

DStream 的算子就那么几十个,遇到复杂逻辑或者要访问外部资源(连接池、状态存储)时就不够用了。transformforeachRDD 就是为这个留的口子:

scala 复制代码
// transform:拿到 RDD,返回新的 RDD(继续流式计算)
val cleaned = ds.transform { rdd =>
  rdd.mapPartitions { iter => /* 每分区初始化一次连接 */ }
}

// foreachRDD:拿到 RDD,做输出(到此为止,不再返回)
ds.foreachRDD { rdd =>
  rdd.foreachPartition { partitionOfRecords =>
    /* 写外部存储 */
  }
}

两者都在 Driver 端直接拿到 RDD,可以随意用 RDD API。一个常见的最佳实践是:把连接这类重量级对象的创建放进 mapPartitions/foreachPartition,而不是在每条记录上 new 一个连接,否则吞吐会崩。


六、总结

  • DStream 是 RDD 的时间序列,不存数据,转换操作是惰性的。
  • DStreamGraph 分 Input/Transformed/ForEach 三类节点,Lineage 负责容错,有状态操作必须 checkpoint。
  • 转换按"无状态/有状态"和"无 shuffle/有 shuffle"两个维度判断,前者决定要不要 checkpoint,后者决定代价高低。
  • getOrCompute 是 DStream 到 RDD 的关键:查缓存 → 递归 compute → 缓存返回,缓存是为了不让同一时间点的 RDD 被重复计算。
  • 复杂逻辑用 transform/foreachRDD 打通 RDD API,连接等重对象放分区级初始化。

作者 :大数据技术实践者

博客blog.starzy.cn

GitHubstarzy1990.github.io

专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践

相关推荐
Freak嵌入式22 分钟前
实战对比:用 mpremote/Putty/MobaXterm 连接树莓派 Pico REPL,谁更省心?
java·大数据·开发语言·科技·单片机·嵌入式硬件
智圣新创0133 分钟前
从制度落地到效能可测:智圣新创第二课堂成绩单体系高职场景建设的全国普适性实践
大数据·人工智能
航飞光电市场经理1 小时前
2026年工业UWB定位降本实战:基于航飞光电UWB信标+蓝牙信标+LoRa融合方案的深度解析
大数据·人工智能
跨境数据猎手1 小时前
外贸独立站开发:从选型到上线全流程梳理
大数据·架构·自动化
whcyhhh2 小时前
头歌实践教学平台:大数据存储2023(十三1)
大数据·数据库·python
TDengine (老段)2 小时前
TDengine 应用案例 — IT 运维与可观测性
大数据·运维·数据库·制造·时序数据库·tdengine·涛思数据
金立基包装胶水2 小时前
水性纸袋热封胶,常见故障有多少种?
大数据·笔记·其他
geneculture2 小时前
从融智学到人机互助:技术成果交易的理论重构与战略前瞻——基于“九五至尊模型”与融智学三大判断的分析
大数据·人工智能·融智学的重要应用·哲学与科学统一性·融智时代(杂志)·序位逻辑的实例化·中国企业知识产权战略专栏
lvts_cs2 小时前
如何判断化工产业规划的定位布局是否合理
大数据·人工智能