2022-02-24-Spark-44(性能调优通用调优)

1. 应用开发的原则

  1. 原则一:坐享其成
    我们应该尽可能地充分利用 Spark 为我们提供的“性能红利”,如钨丝计划、AQE、SQL functions 等等。
    AQE 可以让 Spark 在运行时的不同阶段,结合实时的运行时状态,周期性地动态调整前面的逻辑计划,然后根据再优化的逻辑计划,重新选定最优的物理计划,从而调整运行时后续阶段的执行方式。AQE 功能默认是关闭的,如果我们想要充分利用自动分区合并、自动数据倾斜处理和 Join 策略调整,需要把相关的配置项打开


    AQE
  2. 原则二:能省则省、能拖则拖
    尽量把能节省数据扫描量和数据处理量的操作往前推;尽力消灭掉 Shuffle,省去数据落盘与分发的开销;如果不能干掉 Shuffle,尽可能地把涉及 Shuffle 的操作拖到最后去执行。

  3. 原则三:跳出单机思维模式
    比如忽视实例化 Util 操作的行为还有很多,比如在循环语句中反复访问 RDD,用临时变量缓存数据转换的中间结果等等,单机思维模式会让开发者在分布式环境中,无意识地引入巨大的计算开销。

2. 配置项速查手册

计算负载主要由 Executors 承担,Driver 主要负责分布式调度,调优空间有限,因此对 Driver 端的配置项我们不作考虑

配置项的分类
  1. 首先,硬件资源类包含的是与 CPU、内存、磁盘有关的配置项
  2. 其次,Shuffle 类是专门针对 Shuffle 操作的。
  3. 最后,Spark SQL 早已演化为新一代的底层优化引擎。
哪些配置项与CPU设置有关?

通过如下参数就可以明确有多少 CPU 资源被划拨给 Spark 用于分布式计算。
spark.cores.max 集群
spark.executor.cores Executor
spark.task.cpus 计算任务

并行度
spark.default.parallelism 并行度
spark.sql.shuffle.partitions 用于明确指定数据关联或聚合操作中 Reduce 端的分区数量。

与CPU有关的配置项
哪些配置项与内存设置有关?
与内存有关的配置项

在平衡 Execution memory 与 Storage memory 的时候,如果 RDD 缓存是刚需,我们就把 spark.memory.storageFraction 调大,并且在应用中优先把缓存灌满,再把计算逻辑应用在缓存数据之上。除此之外,我们还可以同时调整 spark.rdd.compress 和 spark.memory.storageFraction 来缓和 Full GC 的冲击

哪些配置项与磁盘设置有关?

spark.local.dir 这个配置项,这个参数允许开发者设置磁盘目录,该目录用于存储 RDD cache 落盘数据块和 Shuffle 中间文件。如果你的经费比较充裕,有条件在计算节点中配备足量的 SSD 存储,甚至是更多的内存资源,完全可以把 SSD 上的文件系统目录,或是内存文件系统添加到 spark.local.dir 配置项中去,从而提供更好的 I/O 性能。

Shuffle 类配置项

Configuration - Spark 3.2.1

缓冲区相关配置

首先,在 Map 阶段,计算结果会以中间文件的形式被写入到磁盘文件系统。同时,为了避免频繁的 I/O 操作,Spark 会把中间文件存储到写缓冲区(Write Buffer)。这个时候,我们可以通过设置 spark.shuffle.file.buffer 来扩大写缓冲区的大小,缓冲区越大,能够缓存的落盘数据越多,Spark 需要刷盘的次数就越少,I/O 效率也就能得到整体的提升。
其次,在 Reduce 阶段,因为 Spark 会通过网络从不同节点的磁盘中拉取中间文件,它们又会以数据块的形式暂存到计算节点的读缓冲区(Read Buffer)。缓冲区越大,可以暂存的数据块越多,在数据总量不变的情况下,拉取数据所需的网络请求次数越少,单次请求的网络吞吐越高,网络 I/O 的效率也就越高。这个时候,我们就可以通过 spark.reducer.maxSizeInFlight 配置项控制 Reduce 端缓冲区大小,来调节 Shuffle 过程中的网络负载。

Reduce端相关配置项

自 1.6 版本之后,Spark 统一采用 Sort shuffle manager 来管理 Shuffle 操作,在 Sort shuffle manager 的管理机制下,无论计算结果本身是否需要排序,Shuffle 计算过程在 Map 阶段和 Reduce 阶段都会引入排序操作。
在不需要聚合,也不需要排序的计算场景中,我们就可以通过设置 spark.shuffle.sort.bypassMergeThreshold 的参数,来改变 Reduce 端的并行度(默认值是 200)。当 Reduce 端的分区数小于这个设置值的时候,我们就能避免 Shuffle 在计算过程引入排序。

Spark SQL 大类配置项

Spark SQL 下面的配置项还是蛮多的,其中对执行性能贡献最大的,当属 AQE(Adaptive query execution,自适应查询引擎)引入的那 3 个特性了,也就是自动分区合并、自动数据倾斜处理和 Join 策略调整。


AQE
哪些配置项与自动分区合并有关?
AQE自动分区合并相关配置项

AQE 事先并不判断哪些分区足够小,而是按照分区编号进行扫描,当扫描量超过“目标尺寸”时,就合并一次,那么,“目标尺寸”由什么来决定的呢?Spark 提供了两个配置项来共同决定分区合并的“目标尺寸”,分区合并的目标尺寸取 advisoryPartitionSizeInBytes 与 partitionSize (每个分区的平均大小)之间的最小值。
我们来举个例子。假设,Shuffle 过后数据大小为 20GB,minPartitionNum 设置为 200,反推过来,每个分区的尺寸就是 20GB / 200 = 100MB。再假设,advisoryPartitionSizeInBytes 设置为 200MB,最终的目标分区尺寸就是取(100MB,200MB)之间的最小值,也就是 100MB。因此你看,并不是你指定了 advisoryPartitionSizeInBytes 是多少

哪些配置项与自动数据倾斜处理有关?

首先,分区尺寸必须要大于 spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes 参数的设定值,才有可能被判定为倾斜分区。然后,AQE 统计所有数据分区大小并排序,取中位数作为放大基数,尺寸大于中位数一定倍数的分区会被判定为倾斜分区,中位数的放大倍数也是由参数 spark.sql.adaptive.skewJoin.skewedPartitionFactor(默认值是 5 倍) 控制。


AQE数据倾斜处理相关配置项
哪些配置项与 Join 策略调整有关?

实际上指的是,把会引入 Shuffle 的 Join 方式,如 Hash Join、Sort Merge Join,“降级”(Demote)为 Broadcast Join。
在 Spark 发布 AQE 之前,开发者可以利用 spark.sql.autoBroadcastJoinThreshold 配置项对数据关联操作进行主动降级。这个参数的默认值是 10MB,参与 Join 的两张表中只要有一张数据表的尺寸小于 10MB


AQE推出之前Join策略相关配置项

不过,autoBroadcastJoinThreshold 这个参数虽然好用,但是有两个让人头疼的短板。一是可靠性较差。尽管开发者明确设置了广播阈值,而且小表数据量在阈值以内,但 Spark 对小表尺寸的误判时有发生,导致 Broadcast Join 降级失败。二来,预先设置广播阈值是一种静态的优化机制,它没有办法在运行时动态对数据关联进行降级调整。

AQE 很好地解决了这两个头疼的问题。首先,AQE 的 Join 策略调整是一种动态优化机制,对于刚才的两张大表,AQE 会在数据表完成过滤操作之后动态计算剩余数据量,当数据量满足广播条件时,AQE 会重新调整逻辑执行计划,在新的逻辑计划中把 Shuffle Joins 降级为 Broadcast Join。再者,运行时的数据量估算要比编译时准确得多,因此 AQE 的动态 Join 策略调整相比静态优化会更可靠、更稳定。


AQE推出之后Join策略相关配置项

3. 性能杀手Shuffle

Map 阶段是如何输出中间文件的?
  1. Map 阶段的输出到底是什么?
    Map 阶段最终生产的数据会以中间文件的形式物化到磁盘中,这些中间文件就存储在 spark.local.dir 设置的文件目录里。中间文件包含两种类型:一类是后缀为 data 的数据文件,存储的内容是 Map 阶段生产的待分发数据;另一类是后缀为 index 的索引文件,它记录的是数据文件中不同分区的偏移地址。这里的分区是指 Reduce 阶段的分区,因此,分区数量与 Reduce 阶段的并行度保持一致。
  2. groupByKey版Map 阶段的计算步骤?
    对于分片中的数据记录,逐一计算其目标分区,并将其填充到 PartitionedPairBuffer(第一个元素是(目标分区,Key),第二个元素是 Value。);
    PartitionedPairBuffer 填满后,如果分片中还有未处理的数据记录,就对 Buffer 中的数据记录按(目标分区 ID,Key)进行排序,将所有数据溢出到临时文件,同时清空缓存;
    重复步骤 1、2,直到分片中所有的数据记录都被处理;
    对所有临时文件和 PartitionedPairBuffer 归并排序(文件内的数据是有序的),最终生成数据文件和索引文件。
  3. reduceByKey版的Map 阶段的计算步骤?
    在计算的过程中,reduceByKey 采用一种叫做 PartitionedAppendOnlyMap 的数据结构来填充数据记录。这个数据结构是一种 Map,而 Map 的 Value 值是可累加、可更新的。因此,PartitionedAppendOnlyMap 非常适合聚合类的计算场景,如计数、求和、均值计算、极值计算等等。
    以此类推,最终合并的数据文件也会小很多。依靠高效的内存数据结构、更少的磁盘文件、更小的文件尺寸,我们就能大幅降低了 Shuffle 过程中的磁盘和网络开销。
Reduce 阶段是如何进行数据分发的?

每个 Map Task 生成的数据文件,都包含所有 Reduce Task 所需的部分数据。因此,任何一个 Reduce Task 要想完成计算,必须先从所有 Map Task 的中间文件里去拉取属于自己的那部分数据。索引文件正是用于帮助判定哪部分数据属于哪个 Reduce Task。Reduce Task 通过网络拉取中间文件的过程,实际上就是不同 Stages 之间数据分发的过程。
显然,Shuffle 中数据分发的网络开销,会随着 Map Task 与 Reduce Task 的线性增长,呈指数级爆炸。

性能杀手
  1. 首先,对于 Shuffle 来说,它需要消耗所有的硬件资源:无论是 PartitionedPairBuffer、PartitionedAppendOnlyMap 这些内存数据结构,还是读写缓冲区,都会消耗宝贵的内存资源;由于内存空间有限,因此溢出的临时文件会引入大量磁盘 I/O,而且,Map 阶段输出的中间文件也会消耗磁盘;呈指数级增长的跨节点数据分发,带来的网络开销更是不容小觑。
  2. 其次,Shuffle 消耗的不同硬件资源之间很难达到平衡


    延迟

4. 广播变量

两种创建广播变量的方式
  1. 从普通变量创建广播变量。在广播变量的运行机制下,普通变量存储的数据封装成广播变量,由 Driver 端以 Executors 为粒度进行分发,每一个 Executors 接收到广播变量之后,将其交由 BlockManager 管理。
  2. 从分布式数据集创建广播变量,这就要比第一种方式复杂一些了。第一步,Driver 需要从所有的 Executors 拉取数据分片,然后在本地构建全量数据;第二步,Driver 把汇总好的全量数据分发给各个 Executors,Executors 再将接收到的全量数据缓存到存储系统的 BlockManager 中。

Shuffle Joins
第一步就是对参与关联的左右表分别进行 Shuffle,Shuffle 的分区规则是先对 Join keys 计算哈希值,再把哈希值对分区数取模。Shuffle 完成之后,第二步就是在同一个 Executors 内,Reduce task 就可以对 userID 一致的记录进行关联操作。


Shuffle Joins的数据分发

Broadcast Join


Broadcast Join
利用配置项强制广播

使用广播阈值配置项让 Spark 优先选择 Broadcast Joins 的关键,就是要确保至少有一张表的存储尺寸小于广播阈值(数据表在磁盘上的存储大小,同一份数据在内存中的存储大小往往会比磁盘中的存储大小膨胀数倍)

  1. 利用配置项强制广播


    利用配置项强制广播,它的设置值是存储大小,默认是 10MB
  2. 用 Join Hints 强制广播
    设置 Join Hints 的方法就是在 SQL 结构化查询语句里面加上一句“/*+ broadcast(某表) */”
  3. 用 broadcast 函数强制广播
广播变量不是银弹
  1. 首先,从性能上来讲,Driver 在创建广播变量的过程中,需要拉取分布式数据集所有的数据分片。
  2. 其次,从功能上来讲,并不是所有的 Joins 类型都可以转换为 Broadcast Joins。一来,Broadcast Joins 不支持全连接(Full Outer Joins);二来,在所有的数据关联中,我们不能广播基表。或者说,即便开发者强制广播基表,也无济于事。比如说,在左连接(Left Outer Join)中,我们只能广播右表;在右连接(Right Outer Join)中,我们只能广播左表。

5. CPU视角

CPU 与内存的平衡本质上是什么?

Spark 将内存分成了 Execution Memory 和 Storage Memory 两类,分别用于分布式任务执行和 RDD 缓存。其中,RDD 缓存虽然最终占用的是 Storage Memory,但在 RDD 展开(Unroll)之前,计算任务消耗的还是 Execution Memory。因此,Spark 中 CPU 与内存的平衡,其实就是 CPU 与执行内存之间的协同与配比。

三足鼎立:并行度、并发度与执行内存

并行度指的是为了实现分布式计算,分布式数据集被划分出来的份数。并行度明确了数据划分的粒度:并行度越高,数据的粒度越细,数据分片越多,数据越分散。并行度可以通过两个参数来设置,分别是 spark.default.parallelism 和 spark.sql.shuffle.partitions。前者用于设置 RDD 的默认并行度,后者在 Spark SQL 开发框架下,指定了 Shuffle Reduce 阶段默认的并行度。并发度呢?Executor 的线程池大小由参数 spark.executor.cores 决定,每个任务在执行期间需要消耗的线程数由 spark.task.cpus 配置项给定。两者相除得到的商就是并发度,也就是同一时间内,一个 Executor 内部可以同时运行的最大任务数量。又因为,spark.task.cpus 默认数值为 1,并且通常不需要调整,所以,并发度基本由 spark.executor.cores 参数敲定。就 Executor 的线程池来说,尽管线程本身可以复用,但每个线程在同一时间只能计算一个任务,每个任务负责处理一个数据分片。因此,在运行时,线程、任务与分区是一一对应的关系。

  1. CPU 低效原因之一:线程挂起
  2. CPU 低效原因之二:调度开销


    三足鼎立

6. 内存视角

User Memory 性能隐患

对于 User Memory 内存区域来说,使用 空间去重复存储同样的数据,本身就是降低了内存的利用率

预估内存占用
  1. 第一步,计算 User Memory 的内存消耗。我们先汇总应用中包含的自定义数据结构,并估算这些对象的总大小 #size,然后用 #size 乘以 Executor 的线程池大小,即可得到 User Memory 区域的内存消耗 #User。
  2. 第二步,计算 Storage Memory 的内存消耗。我们先汇总应用中涉及的广播变量和分布式数据集缓存,分别估算这两类对象的总大小,分别记为 #bc、#cache。另外,我们把集群中的 Executors 总数记作 #E。这样,每个 Executor 中 Storage Memory 区域的内存消耗的公式就是:#Storage = #bc + #cache / #E。
  3. 第三步,计算执行内存的消耗。我们知道执行内存的消耗与多个因素有关。第一个因素是 Executor 线程池大小 #threads,第二个因素是数据分片大小,而数据分片大小取决于数据集尺寸 #dataset 和并行度 #N。因此,每个 Executor 中执行内存的消耗的计算公式为:#Execution = #threads * #dataset / #N。
调整内存配置项
  1. 首先,根据定义,spark.memory.fraction 可以由公式(#Storage + #Execution)/(#User + #Storage + #Execution)计算得到。
  2. 同理,spark.memory.storageFraction 的数值应该参考(#Storage)/(#Storage + #Execution)。
  3. 最后,对于 Executor 堆内内存总大小 spark.executor.memory 的设置,我们自然要参考 4 个内存区域的总消耗,也就是 300MB + #User + #Storage + #Execution。不过,我们要注意,利用这个公式计算的前提是,不同内存区域的占比与不同类型的数据消耗一致。
Cache

对于存储级别来说,实际开发中最常用到的有两个,MEMORY_ONLY 和 MEMORY_AND_DISK,它们分别是 RDD 缓存和 DataFrame 缓存的默认存储级别。对于缓存计算来说,它分为 3 个步骤,第一步是 Unroll,把 RDD 数据分片的 Iterator 物化为对象值,第二步是 Transfer,把对象值封装为 MemoryEntry,第三步是把 BlockId、MemoryEntry 价值对注册到 LinkedHashMap 数据结构。另外,当数据缓存需求远大于 Storage Memory 区域的空间供给时,Spark 利用 LinkedHashMap 数据结构提供的特性,会遵循 LRU 和兔子不吃窝边草这两个基本原则来清除内存空间:LRU:按照元素的访问顺序,优先清除那些“最近最少访问”的 BlockId、MemoryEntry 键值对兔子不吃窝边草:在清除的过程中,同属一个 RDD 的 MemoryEntry 拥有“赦免权”

OOM
  1. Driver 端的 OOM
    创建的数据集超过内存上限
    收集的结果集超过内存上限
    调节 Driver 端侧内存大小我们要用到 spark.driver.memory 配置项,预估数据集尺寸可以用“先 Cache,再查看执行计划”的方式
  2. Storage Memory 的 OOM
    不会发生,数据集不能完全缓存到 MemoryStore,Spark 也不会抛 OOM 异常,额外的数据要么落盘(MEMORY_AND_DISK)、要么直接放弃(MEMORY_ONLY)
  3. User Memory 的 OOM
    User Memory 用于存储用户自定义的数据结构,如数组、列表、字典等。因此,如果这些数据结构的总大小超出了 User Memory 内存区域的上限就会OOM,解决 User Memory 端 OOM 的思路和 Driver 端的并无二致,也是先对数据结构的消耗进行预估,然后相应地扩大 User Memory 的内存配置,总大小由 spark.executor.memory * ( 1 - spark.memory.fraction)计算得到
  4. Execution Memory 的 OOM
    数据量并不是决定 OOM 与否的关键因素,数据分布与 Execution Memory 的运行时规划是否匹配才是,一旦分布式任务的内存请求超出 1/N 这个上限,Execution Memory 就会出现 OOM 问题。而且,相比其他场景下的 OOM 问题,Execution Memory 的 OOM 要复杂得多,它不仅仅与内存空间大小、数据分布有关,还与 Executor 线程池和运行时任务调度有关。
    数据倾斜:Spark 在 Reduce 阶段支持 Spill 和外排,PairBuffer 和 AppendOnlyMap 等数据结构的内存消耗,以及数据排序的临时内存消耗
    消除数据倾斜,调整数据分片尺寸
    调整 Executor 线程池、内存、并行度等相关配置,提高 1/N 上限
    数据膨胀:磁盘中的数据进了 JVM 之后会膨胀
    把数据打散,提高数据分片数量、降低数据粒度
    加大内存配置,结合 Executor 线程池调整,提高 1/N 上限

7. 磁盘视角

磁盘在功能上的作用
  1. 溢出临时文件
  2. 存储 Shuffle 中间文件
  3. 缓存分布式数据集。也就是说,凡是带DISK字样的存储模式,都会把内存中放不下的数据缓存到磁盘
磁盘复用
  1. 磁盘复用的收益之一就是缩短失败重试的路径,在保障作业稳定性的同时提升执行性能。
  2. ReuseExchange 机制下的磁盘复用:相同或是相似的物理计划可以共享 Shuffle 计算的中间结果

8. 网络视角

不同硬件资源处理延迟对比
数据读写

PROCESS_LOCAL:任务与数据同在一个 JVM 进程中
NODE_LOCAL:任务与数据同在一个计算节点,数据可能在磁盘上或是另一个 JVM 进程中
RACK_LOCAL:任务与数据不在同一节点,但在同一个物理机架上
ANY:任务与数据是跨机架、甚至是跨 DC(Data Center,数据中心)的关系访问数据源是否会引入网络开销,取决于任务与数据的本地性关系,也就是任务的本地性级别


不同本地性级别与磁盘、网络开销的关系
数据处理

Shuffle 作为大多数计算场景的“性能瓶颈担当”,确实是网络开销的罪魁祸首。根据“能省则省”的开发原则,我们自然要想尽办法去避免 Shuffle。

数据传输

在数据通过网络分发之前,我们可以利用 Kryo Serializer 序列化器,提升序列化字节的存储效率,从而有效降低在网络中分发的数据量,整体上减少网络开销。


与Kryo Serializer有关的配置项

9. 补充

  1. join分析:shuffle hash join、broadcast hash join join分析:shuffle hash join、broadcast hash join,hash映射成功之后再检查 join 条件的
  2. 使用 Parquet、ORC 等文件格式,去坐享谓词下推带来的数据读取效率",应该如何理解?谓词下推本身,不依赖于任何文件存储格式,它本身就是Spark SQL的优化策略,DataFrame里面如果包含filter一类的操作,他们就会尽可能地被推到执行计划的最下面。
    但是,谓词下推的效果,和文件存储格式有关。假设是CSV这种行存格式,那么谓词下推顶多是在整个执行计划的shuffle之前,降低数据量大小。但如果是orc、Parquet这种列存文件,谓词下推能直接推到文件扫描上去,直接在磁盘扫描阶段,就降低文件扫描量,降低i/o开销,从而提升执行性能。
  3. 如何利用钨丝计划的优势?
  4. 如何利用 AQE 的优势?
  5. Task,Partition的概念:Spark中Task,Partition
  6. 并行度(Parallelism),并行计算任务(Paralleled Tasks),这两个概念:并行度的出发点是数据,它明确了数据划分的粒度。像分区数量、分片数量、Partitions 这些概念都是并行度的同义词。并行计算任务则不同,它指的是在任一时刻整个集群能够同时计算的任务数量。换句话说,它的出发点是计算任务、是 CPU,由与 CPU 有关的三个参数共同决定,并行度决定了数据粒度,数据粒度决定了分区大小,分区大小则决定着每个计算任务的内存消耗。在同一个 Executor 中,多个同时运行的计算任务“基本上”是平均瓜分可用内存的,每个计算任务能获取到的内存空间是有上限的,因此并行计算任务数会反过来制约并行度的设置。
  7. 内存空间是有限的,该把多少内存划分给堆内,又该把多少内存留给堆外呢?对于需要处理的数据集,如果数据模式比较扁平,而且字段多是定长数据类型,就更多地使用堆外内存。相反地,如果数据模式很复杂,嵌套结构或变长字段很多,就更多采用 JVM 堆内内存会更加稳妥
  8. 在堆内内存里,该怎么平衡 User Memory 和 Spark 用于计算的内存空间?spark.memory.fraction 的默认值是 0.6,也就是 JVM 堆内空间的 60% 会划拨给 Spark 支配,剩下的 40% 划拨给 User Memory。当在 JVM 内平衡 Spark 可用内存和 User Memory 时,你需要考虑你的应用中类似的自定义数据结构多不多、占比大不大?然后再相应地调整两块内存区域的相对占比。
  9. 在统一内存管理模式下,该如何平衡 Execution Memory 和 Storage Memory?如果你的应用类型是“缓存密集型”,如机器学习训练任务,就很有必要通过调节这个参数来保障数据的全量缓存。在这个过程中,你要特别注意 RDD 缓存与执行效率之间的平衡,首先,RDD 缓存占用的内存空间多了,Spark 用于执行分布式计算任务的内存空间自然就变少了,而且数据分析场景中常见的关联、排序和聚合等操作都会消耗执行内存,这部分内存空间变少,自然会影响到这类计算的执行效率。其次,大量缓存引入的 GC(Garbage Collection,垃圾回收)负担对执行效率来说是个巨大的隐患。我们可以调节 spark.rdd.compress 这个参数。RDD 缓存默认是不压缩的,启用压缩之后,缓存的存储效率会大幅提升,有效节省缓存内存的占用,从而把更多的内存空间留给分布式任务执行。但都是以引入额外的计算开销、牺牲 CPU 为代价的。所以说,性能调优的过程本质上就是不断地平衡不同硬件资源消耗的过程。
  10. 为什么堆外内存如此优秀?:Spark 开辟的堆外内存基于紧凑的二进制格式,相比 JVM 堆内内存,Spark 通过 Java Unsafe API 在堆外内存中的管理,才会有那么多的优势。
  11. spark.sql.shuffle.partitions:只有你的计算中涉及Joins或是聚合,spark.sql.shuffle.partitions,这个参数的设置,才会影响Shuffle Reduce阶段的并行度。如果你的作业没有Joins或是聚合计算,这个参数设了也是摆设。
  12. Class Student是存在User Memory? new Student("小明")是存在Executor Memory?1. 如果你用RDD封装这些自定义类型,比如RDD[Student],那么,数据集消耗的是Execution memory。2. 相反,如果你是在处理分布式数据集的函数中,new Student来辅助计算过程,那么这个对象,是放在User memory里面的。
  13. AQE 如何判定数据分区是否倾斜呢?它又是怎么把大分区拆分成多个小分区的?我们还是通过一个例子来理解。假设数据表 A 有 3 个分区,分区大小分别是 80MB、100MB 和 512MB。显然,这些分区按大小个排序后的中位数是 100MB,因为 skewedPartitionFactor 的默认值是 5 倍,所以大于 100MB * 5 = 500MB 的分区才有可能被判定为倾斜分区。在我们的例子中,只有最后一个尺寸是 512MB 的分区符合这个条件。这个时候,Spark 还不能完全判定它就是倾斜分区,还要看 skewedPartitionThresholdInBytes 配置项,这个参数的默认值是 256MB。对于那些满足中位数条件的分区,必须要大于 256MB,Spark 才会把这个分区最终判定为倾斜分区。假设 skewedPartitionThresholdInBytes 设定为 1GB,那在我们的例子中,512MB 那个大分区,Spark 也不会把它看成是倾斜分区,自然也就不能享受到 AQE 对于数据倾斜的优化处理。检测到倾斜分区之后,接下来就是对它拆分,拆分的时候还会用到 advisoryPartitionSizeInBytes 参数。假设我们将这个参数的值设置为 256MB,那么,刚刚那个 512MB 的倾斜分区会以 256MB 为粒度拆分成多份,因此,这个大分区会被拆成 2 个小分区( 512MB / 256MB =2)。拆分之后,原来的数据表就由 3 个分区变成了 4 个分区,每个分区的尺寸都不大于 256MB。
  14. AQE 中数据倾斜的处理机制?启用动态 Join 策略调整还有个前提,也就是要满足 nonEmptyPartitionRatioForBroadcastJoin 参数的限制。这个参数的默认值是 0.2,大表过滤之后,非空的数据分区占比要小于 0.2,才能成功触发 Broadcast Join 降级。我们来举个例子。假设,大表过滤之前有 100 个分区,Filter 操作之后,有 85 个分区内的数据因为不满足过滤条件,在过滤之后都变成了没有任何数据的空分区,另外的 15 个分区还保留着满足过滤条件的数据。这样一来,这张大表过滤之后的非空分区占比是 15 / 100 = 15%,因为 15% 小于 0.2,所以这个例子中的大表会成功触发 Broadcast Join 降级。相反,如果大表过滤之后,非空分区占比大于 0.2,那么剩余数据量再小,AQE 也不会把 Shuffle Joins 降级为 Broadcast Join。因此,如果你想要充分利用 Broadcast Join 的优势,可以考虑把这个参数适当调高。
  15. map端的合并导致同一个key的数据没有被拉取到同一个执行reduce task的executor中?假设是单表shuffle,比如reduceByKey这种,那么合并之后不影响,即便一个分片有多个key,也不要紧,只要保证同一个key的payload都在一个分区就行。两表join其实外表,也就是驱动表,会驱动内表的“全量扫描”,带引号是因为效率不一样,smj、hj不用全量,但意思是一样的,就是不管内表有多少分区,都会被外表驱动着去被遍历。因此,不轮内表数据分配到了哪个分区,其实都还是在一个executor进程内,所以,不影响join逻辑,也不影响效率。
  16. spark.shuffle.sort.bypassMergeThreshold 这个阈值为什么是跟Reduce 端的分区数有关?如果你的reduce阶段并行度非常的高,那么map task的计算开销会非常大,要同时打开非常多的临时文件、建立非常多的写buffer
  17. 比较大的shuffle 可以对下面的参数进行调节,提高整个shuffle 的健壮性?
    spark.shuffle.compress 是否对shuffle 的中间结果进行压缩,如果压缩的话使用spark.io.compression.codec 的配置进行压缩
    spark.shuffle.io.maxRetries io 失败的重试次数,在大型shuffle遇到网络或者GC 问题的时候很有用。
    spark.shuffle.io.retryWait io 失败的时候的等待时间
  18. bypass的实现机制? Spark源码精读分析计划 (shuffle write)
    a)为每一个reduce task生成一个临时文件
    b)为每个临时文件创建写buffer、以及一个serializer对象,用于序列化数据
    c)保持所有的临时文件打开,将map阶段的数据,按照reduce端partitionId的不同,依次写入到这些临时文件
    d)最后,map task计算完毕,把所有这些临时文件合并到一起,生成data文件,并同时生成index文件
  19. 在shuffle中对所有临时文件和内存数据结构中剩余的数据记录做归并排序,是结合堆排序的吗,临时文件太多的时候,会不会不能同时打开这么多文件,还是用的类似优化版的两两归并呢?通常来说,每个Task处理的数据分片大小在200MB最佳,这个是结合经验得出的结论。那么我们就可以利用后面说的“三足鼎立”来保证每个Task的分片大小就在200MB左右。在这样的情况下,现代计算机的硬件资源基本上都比较充足,比如说,对于一个有着5g Execution Memory,32 cores的Executors来说,每个Task能分到的内存时160MB左右,对于200MB的数据分区来说,其实spills的数量是有限的,因此这种情况下,同时打开多个临时文件的压力其实还好。
  20. 理解 by pass 机制极其性能瓶颈:注意是为每一个reduce task生成一个临时文件并且在合并的时候要打开所有的临时文件进行合并,所以说不宜reduce的分区过多(分区数是怎么决定的)
  21. Spark 广播机制现有的实现方式是存在隐患的,在数据量较大的情况下,Driver 可能会成为瓶颈,你能想到更好的方式来重新实现 Spark 的广播机制吗?https://issues.apache.org/jira/browse/SPARK-17556 改成由driver获取到数据分布,然后通知各个executor之间进行拉取,这样可以利用多个executor网络,避免只有driver组装以后再一个一个发送效率过低
  22. 有什么办法能准确地预估一张表在内存中的存储大小呢?第一步,把要预估大小的数据表缓存到内存,比如直接在 DataFrame 或是 Dataset 上调用 cache 方法;第二步,读取 Spark SQL 执行计划的统计数据。
  23. 字符串“abcd”只需要消耗 4 个字节,为什么JVM 在堆内存储这 4 个字符串总共需要消耗 48 个字节?Project Tungsten: Improving the Efficiency of Spark Applications
  24. 执行内存计算公式?堆内执行内存的初始值由很多参数共同决定,具体的计算公式是:spark.executor.memory * spark.memory.fraction * (1 - spark.memory.storageFraction)。堆外执行内存的计算:spark.memory.offHeap.size * (1 - spark.memory.storageFraction)
  25. 有效提升 CPU 利用率的方法?首先,在一个 Executor 中,每个 CPU 线程能够申请到的内存比例是有上下限的,最高不超过 1/N,最低不少于 1/N/2,其中 N 代表线程池大小。其次,在给定线程池大小和执行内存的时候,并行度较低、数据分片较大容易导致 CPU 线程挂起,线程频繁挂起不利于提升 CPU 利用率,而并行度过高、数据过于分散会让调度开销更显著,也不利于提升 CPU 利用率。最后,在给定执行内存 M、线程池大小 N 和数据总量 D 的时候,想要有效地提升 CPU 利用率,我们就要计算出最佳并行度 P,计算方法是让数据分片的平均大小 D/P 坐落在(M/N/2, M/N)区间。这样,在运行时,我们的 CPU 利用率往往不会太差。(注意理解并行度的意义,这里的公式表明提供了一个范围区间,每个并行的task处理的数据量等同与总的内存数/线程数)
  26. 从 Executor 并发度、执行内存大小和分布式任务并行度出发,你认为在什么情况下会出现 OOM 的问题?在每个线程都分配到了最大内存,即 M/N 的内存时,如果 task 还需要更多的内存,那么就会发生 OOM。 在每个线程都分配到了最少内存,即 M/2N的内存时,如果 task 还需要更多的内存,此时又没有其他线程释放内存供其使用,那么也会导致OOM。
  27. 你觉得,为什么 Eviction 规则要遵循“兔子不吃窝边草”呢?如果允许同一个 RDD 的 MemoryEntry 被驱逐,有什么危害吗?
  28. 对于 DataFrame 的缓存复用,Cache Manager 为什么没有采用根据 Optimized Logical Plan 的方式,你觉得难点在哪里?如果让你实现 Cache Manager 的话,你会怎么做?
  29. Spark调度系统:Spark调度系统
  30. ReuseExchange 机制触发条件:多个查询所依赖的分区规则要与 Shuffle 中间数据的分区规则保持一致多个查询所涉及的字段(Attributes)要保持一致

你可能感兴趣的:(2022-02-24-Spark-44(性能调优通用调优))