大数据工程师面试题 - Spark 基础调优(二)
原则三:对多次使用的RDD进行持久化
当你在Spark代码中多次对一个RDD做了算子操作后,恭喜,你已经实现Spark作业第一步的优化了,也就是尽可能复用RDD。此时就该在这个基础之上,进行第二步优化了,也就是要保证对一个RDD执行多次算子操作时,这个RDD本身仅仅被计算一次。
Spark中对于一个RDD执行多次算子的默认原理是这样的:每次你对一个RDD执行一个算子操作时,都会重新从源头处计算一遍,计算出那个RDD来,然后再对这个RDD执行你的算子操作。这种方式的性能是很差的。
因此对于这种情况,我们的建议是:对多次使用的RDD进行持久化。此时Spark就会根据你的持久化策略,将RDD中的数据保存到内存或者磁盘中。以后每次对这个RDD进行算子操作时,都会直接从内存或磁盘中提取持久化的RDD数据,然后执行算子,而不会从源头处重新计算一遍这个RDD,再执行算子操作。
对多次使用的RDD进行持久化的代码示例
// 如果要对一个RDD进行持久化,只要对这个RDD调用cache()和persist()即可。 // 正确的做法。 // cache()方法表示:使用非序列化的方式将RDD中的数据全部尝试持久化到内存中。 // 此时再对rdd1执行两次算子操作时,只有在第一次执行map算子时,才会将这个rdd1从源头处计算一次。 // 第二次执行reduce算子时,就会直接从内存中提取数据进行计算,不会重复计算一个rdd。 val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").cache() rdd1.map(...) rdd1.reduce(...) // persist()方法表示:手动选择持久化级别,并使用指定的方式进行持久化。 // 比如说,StorageLevel.MEMORY_AND_DISK_SER表示,内存充足时优先持久化到内存中,内存不充足时持久化到磁盘文件中。 // 而且其中的_SER后缀表示,使用序列化的方式来保存RDD数据,此时RDD中的每个partition都会序列化成一个大的字节数组,然后再持久化到内存或磁盘中。 // 序列化的方式可以减少持久化的数据对内存/磁盘的占用量,进而避免内存被持久化数据占用过多,从而发生频繁GC。 val rdd1 = sc.textFile("hdfs://192.168.0.1:9000/hello.txt").persist(StorageLevel.MEMORY_AND_DISK_SER) rdd1.map(...) rdd1.reduce(...)
对于persist()方法而言,我们可以根据不同的业务场景选择不同的持久化级别。
Spark的持久化级别:
- MEMORY_ONLY:这种策略使用未序列化的Java对象格式将数据存储在内存中。如果内存不足以保存所有数据,那么这些数据可能无法持久化。这就意味着,后续对RDD的操作会需要从数据源头重新计算这部分数据。当我们使用cache()方法时,实际上就是采用了这种持久化策略。
- MEMORY_AND_DISK: 这种策略也是使用未序列化的Java对象格式,但它会优先将数据存储在内存中。如果内存不足,则会将数据写入磁盘。因此,后续对RDD的操作需要从磁盘中读取这部分数据。
- MEMORY_ONLY_SER: 这种策略与MEMORY_ONLY大致相同,区别仅在于它会将RDD的数据序列化。这就意味着RDD的每个分区都将被序列化成一个字节数组。这种方式更为节省内存,因此可以避免因数据持久化导致的频繁GC。
- MEMORY_AND_DISK_SER: 这种策略与MEMORY_AND_DISK基本一致,区别仅在于它也会将RDD的数据序列化成一个字节数组。这同样是为了节省内存,并避免频繁的GC操作。
- DISK_ONLY: 这种策略全部依赖于未序列化的Java对象格式,将所有数据写入磁盘文件中。
- MEMORY_ONLY_2, MEMORY_AND_DISK_2, 等: 对于以上任一持久化策略,如果加上后缀"_2",则意味着每个持久化的数据都会复制一份副本,并将副本保存到其他节点上。这种基于副本的持久化策略主要用于故障恢复。也就是说,如果某个节点失效,我们仍可以使用其他节点上的数据副本进行RDD计算,否则就只能从数据源头重新计算了。
如何选择一种最合适的持久化策略
以下是这段话的重写:
- 在所有的情况中,最高效的策略一般是MEMORY_ONLY。但这需要内存有足够的空间轻松存储整个RDD的所有数据。这样可以回避序列化和反序列化操作,减少性能损耗。对RDD的后续操作都会在内存中进行,无需将数据从磁盘中读取,大大提升了性能。此外,也无需复制数据并远程传输到其他节点。需要注意的是,在实际的生产环境中,直接使用这种策略的情景可能比较有限。当RDD中的数据量非常大(比如几十亿)时,直接使用这种持久化级别可能导致Java虚拟机内存溢出(OOM)。
- 当在使用MEMORY_ONLY策略时出现了内存溢出,可以尝试使用MEMORY_ONLY_SER策略。该策略会将序列化后的RDD数据存储在内存中,每个分区只是一个字节数组,大大节省了对象数量和内存使用。这种策略增加了序列化和反序列化的性能损耗,但后续操作能够在内存中完成,因此整体性能仍然很高。然而,同样需要注意的是,RDD中的数据过多可能还会导致内存溢出。
- 如果不能使用纯内存的策略,建议使用MEMORY_AND_DISK_SER策略,而不是MEMORY_AND_DISK。这一步意味着RDD的数据量过大,内存无法完全编写所有的数据。序列化的数据更加节约内存和磁盘空间。此外,这种策略会优先尝试将数据缓存在内存中,只有在内存不足的情况下才会将数据写入磁盘。
- 通常情况下,不建议使用DISK_ONLY和带有“_2”后缀的策略。因为完全基于磁盘进行数据的读写会导致性能急剧降低,有些情况下,不如重新计算所有RDD的结果。带有“_2”后缀的策略需要将所有数据复制一份,并发送到其他节点,可能会导致较大的性能开销。除非是对作业的可靠性有严格要求,否则无需使用这两种策略。
大家好,我是大数据欧老师,就职于互联网某头部大厂,超过 8 年的大数据从业经历。如果你有面试大数据工程师的打算,欢迎找我聊一聊!
#大数据##大数据工程师##大数据知识体系##大数据面试##大数据面经#大数据欧老师 - 面试真题分享 文章被收录于专栏
解决职场真实面试问题,分享同学真实成功案例,欢迎订阅关注!