1.控制shuffle reduce端缓冲大小以避免OOM
map端的task是不断的输出数据的,数据量可能是很大的。
但是,其实reduce端的task,并不是等到map端task将属于自己的那份数据全部写入磁盘文件之后,再去拉取的。map端写一点数据,reduce端task就会拉取一小部分数据,立即进行后面的聚合、算子函数的应用。
每次reduece能够拉取多少数据,就由buffer来决定。因为拉取过来的数据,都是先放在buffer中的。然后才用后面的executor分配的堆内存占比(0.2),hashmap,去进行后续的聚合、函数的执行。
reduce端缓冲(buffer),可能会出什么问题?
可能是会出现,默认是48MB,也许大多数时候,reduce端task一边拉取一边计算,不一定一直都会拉满48M的数据。可能大多数时候,拉取个10M数据,就计算掉了。
大多数时候,也许不会出现什么问题。但是有的时候,map端的数据量特别大,然后写出的速度特别快。reduce端所有task,拉取的时候,全部达到自己的缓冲的最大极限值,缓冲,48M,全部填满。
这个时候,再加上你的reduce端执行的聚合函数的代码,可能会创建大量的对象。也许,一下子,内存就撑不住了,就会OOM。reduce端的内存中,就会发生内存溢出的问题。
针对上述的可能出现的问题,我们该怎么来解决呢?
这个时候,就应该减少reduce端task缓冲的大小。我宁愿多拉取几次,但是每次同时能够拉取到reduce端每个task的数量,比较少,就不容易发生OOM内存溢出的问题。(比如,可以调节成12M)
在实际生产环境中,我们都是碰到过这种问题的。这是典型的以性能换执行的原理。reduce端缓冲小了,不容易OOM了,但是,性能一定是有所下降的,你要拉取的次数就多了。就走更多的网络传输开销。
这种时候,只能采取牺牲性能的方式了,spark作业,首先,第一要义,就是一定要让它可以跑起来。分享一个经验,曾经写过一个特别复杂的spark作业,写完代码以后,半个月之内,就是跑不起来,里面各种各样的问题,需要进行troubleshooting。调节了十几个参数,其中就包括这个reduce端缓冲的大小。总算作业可以跑起来了。
然后才去考虑性能的调优。
spark.reducer.maxSizeInFlight,48
spark.reducer.maxSizeInFlight,24
2.解决JVM GC导致的shuffle文件拉取失败
spark.shuffle.io.maxRetries 60 – 尝试重试拉取文件次数
spark.shuffle.io.retryWait 60s – 每次重试拉取文件间隔时长
3.解决YARN队列资源不足导致的application直接失败
①YARN,发现资源不足时,你的spark作业,并没有hang在那里,等待资源的分配,而是直接打印一行fail的log,直接就fail掉了。
②YARN,发现资源不足,你的spark作业,就hang在那里。一直等待之前的spark作业执行完,等待有资源分配给自己来执行。
解决方案:
(1)在你的J2EE(我们这个项目里面,spark作业的运行,之前说过了,J2EE平台触发的,执行spark-submit脚本),限制,同时只能提交一个spark作业到yarn上去执行,确保一个spark作业的资源肯定是有的。
(2)你应该采用一些简单的调度区分的方式,比如说,你有的spark作业可能是要长时间运行的,比如运行30分钟;有的spark作业,可能是短时间运行的,可能就运行2分钟。此时,都提交到一个队列上去,肯定不合适。很可能出现30分钟的作业卡住后面一大堆2分钟的作业。分队列,可以申请(跟你们的YARN、Hadoop运维的同学申请)。你自己给自己搞两个调度队列。每个队列的根据你要执行的作业的情况来设置。在你的J2EE程序里面,要判断,如果是长时间运行的作业,就干脆都提交到某一个固定的队列里面去把;如果是短时间运行的作业,就统一提交到另外一个队列里面去。这样,避免了长时间运行的作业,阻塞了短时间运行的作业。
(3)你的队列里面,无论何时,只会有一个作业在里面运行。那么此时,就应该用我们之前讲过的性能调优的手段,去将每个队列能承载的最大的资源,分配给你的每一个spark作业,比如80个executor;6G的内存;3个cpu core。尽量让你的spark作业每一次运行,都达到最满的资源使用率,最快的速度,最好的性能;并行度,240个cpu core,720个task。
(4)在J2EE中,通过线程池的方式(一个线程池对应一个资源队列),来实现上述我们说的方案。
4.解决各种序列化导致的报错
当spark作业在运行过程中报错,而且报错信息中含有Serializable等类似词汇,那么可能是序列化问题导致的报错。
解决方案:
(1)你的算子函数里面,如果使用到了外部的自定义类型的变量,那么此时,就要求你的自定义类型,必须是可序列化的。
(2)如果要将自定义的类型,作为RDD的元素类型,那么自定义的类型也必须是可以序列化的
(3)不能在上述两种情况下,去使用一些第三方的,不支持序列化的类型
5.解决算子函数返回NULL导致的问题
在一些算子函数里,需要我们有一个返回值,但是在一些情况下我们不希望有返回值,此时如果我们直接返回NULL,会报错,例如Scala.Math(NULL)异常
解决方案:
(1)返回特殊值,不返回NULL,例如"-1"
(2)在通过算子获取到了一个RDD之后,可以对这个RDD进行filter过滤,将数值为-1的数据给过滤掉
(3)在使用完filter算子后,继续调用coalesce算子进行优化
6.解决yarn-client模式导致的网卡流量激增问题
yarn-client原理图如下:
在yarn-client模式下,Driver启动在本地机器上,而Driver负责所有的任务调度,需要与yarn集群上的多个executor进行频繁的通信。
假设有100个executor,1000个task,那么每个executor分配到10个task之后,Driver要频繁的跟executor上运行的1000个task进行通信,通信数据非常多,并且通信品类特别高。这就导致有可能在spark任务运行过程中,由于频繁大量的网络通讯,本地机器的网卡流量激增。
注意,yarn-client模式只会在测试环境中使用,而之所以使用yarn-client模式,是由于可以看到详细全面的log信息,通过查看log,可以锁定程序中存在的问题,避免在生产环境下发生故障。
在生产环境下,使用的一定是yarn-cluster模式。在yarn-cluster模式下,就不会造成本地机器网卡流量激增问题,如果yarn-cluster模式下存在网络通信的问题,需要运维团队进行解决。
7.解决yarn-cluster模式的JVM栈内存溢出无法执行问题
yarn-cluster原理图如下:
当spark作业中包含SparkSQL的内容时,可能会碰到yarn-client模式下可以运行,但是yarn-cluster模式下无法提交运行(报出OOM错误)的情况
yarn-client模式下,Driver是运行在本地机器上的,spark使用的jvm的PermGen的配置,是本地机器上的spark-class文件,jvm永久代的大小是128M,这个是没有问题的,但是在yarn-cluster模式下,Driver运行在yarn集群的某个节点上,使用的是没有经过配置的默认配置,PermGen永久代大小为82M。
SparkSQL内部要进行很复杂的SQL的语义解析、语法树转换等等,非常复杂,如果sql语句本身就非常复杂,那么很有可能会导致性能损耗和内存占用,特别是对PermGen的占用会比较大。
所以,此时如果PermGen的占用好过了82M,但是又小于128M,就会出现yarn-client模式下可以运行,yarn-cluster模式下无法运行的情况。
解决上述问题的方法是增加PermGen的容量,需要在spark-submit脚本中对相关参数进行设置,设置方法:
--conf spark.driver.extraJavaOptions="-XX:PermSize=128M -XX:MaxPermSize=256M"
8.错误的持久化方式以及CheckPoint的使用
spark持久化在大部分情况下是没有问题,但是有时候数据会丢失,一旦数据丢失,就需要对丢失的数据重新进行计算,计算完后再缓存和使用,为了避免数据的丢失,可以选择对这个RDD进行CheckPoint,也就是将数据持久化一份到容错的文件系统上(比如HDFS)
一个RDD缓存并CheckPoint后,如果一旦发现缓存丢失,就会优先查看CheckPoint数据存不存在,如果有,就会使用CheckPoint数据,而不用重新计算。也就是说,CheckPoint可以视为cache的保障机制,如果cache失败,就使用CheckPoint的数据。
使用CheckPoint的优点在于提高了spark作业的可靠性,一旦缓存出现问题,不需要重新计算数据,缺点在于,CheckPoint时需要将数据写入到HDFS等文件系统,对性能的消耗较大。
使用方式:
①SparkContext,设置checkpoint目录
②对RDD执行checkpoint操作,和cache联合使用