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 · 大数据架构 · 数据工程实践

相关推荐
天行健,君子而铎1 小时前
2026年中国API安全产品综合排名:选型指南与市场趋势解析
大数据·数据库·安全
未来之窗软件服务1 小时前
Web 技能的自主测试系统开好处发架构思维-东方仙盟
大数据·前端·架构·仙盟创梦ide·东方仙盟
IT毕设实战小研1 小时前
基于大数据的二手车数据分析与预测
java·大数据·后端·爬虫·python·算法·课程设计
Allen_LVyingbo1 小时前
医疗AI可扩展网格信息系统中网格计算技术的智能优化
大数据·人工智能·python·算法·机器学习·django·健康医疗
接口不宕机1 小时前
1688 图搜 API 实战:溯源竞品上游货源,监控批发底价与库存变动
大数据
码视野3 小时前
多宠 RFID 颈圈识别与湿粮半导体制冷保鲜分餐喂食器解决方案(软硬件一体化)
大数据·人工智能·python
2601_954526754 小时前
2026 生成式搜索引擎底层技术演进与 GEO优化服务商推荐:大模型检索增强(RAG)管道与语义对齐工程实战
大数据·人工智能·搜索引擎
A15362554 小时前
WMS 仓储管理系统推荐:企业如何选适配的仓储管理系统
大数据·人工智能
星野川崎20610 小时前
电商多店运维:云机24小时挂机频繁掉线、账号无故风控深度原因分析及解决方案
大数据·运维·服务器·云计算·电商