一、控制shuffle reduce端缓冲大小以避免OOM
reduce端的task读取数据时,并不是等到map端task将属于自己的那份数据全部写入磁盘文件之后再去拉取,而是先把数据拉取进缓冲区,然后才用后面的executor分配的堆内存占比(比如0.2)来进行后续的聚合、函数的执行。reduce端缓存默认是48M。
①减小reduce端缓冲大小
当map端数据量比较大,并且写出的速度很快的时候,reduce端所有task拉取数据的时候,全部达到自己缓冲的最大值。同时,reduce端的函数在执行的时候,可能会创建大量的对象。这个时候,内存支撑不住,就会OOM内存溢出。
出现这种情况,应该减少reduce端task缓冲的大小,让task多拉取几次数据,但是每次拉取的数据量比较少,不容易发生OOM内存溢出。这样可能会是性能下降,但是可以保证程序的安全运行。在程序安全运行的前提下,再考虑性能调优。
②增加reduce端缓冲大小
如果map端输出的数据量不是很大,application的资源充足,可以尝试增加reduce端缓冲大小。使reduce端task每次拉取更多的数据,这样拉取的次数变少了,网络传输性能的开销减少。并且reduce端聚合操作进行的次数减少,使性能得到一定的提升。
③实现
SparkConf.set("spark.reducer.maxSizeInFlight","96")
二、JVM GC导致的shuffle文件拉取失败
①问题描述
有时候会出现一种非常普遍的情况,在spark作业中报...file not found...的异常,并且有时候重新提交作业之后不再报错。
这是因为下一个stage的task在去上一个stage生成的文件中拉取数据时,上一个stage的executor的JVM进程因为内存不足,发生GC。JVM一旦发生GC,就会导致executor内所有的工作线程全部停止工作,比如管理文件信息的BlockManager,基于netty的网络通信。
这时,下一个stage的executor可能还没有停止,task去上一个stage的task所在的executor拉取数据,结果发现对方在GC,拉取数据失败。过一会儿再次拉取数据,可能GC已经完成,拉取数据成功,不再报错。
②解决方案
调整两个参数:
spark.shuffle.io.maxRetries 3
spark.shuffle.io.retryWait 5s
第一个参数,表示shuffle文件拉取的时候,如果没拉取成功,可以重试几次,默认是3次。
第二个参数,表示每次重试拉取文件的时间间隔,默认是5s。
默认情况下,15秒内拉取不到数据则会报异常。
在生产环境中,我们可以根据具体情况适当调大这两个参数,比如
spark.shuffle.io.maxRetries 30
spark.shuffle.io.retryWait 60s
重试次数调整为30,时间间隔调整为60s,这样,文件拉取在30分钟之内完成都不会报错。
三、解决YARN队列资源不足导致的application直接失败
①问题描述
在基于yarn来提交spark作业时,由于队列的资源有限,可能是500G内存,240个cpu core。在spark application作业的spark-submit中配置了80个executor,每个executor4G内存,2个cpu core。那么这个application大概要消耗掉320G内存,160个cpu core。即便加上集群的其他消耗,队列资源也是足够的,比如一共消耗了400G内存,200个cpu core。
当提交另一个spark作业时,又需要申请320G内存,160个cpu core,这个时候,可能导致的结果有两个:
- YARN发现资源不足,spark作业没有卡在那里等待分配资源,而是直接打印一行fail的日志,直接fail掉
- YARN发现资源不足,spark作业卡在那里等待分配资源。一直等到之前的spark作业执行完成,有资源分配给自己来继续执行
如果是第二种结果,则不需要处理。
如果是第一种结果,可以采取如下的解决方案。
②解决方案
- 在J2EE中限制,同时只能提交一个spark作业到yarn上去执行,确保一个spark作业的资源是足够的。
- 采用一些简单的调度分区的方式。比如说,有的spark作业是要长时间运行的,有的spark作业只需要运行2分钟。如果都提交到一个队列上去,运行时间长的spark作业可能会阻塞运行时间短的作业。这个时候,可以分成两个队列,一个队列用来运行长时间运行的作业,一个队列运行短时间运行的作业。
至此,我们会发现,无论何时,队列里面只有一个作业在运行。那么此时,可以通过性能调优的手段,将作业运行的资源调到最大,是队列的资源得到最大化的利用。比如上面这个例子,每个executor分配6G内存、3个cpu core、并行度调整为720。
③实现
在J2EE中,可以通过线程池的方式(一个线程池对应一个资源队列),来实现上述的方案。
线程池的容量为1,每次只能有一个作业运行,其他的作业在后面排队。需要把作业放到哪个队列中运行,就把它放到对应的线程池中运行。
|
ExecutorService threadPool = Executors.newFixedThreadPool(1);
threadPool.submit(new Runnable() {
@Override public void run() {
}
}); |
四、解决各种序列化导致的报错
用client模式提交作业,观察本地打印出来的log文件,可能会出现类似于Serializable、Serialize等等字眼,说明遇到了序列化问题相关的报错。
关于序列化问题导致的报错,需要注意一下三点:
- 在算子函数中,如果使用到了外部自定义类型的变量,要求自定义类型必须是可序列化的
- 如果要将自定义的类型,作为RDD的元素类型,要求自定义的类型必须是可以序列化的
- 不能在上述两种情况下,使用一些第三方的不支持序列化的类型
五、解决算子函数返回NULL导致的问题
在算子函数中,需要有一个返回值。但是,有时候我们不需要有返回值,这个时候如果直接返回NULL的话,则会报错。
对于这种情况,可以参考一下解决办法:
- 在返回的时候,返回一些特殊的值(比如-999),不要直接返回NULL
- 在通过算子获取到一个RDD之后,可以对这个RDD执行filter操作,进行数据过滤。filter内部,可以对数据进行判定,如果是-999,则返回false,给过滤掉即可
- filter之后,可以使用coalesce算子压缩RDD的partition数量,让各个partition的数据比较紧凑一些,提升性能
六、解决yarn-client模式导致的网卡流量激增问题
①yarn-client模式的工作原理

②问题描述及原因分析
yarn-client模式下,driver启动在本地机器,全权负责所有任务的调度,跟yarn集群上运行的多个executor进行频繁的通信(中间有task的启动信息、task的执行统计信息、task的运行状态、shuffle的输出结果)。
比如有100个executor,10个stage,1000个task。每个stage运行的时候,都有1000个task提交到executor上面运行,平均每个executor有10个task。此时,driver要频繁地跟executor上运行的1000个task进行通信。通信消息特别多,频率特别高,运行完一个stage,接着运行下一个stage,又是频繁的通信。
整个spark作业运行的生命周期内,都会频繁地进行通信和调度。所有这一切的通信和调度都是从本地机器上发出和接收的。这段时间内,本地机器进行频繁的网络通信,导致本地机器的网络负载非常非常高,网卡流量激增。这种情况下,会对公司的网络或者其他的机器造成负面和恶劣的影响。
③解决方法
yarn-client模式,通常只会使用在测试环境(写好某个spark作业,打成一个jar包,在某台测试机器上,用yarn-client模式提交)。因为测试的行为是偶尔的,不会长时间连续提交大量的spark作业。但是通过yarn-client模式提交作业也有好处,可以在本地机器观察到详细全面的log,通过log解决线上报错的故障(troubleshooting)、对性能进行观察和调试。
实际上线后,在生产环境中,都用yarn-cluster模式提交spark作业。这种模式下,不是本地机器运行driver,是yarn集群中,某个节点会运行driver进程,负责task调度。
七、yarn-cluster模式的JVM栈内存溢出导致的无法执行作业问题
①yarn-cluster模式的工作原理

②问题描述及原因分析
有的时候可能会遇到这种情况:运行的spark作业中包含spark sql语句,在yarn-client模式下可以正常运行,但是在yarn-cluster模式下无法正常运行,报出JVM的PermGen(永久代)内存溢出的OOM。
yarn-client模式下,driver运行在本地机器上,spark使用的JVM的PermGen的配置,是本地的spark-class文件(spark客户端有默认配置),JVM永久代的大小默认是128M,运行spark作业没有问题;但是在yarn-cluster模式下,driver是运行在yarn集群中某个节点上的,使用的是默认配置,82M。
spark sql的内部要进行很复杂的SQL语义解析、语法树的转换等等。这种情况下,sql可能会造成性能和内存的消耗,对永久代PermGen的占用比较大。
此时,如果对永久代PermGen的占用需求,超过82M而小于128M的话,可能就会出现上述的问题。
③解决方法
在yarn-cluster模式下,给driver的PermGen设置大一些。
在spark-submit脚本中,加入以下配置:
|
--conf spark.driver.extraJavaOptions="-XX:PermSize=128M -XX:MaxPermSize=256M" |
永久代PermGen最小为128M,最大为256M。基本可以保证spark作业不会出现上述的问题。
④spark sql需要注意的问题
如果spark sql中有大量的or语句,达到成百上千的时候,可能会导致driver端的jvm stack overflow,JVM栈内存溢出的问题。
这是由于调用的方法层级过多,产生大量的、非常深的、超出了JVM栈深度限制的递归。(猜测,spark sql有大量的or语句时,spark sql内部源码中,在解析sql,比如转换成语法树,或者进行执行计划的生成时,对or的处理是递归)
这种情况下,建议不要写特别复杂的spark sql语句,而是采用替代方案:将一条sql语句,拆解成多条sql语句来执行。每条sql语句的or语句不超过100个。通常情况下,可以解决上述问题。
八、错误的持久化方式以及checkpoint的使用
①错误的持久化方式
|
ordersRDD.cache() ordersRDD.count() ordersRDD.take() |
上面这种方式,在运行的时候会报错,比如file not found
正确的使用方式如下:
|
ordersRDD ordersRDD = ordersRDD.cache() & val cachedOrdersRDD = ordersRDD.cache() |
②checkpoint的使用

持久化,大多数情况下可以正常工作。但有的时候可能会出现意外。比如缓存在内存中的数据莫名其妙地丢失、文件被误删除,等等。这个时候,如果要对这个RDD执行某些操作,发现RDD的某个partition找不到,需要重新计算这个RDD。而重新计算RDD,可能及其耗费时间和资源。
这种情况下,可以选择对这个RDD进行checkpoint,持久化一份到容错的文件系统中(比如hdfs)。在对这个RDD进行计算的时候,如果发现这个RDD的缓存数据不见了,优先找一个有没有checkpoint数据,有的话,直接拿过来,不需要再次进行计算。
当然,checkpoint有利也有弊。利在于提高了spark作业的可靠性,缓存数据丢失时,不需要进行大量的计算;弊在于进行checkpoint操作将数据写入文件系统中的时候,需要消耗部分性能。所以,checkpoint是用性能换可靠性。
checkpoint的使用:
- SparkContext,设置checkpoint目录——sc.checkpointFile("xxxxxx");
- 对RDD执行checkpoint操作——xxxRDD.checkpoint;
本文深入探讨Spark性能调优的七大关键策略,包括控制shuffle reduce端缓冲大小避免OOM、解决JVMGC导致的shuffle文件拉取失败、解决YARN队列资源不足导致的application直接失败等问题,提供详实的解决方案和代码示例。
2186

被折叠的 条评论
为什么被折叠?



