2022-02-20-Spark-43(性能调优原理)

1. 弹性分布式数据集

def createInstance(factDF: DataFrame, startDate: String, endDate: String): DataFrame = {
val instanceDF = factDF
    .filter(col("eventDate") > lit(startDate) && col("eventDate") <= lit(endDate))
    .groupBy("dim1", "dim2", "dim3", "event_date")
    .agg("sum(value) as sum_value")
instanceDF
}
 
pairDF.collect.foreach{
case (startDate: String, endDate: String) =>
    val instance = createInstance(factDF, startDate, endDate)
    val outPath = s"${rootPath}/endDate=${endDate}/startDate=${startDate}"
    instance.write.parquet(outPath)
} 

单机思维,factDF是一个大数据集,每次foreach都会调用createInstance导致这个数据集被多次扫描

RDD

RDD 的 4 大属性可以划分为两类,横向属性和纵向属性。其中,横向属性锚定数据分片实体,并规定了数据分片在分布式集群中如何分布;纵向属性用于在纵深方向构建 DAG,通过提供重构 RDD 的容错能力保障内存计算的稳定性。


四大属性

preferredLocations:移动计算不移动数据,数据在哪就在哪计算,减少IO

2. 内存计算

为什么Spark是内存计算的,要回答这个问题就要回答Spark中Stage的概念的意义是什么了:在同一 Stage 内部,所有算子融合为一个函数(捏合),Stage 的输出结果由这个函数一次性作用在输入数据集而产生。这也说明Shuffle(上下游的分区器是否是一致的)是多么的影响性能啊

所谓内存计算,不仅仅是指数据可以缓存在内存中,更重要的是让我们明白了,通过计算的融合( Stage内的流水线式计算模式)来大幅提升数据在内存中的转换效率,进而从整体上提升应用的执行性能 深入浅出 Spark:内存计算的由来

在同一 Stage 内部,所有算子融合为一个函数(捏合)

3. 调度系统

调度系统中的核心组件
调度系统中的核心组件
  1. DAGScheduler
    一是把用户 DAG 拆分为 Stages,二是在 Stage 内创建计算任务 Tasks,这些任务囊括了用户通过组合不同算子实现的数据转换逻辑。然后,执行器 Executors 接收到 Tasks,会将其中封装的计算函数应用于分布式数据分片,去执行分布式的计算过程。
  2. SchedulerBackend
    是对于资源调度器的封装与抽象,为了支持多样的资源调度模式如 Standalone、YARN 和 Mesos,SchedulerBackend 提供了对应的实现类。在运行时,Spark 根据用户提供的 MasterURL,来决定实例化哪种实现类的对象。对于集群中可用的计算资源,SchedulerBackend 会用一个叫做 ExecutorDataMap 的数据结构,来记录每一个计算节点中 Executors 的资源状态。ExecutorDataMap 是一种 HashMap,它的 Key 是标记 Executor 的字符串,Value 是一种叫做 ExecutorData 的数据结构,ExecutorData 用于封装 Executor 的资源状态,如 RPC 地址、主机地址、可用 CPU 核数和满配 CPU 核数等等,它相当于是对 Executor 做的“资源画像”。
  3. TaskScheduler
    左边有需求,右边有供给,如果把 Spark 调度系统看作是一个交易市场的话,那么中间还需要有个中介来帮它们对接意愿、撮合交易,从而最大限度地提升资源配置的效率。在 Spark 调度系统中,这个中介就是 TaskScheduler。TaskScheduler 的职责是,基于既定的规则与策略达成供需双方的匹配与撮合。TaskScheduler 的核心是任务调度的规则和策略,TaskScheduler 的调度策略分为两个层次,一个是不同 Stages 之间的调度优先级,一个是 Stages 内不同任务之间的调度优先级。
    对于同一个 Stages 内部不同任务之间的调度优先级,Stages 内部的任务调度相对来说简单得多。当 TaskScheduler 接收到来自 SchedulerBackend 的 WorkerOffer 后,TaskScheduler 会优先挑选那些满足本地性级别要求的任务进行分发。
    Spark 调度系统的原则是尽可能地让数据呆在原地、保持不动,同时尽可能地把承载计算任务的代码分发到离数据最近的地方,从而最大限度地降低分布式系统中的网络开销
    spark的资源调度和任务调度是分开的,也就是说,不论你用standalone、yarn还是mesos,spark在申请硬件资源的时候,以cpu、memory量化申请executors的过程,是先于任务调度的。用你的话说,executors已经提前申请好了,申请executors的时候,只看cpu和memory是否满足要求,不会考虑locality这些与任务有关的细节

4. 存储系统

Spark 存储系统是为谁服务
  1. RDD 缓存
    是将 RDD 以缓存的形式物化到内存或磁盘的过程
  2. Shuffle 中间文件
    Shuffle 中间文件实际上就是 Shuffle Map 阶段的输出结果,这些结果会以文件的形式暂存于本地磁盘。在集群范围内,Reducer 想要拉取属于自己的那部分中间数据,就必须要知道这些数据都存储在哪些节点,以及什么位置。而这些关键的元信息,正是由 Spark 存储系统保存并维护的。
  3. 广播变量
    利用存储系统,广播变量可以在 Executors 进程范畴内保存全量数据。这样一来,对于同一 Executors 内的所有计算任务,应用就能够以 Process local 的本地性级别,来共享广播变量中携带的全量数据
存储系统的基本组件

与调度系统类似,Spark 存储系统是一个囊括了众多组件的复合系统,如 BlockManager、BlockManagerMaster、MemoryStore、DiskStore 和 DiskBlockManager 等等

  1. BlockManager
    在 Executors 端负责统一管理和协调数据的本地存取与跨节点传输。
    对外,BlockManager 与 Driver 端的 BlockManagerMaster 通信,不仅定期向 BlockManagerMaster 汇报本地数据元信息,还会不定时按需拉取全局数据存储状态
    对内,BlockManager 通过组合存储系统内部组件的功能来实现数据的存与取、收与发
  2. MemoryStore
    MemoryStore 用来管理数据在内存中的存取
    MemoryStore 支持对象值和字节数组,统一采用 MemoryEntry 数据抽象对它们进行封装。对象值和字节数组二者之间存在着一种博弈关系,所谓的“以空间换时间”和“以时间换空间”,两者的取舍还要看具体的应用场景。
  3. DiskStore
    DiskStore 用来管理数据在磁盘中的存取。
    利用 DiskBlockManager 维护的数据块与磁盘文件的对应关系,来完成字节序列与磁盘文件之间的转换。

5. 内存管理基础

内存的管理模式
JVM堆内内存的申请与释放

在管理方式上,Spark 会区分堆内内存(On-heap Memory)和堆外内存(Off-heap Memory)。这里的“堆”指的是 JVM Heap,因此堆内内存实际上就是 Executor JVM 的堆内存;堆外内存指的是通过 Java Unsafe API,像 C++ 那样直接从操作系统中申请和释放内存空间
其中,堆内内存的申请与释放统一由 JVM 代劳。比如说,Spark 需要内存来实例化对象,JVM 负责从堆内分配空间并创建对象,然后把对象的引用返回,最后由 Spark 保存引用,同时记录内存消耗。反过来也是一样,Spark 申请删除对象会同时记录可用内存,JVM 负责把这样的对象标记为“待删除”,然后再通过垃圾回收(Garbage Collection,GC)机制将对象清除并真正释放内存。
堆外内存则不同,Spark 通过调用 Unsafe 的 allocateMemory 和 freeMemory 方法直接在操作系统内存中申请、释放内存空间这样的内存管理方式自然不再需要垃圾回收机制,也就免去了它带来的频繁扫描和回收引入的性能开销。更重要的是,空间的申请与释放可以精确计算,因此 Spark 对堆外可用内存的估算会更精确,对内存的利用率也更有把握。

内存区域的划分
不同内存区域的划分与计算
  1. 堆外内存
    Spark 把堆外内存划分为两块区域:一块用于执行分布式任务,如 Shuffle、Sort 和 Aggregate 等操作,这部分内存叫做 Execution Memory;一块用于缓存 RDD 和广播变量等数据,它被称为 Storage Memory。
  2. 堆内内存
    堆内内存的划分方式和堆外差不多,Spark 也会划分出用于执行和缓存的两份内存空间。不仅如此,Spark 在堆内还会划分出一片叫做 User Memory 的内存空间,它用于存储开发者自定义数据结构。
    除此之外,Spark 在堆内还会预留出一小部分内存空间,叫做 Reserved Memory,它被用来存储各种 Spark 内部对象,例如存储系统中的 BlockManager、DiskBlockManager 等等。
执行与缓存内存(???)

在 1.6 版本之后,Spark 推出了统一内存管理模式。统一内存管理指的是 Execution Memory 和 Storage Memory 之间可以相互转化

  1. 如果对方的内存空间有空闲,双方就都可以抢占;
  2. 对于 RDD 缓存任务抢占的执行内存,当执行任务有内存需要时,RDD 缓存任务必须立即归还抢占的内存,涉及的 RDD 缓存数据要么落盘、要么清除;
  3. 对于分布式计算任务抢占的 Storage Memory 内存空间,即便 RDD 缓存任务有收回内存的需要,也要等到任务执行完毕才能释放。

6. 补充

  1. 提交spark任务的模式:standalone模式,yarn client和yarn cluster模式的区别。主要是分清楚client,master,worker,executor,Driver(在哪里,有什么用的问题)
  2. 问题:有网络传输是否就是发生了shuffle,Shuffle的含义就是洗牌,将数据打散,父RDD一个分区中的数据如果给了子RDD的多个分区(只要存在这种可能),就是shuffle。Shuffle会有网络传输数据,但是有网络传输,并不意味着就是shuffle。,看一下具体的示例:shuffle依赖
  3. shuffle机制和原理,有哪些shuffle类型,有什么不同的特点或者优势
  4. HashShuffle和Sorted-Based Shuffle的比较,为什么Sorted-Based Shuffle比HashShuffle要好,从哪些层面考虑呢
  5. 回答为什么说Spark是内存式计算,回答DAG的问题(血缘)血统(DAG)
  6. 一个案例:如果对一个dataframe Read以后做了一堆不会触发shuffle 的操作,最后又调用了一下coalesce(1),然后write ,那是不是就意味着从读数据开始的所有操作都会在一个executor上完成?
    shuffle = false,就像你说的,所有操作,从一开始,并行度都是1,都在一个executor计算,显然,这个时候,整个作业非常慢,奇慢无比
    shuffle = true,这个时候,coalesce就会引入shuffle,切割stage。coalesce之前,用源数据DataFrame的并行度,这个时候是多个Executors真正的并行计算;coalesce之后,也就是shuffle之后,并行度下降为1,所有父RDD的分区,全部shuffle到一个executor,交给一个task去计算。
  7. 任务调度的时候不考虑可用内存大小吗?Spark在做任务调度之前,SchedulerBackend封装的调度器,比如Yarn、Mesos、Standalone,实际上已经完成了资源调度,换句话说,整个集群有多少个containers/executors,已经是一件确定的事情了。为什么ExecutorData不存储于内存相关的信息。答案是:不需要。一来,TaskScheduler要达到目的,它只需知道Executors是否有空闲CPU、有几个空闲CPU就可以了,有这些信息就足以让他决定是否把tasks调度到目标Executors上去。二来,每个Executors的内存总大小,在Spark集群启动的时候就确定了,因此,ExecutorData自然是没必要记录像Total Memory这样的冗余信息。Spark对于内存的预估不准,再者,每个Executors的可用内存都会随着GC的执行而动态变化,因此,ExecutorData记录的Free Memory,永远都是过时的信息,TaskScheduler拿到这样的信息,也没啥用。一者是不准,二来确实没用,因为TaskScheduler拿不到数据分片大小这样的信息,TaskScheduler在Driver端,而数据分片是在目标Executors,所以TaskScheduler拿到Free Memory也没啥用,因为它也不能判断说:task要处理的数据分片,是不是超过了目标Executors的可用内存。
  8. 资源调度和任务调度是分开的。资源调度主要看哪些节点可以启动executors,是否能满足executors所需的cpu数量要求,这个时候,不会考虑任务、数据本地性这些因素。资源调度完成之后,在任务调度阶段,spark负责计算每个任务的本地性,效果就是task明确知道自己应该调度到哪个节点,甚至是哪个executors。最后scheduler Backend会把task代码,分发到目标节点的目标executors,完成任务调度,实现数据不动代码动。
  9. DAGScheduler 在创建 Tasks 的过程中,是如何设置每一个任务的本地性级别?DAGScheduler会尝试获取RDD的每个Partition的偏好位置信息,a.如果RDD被缓存,通过缓存的位置信息获取每个分区的位置信息;b.如果RDD有preferredLocations属性,通过preferredLocations获取每个分区的位置信息;c. 遍历RDD的所有是NarrowDependency的父RDD,找到第一个满足a,b条件的位置信息
  10. 关于等待时间和执行时间的平衡?在调度了最契合locality的tasks后还有空闲executor。下一批task本来是有资源可用的,但最适合执行task的executor已被占用,此时会评估下一批tasks等待时间和在空闲executor执行数据传输时间,如果等待时间大于数据传输则直接调度到空闲executor,否则继续等待。把wait参数设置为0,则可以不进行等待,有资源时直接调度执行。看你具体场景。
  11. 能在Driver段处理的就在Driver处理,比如作者提供的初始化字典的例子,否则每个executor都要初始化一边(Driver端把包含字典对象发给的executor)
  12. 透过 RDD 缓存看 MemoryStore
  13. 透过 Shuffle 看 DiskStore
  14. 为什么用到了LinkedHashMap存储(Block ID, MemoryEntry)?当storage memory不足,spark需要删除rdd cache的时候,遵循的是lru,那么问题来了,它咋实现的lru,答案就是它充分利用LinkedHashMap特性:访问有序
  15. spark 做shuffle的时候,shuffle write 要写入磁盘,是否可以直接通过内存传输?
  16. 结合 RDD 数据存储到 MemoryStore 的过程,推演出通过 MemoryStore 通过 getValues/getBytes 方法去访问 RDD 缓存内容的过程吗?
  17. 考 RDD 缓存存储的过程,推演出广播变量存入 MemoryStore 的流程吗?
  18. 堆外内存存在的意义是什么,有什么场景是一定需要堆外内存么?Spark不同任务在堆内堆外内存的使用选择上的逻辑?
  19. 分别计算不同内存区域(Reserved、User、Execution、Storage)的具体大小

你可能感兴趣的:(2022-02-20-Spark-43(性能调优原理))