2. 缓存级别详解:MEMORY_ONLY、MEMORY_AND_DISK、DISK_ONLY等9种级别对比

缓存级别,说白了就是告诉Spark:「你把数据存哪儿?存几份?」

我刚开始用Spark时,觉得这玩意儿不就是个缓存吗?选MEMORY_ONLY不就完了?结果有一次跑一个20GB的迭代计算,集群直接OOM,任务挂了整整三次。后来我才明白——选错缓存级别,比不缓存还可怕。

2.1 9种缓存级别一览

Spark总共提供了9种缓存级别。别被数字吓到,其实核心就3种存储介质:内存、磁盘、堆外内存。再加上副本数,组合出9种。

缓存级别 存储介质 序列化 副本数 适用场景
MEMORY_ONLY 内存 否 1 数据量小,内存够用
MEMORY_ONLY_SER 内存 是 1 内存紧张,可接受序列化开销
MEMORY_AND_DISK 内存+磁盘 否 1 数据量大,内存放不下
MEMORY_AND_DISK_SER 内存+磁盘 是 1 内存紧张且数据量大
DISK_ONLY 磁盘 否 1 数据太大,内存完全放不下
OFF_HEAP 堆外内存 是 1 需要精确内存管理,避免GC
MEMORY_ONLY_2 内存 否 2 高可用,容忍单节点故障
MEMORY_AND_DISK_2 内存+磁盘 否 2 高可用+大容量
DISK_ONLY_2 磁盘 否 2 高可用+磁盘存储

核心原则:带 _SER 的级别会序列化数据,节省空间但增加CPU开销。带 _2 的级别会存两份副本,容错更好但存储翻倍。

2.2 三大主力级别深度对比

实际项目中,90%的场景只用3种:MEMORY_ONLY、MEMORY_AND_DISK、DISK_ONLY。我一个个说。

2.2.1 MEMORY_ONLY

这是默认级别。数据以Java对象形式存在内存里,读写最快。

df.cache()  // 等价于 MEMORY_ONLY
// 或者显式指定
df.persist(StorageLevel.MEMORY_ONLY())

优点:速度极快,没有序列化开销。

缺点:内存放不下就直接挂。不是溢写到磁盘,而是整个分区丢掉,下次用的时候重新算。

我曾经踩过的坑:有一次缓存一个10GB的DataFrame,集群内存总共16GB,我以为够用。结果Spark内部还有shuffle数据、广播变量,实际可用内存不到8GB。任务跑着跑着就OOM了。后来一查,MEMORY_ONLY模式下内存不够就直接丢数据,不会给你任何提示。

2.2.2 MEMORY_AND_DISK

这个级别就聪明多了。内存放不下的部分,自动溢写到磁盘。

df.persist(StorageLevel.MEMORY_AND_DISK())

它的工作流程是这样的:

  1. 优先把数据放内存
  2. 内存不够了,剩下的写磁盘
  3. 读取时,内存有的从内存读,磁盘有的从磁盘读

你想想看,这比MEMORY_ONLY安全多少倍?我现在的项目,只要数据量不确定,一律用MEMORY_AND_DISK。

个人建议:如果你不确定数据量,或者集群资源经常波动,直接用MEMORY_AND_DISK。虽然磁盘IO慢一点,但总比任务挂掉强。

2.2.3 DISK_ONLY

数据全部写磁盘。说实话,这个级别我用得最少。

df.persist(StorageLevel.DISK_ONLY())

什么时候用?数据大到内存完全放不下,但你又不想每次重新计算。比如一个100GB的中间结果,集群内存只有20GB,那就只能走磁盘。

但要注意——磁盘IO比内存慢几个数量级。我做过测试,同样数据量,DISK_ONLY比MEMORY_ONLY慢5-10倍。

2.3 缓存级别选择决策树

为了帮你快速决策,我画了一张图。每次选缓存级别时,按这个流程走一遍就行。

缓存级别选择决策树 数据需要缓存吗? 内存能放下全部数据? 是 否 MEMORY_ONLY 速度最快,默认选项 内存能放下一半? 是 否 MEMORY_AND_DISK 内存+磁盘,安全均衡 DISK_ONLY 全磁盘,慢但稳定 根据场景选择对应级别 💡 决策要点 • 数据量 < 内存60% → MEMORY_ONLY(留出余量给shuffle和广播) • 数据量 > 内存60% 但 < 内存120% → MEMORY_AND_DISK • 数据量 >> 内存 → DISK_ONLY 或重新分区 • 需要高可用 → 加 _2 后缀(如 MEMORY_AND_DISK_2)

2.4 序列化与副本:两个容易被忽略的选项

2.4.1 序列化(_SER)

带_SER的级别会把对象序列化成字节数组再存。好处是省内存,坏处是读写时多一次序列化/反序列化。

我做过一个测试:一个包含100个字段的DataFrame,不序列化占内存2.1GB,序列化后只有680MB。省了将近70%。

// 序列化版本
df.persist(StorageLevel.MEMORY_ONLY_SER())
// 或者用Kryo序列化(更快)
sparkConf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
df.persist(StorageLevel.MEMORY_ONLY_SER())

我的经验:如果数据对象很复杂(比如嵌套结构、大量字符串),序列化效果特别明显。但如果数据本身就很紧凑(比如全是数值类型),序列化省不了多少空间,反而增加CPU开销。

2.4.2 副本(_2)

带_2的级别会在两个不同节点上各存一份。说白了就是「RAID 1」的思路。

什么时候用?

  • 任务特别重要,不能因为一台机器挂掉就重算
  • 计算成本极高(比如一个迭代要跑2小时)
  • 集群稳定性差,经常有节点掉线

但代价也很明显——存储成本翻倍。我一般只在生产环境的核心链路用MEMORY_AND_DISK_2,开发测试环境从来不用。

2.5 实战选择建议

说了这么多,我直接给结论。你按这个优先级选:

  1. 默认首选:MEMORY_AND_DISK —— 安全、均衡、不出幺蛾子
  2. 内存充裕且追求极致性能:MEMORY_ONLY —— 但要确保内存足够,留30%余量
  3. 内存紧张但数据必须复用:MEMORY_AND_DISK_SER —— 序列化省内存
  4. 数据大到离谱:DISK_ONLY —— 慢但稳定,总比重新计算强
  5. 核心链路高可用:加_2后缀 —— 多一份保障

最后提醒一句:缓存不是银弹。如果你发现缓存后性能反而下降了,先检查是不是数据倾斜导致某些分区特别大,或者是不是缓存了根本不会复用的数据。我曾经见过有人把临时变量都cache了,结果内存被打满,任务比不缓存还慢。