背景
无论是Spark on Yarn还是Spark on k8s场景。当我们有了一个资源队列(资源池)时,如何划分资源能够让任务执行的更高效,是我们每个人都会遇到的问题。
Spark Application的组成,及各自角色
Spark Application是由1个driver和多个Executor来组成的。Driver 负责协调 Spark 作业的执行,调度任务,并且负责将作业分解为多个 Stage 和 Task。它还负责维护整个应用程序的状态。 Executor 是实际执行任务的工作节点。每个 Executor 可以并行处理多个任务,并且在任务执行过程中处理数据的计算。
根据driver和Executor在计算任务时所扮演的角色。我们就更能知道资源调整方向。
Spark资源相关参数
基本参数
参数名 | 含义 | 默认值 |
---|---|---|
spark.executor.instances | executor的个数 | 2 |
spark.executor.cores | 单个executor核数 | 1 |
spark.executor.memory | 单个executor的内存 | 1G |
spark.driver.cores | driver的核数 | 1 |
spark.driver.memory | driver的内存 | 1G |
高级参数
参数名 | 含义 | 默认值 |
---|---|---|
spark.executor.memoryOverhead | executor堆外内存 | 384 |
spark.driver.memoryOverhead | driver堆外内存 | 384 |
spark.memory.fraction | 该参数设置了Spark应用程序中可用于执行和存储数据的堆内存比例。默认值为0.6,即60%的堆内存将被分配给Spark的执行和存储数据结构。 | 0.6 |
spark.memory.storageFraction | 该参数用于定义在Spark的总内存中,专门用于存储数据的内存比例。具体来说,这个参数控制了可用于缓存RDD、DataFrame以及其他数据结构的内存量。 | 0.5 |
spark.driver.memoryOverheadFactor | 该参数设置了dirver堆外内存和堆内内存的比例,如果设置了spark.driver.memoryOverhead参数则本参数不生效 | 0.10 |
spark.executor.memoryOverheadFactor | 该参数设置了executor堆外内存和堆内内存的比例,如果设置了spark.executo.memoryOverhead参数则本参数不生效 | 0.10 |
PS. 以上参数详见官方文档:
https://spark.apache.org/docs/latest/configuration.html
Spark资源调优策略
确定Driver
一般来说,由于driver在sc中的分工,所以不需要很大核数,一般1核就够。内存一般默认1G就够用,只有在进行collect,toPandas等需要将executor数据回写到driver才会容易导致oom,需要调大driver内存,或者优化代码逻辑。所以这里给driver分配1个cpu,1GB内存,堆外内存默认384MB一般不需要修改。
确定Executor
核数和内存的比例
剩下的资源都留给executor,Spark官方给出的内存cpu比例是2:1-4:1,也就是每个cpu对应2-4G内存。这个比例确定后,我们通过cpu就可以得到对应的内存。
使用多个 Executors 但每个 Executor 的核心数较小,还是使用较少的 Executors 但每个 Executor 的核心数较大?
同样的cpu我们可以少executor,每个executor多cpu。或者多executor每个executor少cpu。举个例子来说,有10个cpu,我们可以起10个executor,每个executor 1核。也可以1个executor,每个executor10核。当然对应的内存是按照比例分配的,也会影响单个Executor内存大小。
所以区别主要在以下三个点:executor个数,单个executor核心/内存
我们可以从以下几个方面来分析:
任务的性质:计算密集型 vs I/O 密集型
计算密集型任务
如果任务主要是计算(例如复杂的数学运算或数据转换),则使用较少的 Executors 但每个 Executor 有更多的核心可能会更高效。这是因为任务可以更好地利用 CPU 计算资源。
I/O 密集型任务
如果任务涉及大量的 I/O 操作(包括磁盘I/O:读取和写入数据;网络I/O:shuffle),使用更多的 Executors 可能更有利。更多的 Executors 可以并行处理 I/O 操作,从而减少整体的等待时间。
如何判定是计算密集型还是I/O密集型?
可以根据Spark宽窄依赖,如果执行的任务(job)窄依赖比较多,可以说明不需要跨executor操作,executor内部用本地数据交换即可完成,也就是计算密集型任务,所以单个Executor多核心数会比较合适。如果宽依赖比较多,说明经常需要executor回传数据到driver,也就是I/O密集型任务,所以多个Executor,单个Executor核心数少比较合适。
拓展:如何判断宽窄依赖?
- 可以通过编写代码时的算子,通过行动算子和转换算子来判断。
- 如果不熟悉代码编写逻辑,或者是直接执行的sql,由spark来进行转换操作,那可以看SparkUI,进入stage,查看DAG图,都执行了哪些操作,进行进一步分析。
任务的并行度
Spark 的并行度是由任务的数量和可用的 Executors 以及每个 Executor 的核心数共同决定的。增加 Executors 的数量可以提高任务的并行度,从而缩短整体处理时间。
设置合理的并行度:如果任务的并行度较高,使用多个 Executors(即使每个核心数较小)通常会更好,因为这样可以充分利用集群的资源。可以通过设置 spark.default.parallelism 来调整并行度。
资源利用率
使用多个 Executors 可能会提高资源利用率,尤其是在集群中有其他作业并发运行时。如果 Executors 数量较少,可能会导致部分核心闲置,从而影响整体资源利用效率。
任务调度
更多的 Executors 可以减少调度延迟。在 Spark 中,每个 Task 是通过 Executors 执行的,多个 Executors 可以同时处理多个 Task,从而提高任务调度的效率。
内存管理
内存使用:每个 Executor 使用的内存量也会影响性能。如果您选择使用多个 Executors,每个 Executor 的内存可能会较少,这可能会导致中间数据的频繁溢出到磁盘,降低性能。
内存开销:每个 Executor 都会有一些内存开销(如 JVM 的管理开销),多个 Executors 会增加总体内存开销,而少数 Executors 则可以更好地共享内存资源。
总结
- 选择多个 Executors(核心数小):如果任务是 I/O 密集型,或者需要更高的并行度、资源利用率和较低的调度延迟,选择多个 Executors 更有利。
- 选择少数 Executors(核心数大):如果任务是计算密集型,且能够合理利用更多的计算资源,选择少数 Executors 但每个 Executor 的核心数较大可能更有效。
以上只是理论上的分析和调优方向,实际生产中,通过理论得到第一版方案,进行试验,通过监控找到瓶颈,分析瓶颈并进一步试验。是一个迭代的过程,通过试验和监控来评估不同配置下的性能表现,从而优化资源配置。毕竟实践是检验真理的唯一标准。