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:内存计算的由来
3. 调度系统
调度系统中的核心组件
- DAGScheduler
一是把用户 DAG 拆分为 Stages,二是在 Stage 内创建计算任务 Tasks,这些任务囊括了用户通过组合不同算子实现的数据转换逻辑。然后,执行器 Executors 接收到 Tasks,会将其中封装的计算函数应用于分布式数据分片,去执行分布式的计算过程。 - SchedulerBackend
是对于资源调度器的封装与抽象,为了支持多样的资源调度模式如 Standalone、YARN 和 Mesos,SchedulerBackend 提供了对应的实现类。在运行时,Spark 根据用户提供的 MasterURL,来决定实例化哪种实现类的对象。对于集群中可用的计算资源,SchedulerBackend 会用一个叫做 ExecutorDataMap 的数据结构,来记录每一个计算节点中 Executors 的资源状态。ExecutorDataMap 是一种 HashMap,它的 Key 是标记 Executor 的字符串,Value 是一种叫做 ExecutorData 的数据结构,ExecutorData 用于封装 Executor 的资源状态,如 RPC 地址、主机地址、可用 CPU 核数和满配 CPU 核数等等,它相当于是对 Executor 做的“资源画像”。 - 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 存储系统是为谁服务
- RDD 缓存
是将 RDD 以缓存的形式物化到内存或磁盘的过程 - Shuffle 中间文件
Shuffle 中间文件实际上就是 Shuffle Map 阶段的输出结果,这些结果会以文件的形式暂存于本地磁盘。在集群范围内,Reducer 想要拉取属于自己的那部分中间数据,就必须要知道这些数据都存储在哪些节点,以及什么位置。而这些关键的元信息,正是由 Spark 存储系统保存并维护的。 - 广播变量
利用存储系统,广播变量可以在 Executors 进程范畴内保存全量数据。这样一来,对于同一 Executors 内的所有计算任务,应用就能够以 Process local 的本地性级别,来共享广播变量中携带的全量数据
存储系统的基本组件
与调度系统类似,Spark 存储系统是一个囊括了众多组件的复合系统,如 BlockManager、BlockManagerMaster、MemoryStore、DiskStore 和 DiskBlockManager 等等
- BlockManager
在 Executors 端负责统一管理和协调数据的本地存取与跨节点传输。
对外,BlockManager 与 Driver 端的 BlockManagerMaster 通信,不仅定期向 BlockManagerMaster 汇报本地数据元信息,还会不定时按需拉取全局数据存储状态
对内,BlockManager 通过组合存储系统内部组件的功能来实现数据的存与取、收与发 - MemoryStore
MemoryStore 用来管理数据在内存中的存取
MemoryStore 支持对象值和字节数组,统一采用 MemoryEntry 数据抽象对它们进行封装。对象值和字节数组二者之间存在着一种博弈关系,所谓的“以空间换时间”和“以时间换空间”,两者的取舍还要看具体的应用场景。 - DiskStore
DiskStore 用来管理数据在磁盘中的存取。
利用 DiskBlockManager 维护的数据块与磁盘文件的对应关系,来完成字节序列与磁盘文件之间的转换。
5. 内存管理基础
内存的管理模式
在管理方式上,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 对堆外可用内存的估算会更精确,对内存的利用率也更有把握。
内存区域的划分
- 堆外内存
Spark 把堆外内存划分为两块区域:一块用于执行分布式任务,如 Shuffle、Sort 和 Aggregate 等操作,这部分内存叫做 Execution Memory;一块用于缓存 RDD 和广播变量等数据,它被称为 Storage Memory。 - 堆内内存
堆内内存的划分方式和堆外差不多,Spark 也会划分出用于执行和缓存的两份内存空间。不仅如此,Spark 在堆内还会划分出一片叫做 User Memory 的内存空间,它用于存储开发者自定义数据结构。
除此之外,Spark 在堆内还会预留出一小部分内存空间,叫做 Reserved Memory,它被用来存储各种 Spark 内部对象,例如存储系统中的 BlockManager、DiskBlockManager 等等。
执行与缓存内存(???)
在 1.6 版本之后,Spark 推出了统一内存管理模式。统一内存管理指的是 Execution Memory 和 Storage Memory 之间可以相互转化
- 如果对方的内存空间有空闲,双方就都可以抢占;
- 对于 RDD 缓存任务抢占的执行内存,当执行任务有内存需要时,RDD 缓存任务必须立即归还抢占的内存,涉及的 RDD 缓存数据要么落盘、要么清除;
- 对于分布式计算任务抢占的 Storage Memory 内存空间,即便 RDD 缓存任务有收回内存的需要,也要等到任务执行完毕才能释放。
6. 补充
- 提交spark任务的模式:standalone模式,yarn client和yarn cluster模式的区别。主要是分清楚client,master,worker,executor,Driver(在哪里,有什么用的问题)
- 问题:有网络传输是否就是发生了shuffle,Shuffle的含义就是洗牌,将数据打散,父RDD一个分区中的数据如果给了子RDD的多个分区(只要存在这种可能),就是shuffle。Shuffle会有网络传输数据,但是有网络传输,并不意味着就是shuffle。,看一下具体的示例:shuffle依赖
- shuffle机制和原理,有哪些shuffle类型,有什么不同的特点或者优势
- HashShuffle和Sorted-Based Shuffle的比较,为什么Sorted-Based Shuffle比HashShuffle要好,从哪些层面考虑呢
- 回答为什么说Spark是内存式计算,回答DAG的问题(血缘)血统(DAG)
- 一个案例:如果对一个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去计算。 - 任务调度的时候不考虑可用内存大小吗?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的可用内存。
- 资源调度和任务调度是分开的。资源调度主要看哪些节点可以启动executors,是否能满足executors所需的cpu数量要求,这个时候,不会考虑任务、数据本地性这些因素。资源调度完成之后,在任务调度阶段,spark负责计算每个任务的本地性,效果就是task明确知道自己应该调度到哪个节点,甚至是哪个executors。最后scheduler Backend会把task代码,分发到目标节点的目标executors,完成任务调度,实现数据不动代码动。
- DAGScheduler 在创建 Tasks 的过程中,是如何设置每一个任务的本地性级别?DAGScheduler会尝试获取RDD的每个Partition的偏好位置信息,a.如果RDD被缓存,通过缓存的位置信息获取每个分区的位置信息;b.如果RDD有preferredLocations属性,通过preferredLocations获取每个分区的位置信息;c. 遍历RDD的所有是NarrowDependency的父RDD,找到第一个满足a,b条件的位置信息
- 关于等待时间和执行时间的平衡?在调度了最契合locality的tasks后还有空闲executor。下一批task本来是有资源可用的,但最适合执行task的executor已被占用,此时会评估下一批tasks等待时间和在空闲executor执行数据传输时间,如果等待时间大于数据传输则直接调度到空闲executor,否则继续等待。把wait参数设置为0,则可以不进行等待,有资源时直接调度执行。看你具体场景。
- 能在Driver段处理的就在Driver处理,比如作者提供的初始化字典的例子,否则每个executor都要初始化一边(Driver端把包含字典对象发给的executor)
- 透过 RDD 缓存看 MemoryStore
- 透过 Shuffle 看 DiskStore
- 为什么用到了LinkedHashMap存储(Block ID, MemoryEntry)?当storage memory不足,spark需要删除rdd cache的时候,遵循的是lru,那么问题来了,它咋实现的lru,答案就是它充分利用LinkedHashMap特性:访问有序
- spark 做shuffle的时候,shuffle write 要写入磁盘,是否可以直接通过内存传输?
- 结合 RDD 数据存储到 MemoryStore 的过程,推演出通过 MemoryStore 通过 getValues/getBytes 方法去访问 RDD 缓存内容的过程吗?
- 考 RDD 缓存存储的过程,推演出广播变量存入 MemoryStore 的流程吗?
- 堆外内存存在的意义是什么,有什么场景是一定需要堆外内存么?Spark不同任务在堆内堆外内存的使用选择上的逻辑?
- 分别计算不同内存区域(Reserved、User、Execution、Storage)的具体大小