关于Spark Cache
最近有一个用户抱怨很简单的spark作业运行了很长时间,检查发现用户把所有的中间dataset都cache了一遍,在建议用户去掉不必要的cache后,打算对spark的作业执行过程中的cache进行一个深入调研,进而进一步优化我们的服务。脑海中初步有一个思路如下图所示,
- 对Union节点进行cache, 因为union节点在执行的时候被Agg节点和Join节点分别引用两次
- Agg节点和Join节点执行之后,对Union节点进行uncache.
Cache, Persist和Checkpoint
在开始做调研和实验之前,先简单介绍下Spark的cache, persist和hcheckpoint的区别与联系:
- persist, RDD持久化的,将RDD以及RDD的血统(记录了这个RDD如何产生)缓存到内存中,当缓存的RDD失效的时候(如内存损坏),它们可以通过血统重新计算来进行恢复。persist有多个存储级别,存储级别举例:
- Memory_Only:只存于内存
- Disk_Only:只存于磁盘
- Memory_and_disk:内存不够,存于磁盘
- Memory_only_ser:将数据以序列化的方式存于内存
- cache是persist的一种,值得注意的是dataset和RDD的cache是有区别的
spark中DataFrame或Dataset里的
cache()方法默认存储等级为MEMORY_AND_DISK,这跟RDD.cache()的存储等级MEMORY_ONLY是不一样的 - 而checkpoint主要强调将数据做物理存储的(本地磁盘或HDFS上), 这三者都是做RDD持久化的,cache()和persist()是将数据默认缓存在内存中,checkpoint()是将数据做物理存储的(本地磁盘或Hdfs上),checkpoint 将 RDD 缓存到了 HDFS 中,同时忽略了它的血统(也就是RDD之前的那些依赖)。为什么要丢掉依赖?因为可以利用 HDFS 多副本特性保证容错,因此checkpoint后的RDD可以被其它应用程序读到,而persist的缓存则会在作业中断后消失。
什么时候应该CACHE?
市面上关于什么时候该用cache的文章很少,即使Spark官网是这样描述的
Spark also supports pulling data sets into a cluster-wide in-memory cache. This is very useful when data is accessed repeatedly, such as when querying a small “hot” dataset or when running an iterative algorithm like PageRank
其中提到了两个关键词“small”和"hot",话不多说,开始实验验证: 实验1: cache所有共享的(被引用两次及两次以上)dataset 实验2: 只cache数据比较小且共享的dataset 实验3: 跨action共享 实验4: 不cache
每组实验做十次,分别在简单的作业和复杂的作业上执行,实验现象: 实验1:
实验结果,
- 对于简单的ETL作业,执行时间:实验3 < 实验1 ~ 实验2 < 实验4
- 对于负责的作业,实验3 < 实验2 < 实验4< 实验1
原因分析:大量cache会占用spark内存,导致存储内存不够,缓存加到disk中,查阅executor日志验证:
Added rdd_xx_x on disk on ...
添加缓存本身的时间甚至可能超过重新计算的时间。
什么时候应该清除CACHE?
实验1: cache数据比较小且共享的dataset,不做为何unpersist 实验2: 孩子节点执行后unpersist
结果:实验1优于实验2 分析2: 实验2的思路本身有问题,由于spark cache和unpersist都是懒加载,都是在actio操作的时候执行,cache+unpersist相当于没有做cache. 查阅官网文档后发现,当内存不足时,Spark会基于LRU算法,自动清理cache,因此结论是不需要我们手动清除CACHE.
Spark automatically monitors cache usage on each node and drops out old data partitions in a least-recently-used (LRU) fashion. If you would like to manually remove an RDD instead of waiting for it to fall out of the cache, use the `RDD.unpersist()` method. Note that this method does not block by default. To block until resources are freed, specify `blocking=true` when calling this method.