执行数据量较大的spark任务时经常会出现MetadataFetchFailedException:
org.apache.spark.shuffle.MetadataFetchFailedException: Missing an output location for shuffle 10
at org.apache.spark.MapOutputTracker$$anonfun$convertMapStatuses$2.apply(MapOutputTracker.scala:867)
at org.apache.spark.MapOutputTracker$$anonfun$convertMapStatuses$2.apply(MapOutputTracker.scala:863)
at scala.collection.TraversableLike$WithFilter$$anonfun$foreach$1.apply(TraversableLike.scala:733)
at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:33)
at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:186)
原因分析:
shuffle分为shuffle write和shuffle read两部分。
shuffle write的分区数由上一阶段的RDD分区数控制,shuffle read的分区数则是由Spark提供的一些参数控制。
shuffle write可以简单理解为类似于saveAsLocalDiskFile的操作,将计算的中间结果按某种规则临时放到各个executor所在的本地磁盘上。
shuffle read的时候数据的分区数则是由spark提供的一些参数控制。可以想到的是,如果这个参数值设置的很小,同时shuffle read的量很大,那么将会导致一个task需要处理的数据非常大。结果导致JVM crash,从而导致取shuffle数据失败,同时executor也丢失了,看到Failed to connect to host的错误,也就是executor lost的意思。有时候即使不会导致JVM crash也会造成长时间的gc。
解决思路:
- 减少shuffle数据
主要从代码层面着手,可以将不必要的数据在shuffle前进行过滤,比如原始数据有20个字段,只要选取需要的字段进行处理即可,将会减少一定的shuffle数据。 - 修改分区
通过spark.sql.shuffle.partitions控制分区数,默认为200,根据shuffle的量以及计算的复杂度适当提高这个值,例如500。 - 增加失败的重试次数和重试的时间间隔
通过spark.shuffle.io.maxRetries控制重试次数,默认是3,可适当增加,例如10。
通过spark.shuffle.io.retryWait控制重试的时间间隔,默认是5s,可适当增加,例如10s。 - 提高executor的内存
在spark-submit提交任务时,适当提高executor的memory值,例如15G或者20G。 - 考虑是否存在数据倾斜的问题
自我实践总结
这么长时间感觉遇到这个问题都是发生数据倾斜造成,分享一次遇到的成功处理经验:
业务场景
在做学生知识点画像时,基础数据量大且热点数据多,遇到事实表关联(left join)维度表做数据处理,多条件关联时热点学校和热点知识点造成数据倾斜
解决方法
利用多主键(也就是left join条件),塑造唯一主键细化数据粒度,使得数据一一对应
示例
文字描述看不懂我们直接上代码
knowledge_level表(数据量在7000w+):
school_id | paper_id | knowledge_id | … |
---|---|---|---|
001 | 1001 | 100001 | xxxx |
002 | 1011 | 100001 | xxxx |
base_school_paper表(数据量在300w+):
school_id | paper_id | grade | position | subject |
---|---|---|---|---|
001 | 1001 | 一年级 | 中国山东 | 数学 |
002 | 1011 | 三年级 | 中国北京 | 物理 |
数据倾斜方案
发生数据倾斜的代码
select
school_id,
paper_id,
knowledge_id,
....
from knowledge_level knowledge
left join base_school_paper school
on knowledge.school_id = school.schoo_id
and knowledge.paper_id = school.paper_id
处理过后的代码
select
knowledge.school_id AS school_id,
knowledge.paper_id AS paper_id,
knowledge.knowledge_id AS knowledge_id,
....
from (
select
school_id,
paper_id,
knowledge_id,
concat(school_id,'_',paper_id) AS school_paper_id
...
from knowledge_level
) knowledge
left join (
select
school_id,
paper_id,
knowledge_id,
concat(school_id,'_',paper_id) AS school_paper_id
....
from base_school_paper
) school
on knowledge.school_paper_id = school.school_paper_id