经常会碰到一种现象:只有少数 Executor 在运行,别的 Executor 长时间空闲。这种现象比较常见的原因是数据的分区比较少,可以使用 repartition 来提高并行度。
另外一种原因和数据的本地性有关,请看下面的例子:
用户的任务申请了 100 个 executors,每个 executor 的 cores 为 6,那么最多会有 600 个任务同时在运行,刚开始是 600 个任务在运行,接着正在运行的任务越来越少,只剩下 78 个任务在运行。
这个问题会导致 Spark 基于 yarn 的动态分配功能也无法使用了,Executor 长时间空闲之后会被杀死,然后报一大堆让人心烦的 Error 信息。
先回顾一下 Spark 作业提交的流程,如下图所示:
1、首先 DAGSchedular 会把作业划分成多个 Stage,划分的依据:是否需要进行 shuffle 操作。
2、每个 Stage 由很多的 Tasks 组成,Tasks 的数量由这个 Stage 的 partition 数决定。Stage 之间可能有依赖关系,先提交没有前置依赖的 Stage。把 Stage 里的任务包装成一个 TaskSet,交给 TaskScheduler 提交。
3、把 Task 发送给 Executor,让 Executor 执行 Task。
这个问题是出在第二步,TaskScheduler 是怎么提交任务的。这块的逻辑主要是在 CoarseGrainedSchedulerBackend 和 TaskSchedulerImpl。
下面是 CoarseGrainedSchedulerBackend 里面的 makeOffer 方法的主要逻辑:
CoarseGrainedSchedulerBackend 筛选出来活跃的 Executors,交给 TaskSchedulerImpl。
TaskSchedulerImpl 返回一批 Task 描述给 CoarseGrainedSchedulerBackend。
序列化之后的任务的大小没有超过 spark.akka.frameSize 就向 Executor 发送该任务。
问题是出在第二步,根据活跃的 Executors,返回可以执行的 Tasks。具体查看 TaskSchedulerImpl 的 resourceOffers 方法。
1、在内存当中记录传入的 Executor 的映射关系,记录是否有新的机器加入。
2、如果有新的机器加入,要对所有的 TaskSetManager 重新计算本地性。
3、遍历所有的 TaskSetManager,根据 TaskSetManager 计算得出的任务的本地性来分配任务。
分配任务的优先级:
1)同一个 Executor
2)同一个节点
3)没有优先节点
4)同一个机架
5)任务节点
如果上一个优先级的任务的最后发布时间不满足下面这个条件,任务将不会被分发出去,导致出现上面的现象。
判断条件是:curTime - lastLaunchTime >= localityWaits(currentLocalityIndex)
这样设计的初衷是好的,希望先让本地性更好的任务先运行,但是这里没有考虑到 Executor 的空闲时间以及每个 Task 的空闲时间。跳过了这个限制之后,它还是会按照优先级来分配任务的,所以不用担心本地性的问题。
下面这几个参数在官方的配置介绍当中有,但是没介绍清楚,默认都是 3 秒,减小这几个参数就可以绕过限制了。
spark.locality.wait.process 1ms # 超过这个时间,可以执行 NODE_LOCAL 的任务
spark.locality.wait.node 3ms # 超过这个时间,可以执行 RACK_LOCAL 的任务
spark.locality.wait.rack 1s # 超过这个时间,可以执行 ANY 的任务
实践测试,问题解决了,并且速度快了 20%以上。