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())
它的工作流程是这样的:
- 优先把数据放内存
- 内存不够了,剩下的写磁盘
- 读取时,内存有的从内存读,磁盘有的从磁盘读
你想想看,这比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 缓存级别选择决策树
为了帮你快速决策,我画了一张图。每次选缓存级别时,按这个流程走一遍就行。
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 实战选择建议
说了这么多,我直接给结论。你按这个优先级选:
- 默认首选:MEMORY_AND_DISK —— 安全、均衡、不出幺蛾子
- 内存充裕且追求极致性能:MEMORY_ONLY —— 但要确保内存足够,留30%余量
- 内存紧张但数据必须复用:MEMORY_AND_DISK_SER —— 序列化省内存
- 数据大到离谱:DISK_ONLY —— 慢但稳定,总比重新计算强
- 核心链路高可用:加_2后缀 —— 多一份保障
最后提醒一句:缓存不是银弹。如果你发现缓存后性能反而下降了,先检查是不是数据倾斜导致某些分区特别大,或者是不是缓存了根本不会复用的数据。我曾经见过有人把临时变量都cache了,结果内存被打满,任务比不缓存还慢。