Spark 是否需要在每次转换与动作后都对同一 DataFrame 执行 persist() 操作?
Spark DataFrame persist() 使用规则解答
先明确两个Spark计算的基础核心规则,所有疑问都从这两个规则推导:
- DataFrame是逻辑执行计划的不可变封装,任何转换操作(map、filter等)都不会修改原有DataFrame,只会生成携带全新血缘关系的新DataFrame实例
persist()/cache()是给调用该方法时的特定DataFrame实例打上标记:后续对这个实例触发动作计算完成后,会按照设定的存储级别将计算结果存入内存/磁盘缓存。这个标记和变量名没有绑定关系,后续修改变量的指向也不会影响之前已经打上标记的实例。
逐行对应你的示例代码拆解疑问
你的示例代码逻辑对应关系:
var df: DataFrame = spark.read.format(...) // 仅定义"读取外部数据"的逻辑计划,无实际计算 df.persist.count // 给"纯读取"的DataFrame实例打persist标记,触发count动作后,将原始读取的数据集缓存到内存,即你说的缓存#1 df = df.map(...) // 变量df指向全新的、携带"读取+map"血缘的DataFrame实例,和之前被persist的"纯读取"实例无引用关联 val tmp1 = df.filter(...).count() // 触发一次带过滤的动作,计算完成后不会留下任何缓存 df.persist.count // 这里是给当前这个"读取+一次map"的新实例打persist标记,触发count动作后,缓存的是一次map完成的计算结果,即你说的缓存#2 df = df.map(...) // 变量df再次指向全新的、携带"读取+两次map"血缘的DataFrame实例 val tmp2 = df.filter(...).count() // 触发一次带过滤的动作,无缓存落地 df.persist.count // 给当前这个"读取+两次map"的新实例打persist标记,触发count动作后,缓存的是两次map完成的计算结果,即你说的缓存#3
两个核心问题的直接答案
- 每次转换后调用
persist().count()是否有必要?
完全没有必要强制每一步都调用。只有当你后续需要多次复用同一步计算的结果时,对这个实例打persist标记才有性能收益——如果某个DataFrame从生成到被动作计算只用到一次,persist只会额外增加序列化、存储的开销,没有任何收益。 - 多次调用persist后缓存的是不是同一个内存对象?
完全不是。三次persist操作分别绑定了三个不同血缘的DataFrame实例,对应的缓存内容分别是:原始读取结果、一次map转换结果、两次map转换结果,三者是独立存在的内存块,不存在复用关系。
常见的persist使用误区
- 不要误以为persist是给整个计算流程加全局缓存buff:每一次转换生成新的DataFrame后,之前给老实例打的persist标记不会自动传递给新实例,必须单独给需要复用的新实例打标记
- 不要因为变量名都是df,就误以为缓存是同一份:var变量只是一个引用指针,每次重新赋值后指针指向的是完全不同的DataFrame对象,对应的缓存自然也没有关联
- 不要无节制给每一步转换结果加persist:Spark的内存资源是有限的,多个独立缓存的结果会挤占计算内存,甚至引发不必要的内存溢写磁盘,反而拖慢整体性能
- 不要忘了手动
unpersist()不再需要的缓存:如果某个被persist的DataFrame后续不再使用,要及时调用方法释放内存,不然缓存会一直占用资源直到被内存淘汰机制回收
针对你的场景的最优实践
如果你的后续计算逻辑是:两次map完成后,需要基于最终结果多次执行不同的过滤、聚合动作,那完全不需要在每一步转换后都调用persist,只需要在两次map都完成后,给最终的DataFrame实例打一次persist标记,触发一次动作把最终结果缓存下来即可,这种方式的整体缓存开销最小,性能收益最高。
内容的提问来源于stack exchange,提问作者Zoey
相关产品推荐
相关产品推荐

