场景

执行一个spark任务后,发现控制台一直报错如下:

set spark 内存_bc

21/04/21 10:32:29 ERROR cluster.YarnScheduler: Lost executor 3 on cdh-slave1.test.com: Container killed by YARN for exceeding memory limits.  5.5 GB of 5.5 GB physical memory used. Consider boosting spark.yarn.executor.memoryOverhead or disabling yarn.nodemanager.vmem-check-enabled because of YARN-4714.
21/04/21 10:32:29 ERROR client.TransportResponseHandler: Still have 1 requests outstanding when connection from /172.16.4.156:51552 is closed
21/04/21 10:32:31 ERROR cluster.YarnScheduler: Lost executor 2 on cdh-slave3.test.com: Container killed by YARN for exceeding memory limits.  5.5 GB of 5.5 GB physical memory used. Consider boosting spark.yarn.executor.memoryOverhead or disabling yarn.nodemanager.vmem-check-enabled because of YARN-4714.



执行脚本如下:

set spark 内存_spark_02

第一反应内存不够了,于是加executor内存:

--executor-memory 6g

重新执行后报错:

set spark 内存_数据_03

报错不一样,这次是单个executor因为超过yarn容器限制导致创建失败

查看yarn配置:

set spark 内存_set spark 内存_04

单个容器内存最大6G,总所周知:spark任务单个executor所需内存计算规则:

executorMemory + MAX(executorMemory * 0.10, 384m)

所以单个executor需要6144M+614M的内存,所以单个executor最大只能配置5G内存。

由于是测试环境,内存有限,yarn总共最大24G资源,且还有其他小伙伴在用集群,硬件限制的死死的。

于是先去找领导申请资源,得到的回复是先做优化,正式环境到时候可以适当的加内存。

中国有句古话说的好:在绝对的大内存面前,任何的代码优化都显得花里胡哨。

那只有整点花里胡哨的了,在有限的资源上做优化,目标12G到16G内存之间能支棱起来,时间也不能慢。



又回到原点,修改executor内存5G,还是报最原始的错误。

于是发现事情并不简单。需要仔细排查一下了。

报错还很贴心的提示了让我扩大spark.yarn.executor.memoryOverhead或者禁用yarn虚拟内存检测。

扩大spark.yarn.executor.memoryOverhead

set spark 内存_bc_05

解决问题,这次正常



性能优化

数据:输入2500W的ADB数据,输出1500WADB数据

硬件:12核,20G内存

当前程序跑完时间:2h

领导说时间太长了,要优化

那就需要研读代码了(我也只是个接盘侠)

查看Spark UI

set spark 内存_数据_06

发现最后这个Stage切分成了40000多个task

点进去这个Stage查看task:

set spark 内存_spark_07

居然很多task都是处于空跑的状态

排查代码,一个个的debug查看各个DataFrame的partition

最终定位到其中一个DataFrame数据量很少,但是被分了200个分区,导致再和其他DataFrame做JOIN的时候会扩大成更多的分区,但很多分区内数据都是空的。

解决办法:缩小当前DataFrame的分区数

继续定位其他可优化点:

set spark 内存_数据_08

发现很多stage中只有一个分区,点进去其中一个看DAG中分区情况

set spark 内存_set spark 内存_09

发现读取jdbc数据就被分为默认1个分区

结合executors页面查看

set spark 内存_数据_10

妥妥的数据倾斜了

解决方法:修改jdbc读取ADB的分区数

val modelDataSql = "select pay_time, ...   "
    
    //根据时间分区
    val partitionClause = Array(
                            "EXTRACT(month FROM pay_time) = 1 "
                          , "EXTRACT(month FROM pay_time) = 2 "
                          , "EXTRACT(month FROM pay_time) = 3 "
                          , "EXTRACT(month FROM pay_time) = 4 "
                          , "EXTRACT(month FROM pay_time) = 5 "
                          , "EXTRACT(month FROM pay_time) = 6 "
                          , "EXTRACT(month FROM pay_time) = 7 "
                          , "EXTRACT(month FROM pay_time) = 8 "
                          , "EXTRACT(month FROM pay_time) = 9 "
                          , "EXTRACT(month FROM pay_time) = 10 "
                          , "EXTRACT(month FROM pay_time) = 11 "
                          , "EXTRACT(month FROM pay_time) = 12 "
            )

    sparkSession.read.jdbc(adbProperties.getProperty("url"), s" ( $modelDataSql ) T", partitionClause, adbProperties)




优化后:

set spark 内存_spark_11

执行时间:10min

破案

前前后后四处定位问题,花了差不多三四天时间,终于解决了

总结:spark分区数,该大时就要大,该小时就要小