Spark—RDD架构浅析

74 阅读8分钟

一、前言

团队技术之美FY22财年S1阶段学习了Spark RDD架构以及原理,业余时间对Spark、Hadoop生态进行了学习和总结,并在团队内部进行了架构分享。

二、产生背景

MapReduce作业流程

痛点

map task与reduce task的执行是分布在不同的节点上的,reduce执行时需要跨节点去拉取其他节点上的map task结果,这样造成了集群内部的网络资源消耗很严重,而且在节点的内部,相比于内存,磁盘IO对性能的影响是非常严重的。

期望解决的问题

  • 完整地从map task端拉取数据到Reduce端;
  • 在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗;
  • 减少磁盘IO对task执行的影响。

三、RDD概念与介绍

RDD的具体描述RDD(弹性分布式数据集)是Spark提供的最重要的抽象的概念,它是一种有容错机制的特殊集合,可以分布在集群的节点上,以函数式编程操作集合的方式,进行各种并行操作。可以将RDD理解为一个具有容错机制的特殊集合,它提供了一种只读、只能由已存在的RDD变换而来的共享内存,然后将所有数据都加载到内存中,方便进行多次重用。

属性

如何解决迭代计算和交互式计算

  • Spark解决迭代计算:主要实现思想就是RDD,把所有计算的数据保存在分布式的内存中。数据在内存中将大大提升IO操作。这也是Spark涉及的核心:内存计算。
  • Spark实现交互式计算:因为Spark是用scala语言实现的,Spark和scala能够紧密的集成,所以Spark可以完美的运用scala的解释器,使得其中的scala可以向操作本地集合对象一样轻松操作分布式数据集

RDD分区partition

分区是RDD内部并行计算的一个计算单元,RDD的数据集在逻辑上被划分为多个分片,每一个分片称为分区,分区的格式决定了并行计算的粒度,而每个分区的数值计算都是在一个任务中进行的,因此任务的个数,也是由RDD(准确来说是作业最后一个RDD)的分区数决定。

为什么要分区

  • mapreduce里面网络传输主要在shuffle阶段,shuffle的根本原因是相同的key存在不同的节点上,按key进行聚合的时候不得不进行shuffle。shuffle是非常影响网络的,它要把所有的数据混在一起走网络,然后它才能把相同的key走到一起。
  • 分区后对RDD进行操作时,可以对每个分区的数据并行操作

partition与Hbase中block对比分析

  • Hdfs中block是分布式存储中的最小单元,spark的partiton是弹性分布式数据集RDD的最小单元
  • Block位于存储空间,partition 位于计算空间
  • Block是有冗余的、不易轻易丢失,partion没有冗余设计、丢失之后重新计算

RDD分区函数

RDD分区函数可分为HashPartition(哈希分区)和RangePartition(范围分区)

哈希分区:

  • 其数据分区规则为 partitionId = Key.hashCode % numPartitions,其中partitionId代表该Key对应的键值对数据应当分配到的Partition标识,Key.hashCode表示该Key的哈希值,numPartitions表示包含的Partition个数。
  • 缺点:有分区倾斜问题,即数据量不均衡

范围分区:

  • 采用哈希的方式将同一类型的Key分配到同一个Partition中
  • 缺点:可能会导致若干个分区包含的数据过大

以上分区方法不满足需求,还可以自定义分区函数custompartition,同时,需要合理设置分区:

  • 分区太多意味着任务太多,导致总体耗时太多
  • 分区太少,导致每个分区要处理的数据量增大,对节点的内存要求提高,还会导致数据倾斜问题。

计算函数

compute&iterator

Job逻辑执行图

  • RDD不实际存储真正要计算的数据,而是记录了数据的位置在哪里,数据的转换关系(调用了什么方法,传入什么函数)
  • RDD中的所有转换都是惰性求值/延迟执行的,也就是说并不会直接计算。只有当发生一个要求返回结果给Driver的Action动作时,这些转换才会真正运行。
  • 之所以使用惰性求值/延迟执行,是因为这样可以在Action时对RDD操作形成DAG有向无环图进行Stage的划分和并行优化,这种设计让Spark更加有效率地运行。

RDD依赖关系

**窄依赖:**父RDD的一个分区只会被子RDD的一个分区依赖。即一对一或者多对一的关系,可理解为独生子女。 常见的窄依赖有:map、filter、union、mapPartitions、mapValues、join(父RDD是hash-partitioned)等。

**宽依赖:**父RDD的一个分区会被子RDD的多个分区依赖(涉及到shuffle)。即一对多的关系,可理解为超生。 常见的宽依赖有groupByKey、partitionBy、reduceByKey、join(父RDD不是hash-partitioned)等。

设计的好处:

  • 窄依赖的多个分区可以并行计算;
  • 窄依赖的一个分区的数据如果丢失只需要重新计算对应的分区的数据就可以了

DAG构建&划分Stage

RDD算子构建了RDD之间的关系,整个计算过程形成了一个由RDD和依赖关系构成的DAG:

Spark 的计算发生在RDD的Action操作,而对Action之前的所有Transformation,Spark只是记录下RDD生成的轨迹,而不会触发真正的计算。

划分Stage依据

划分依据:宽依赖,像reduceByKey,groupByKey等算子,会导致宽依赖的产生。

核心算法:回溯算法—从后往前回溯/反向解析,遇到窄依赖加入本Stage,遇见宽依赖进行Stage切分。

好处:同一个Stage可以有多个Task任务并行执行,这样大大提高了计算的效率。

容错机制CheckPoint

持久化的局限

持久化/缓存可以把数据放在内存中,虽然是快速的,但是也是最不可靠的;也可以把数据放在磁盘上,也不是完全可靠的!例如磁盘会损坏等。

CheckPoint

Checkpoint的产生就是为了更加可靠的数据持久化,在Checkpoint的时候一般把数据放在在HDFS上,这就天然的借助了HDFS天生的高容错、高可靠来实现数据最大程度上的安全,实现了RDD的容错和高可用。

两者区别:

  • 位置:Persist 和 Cache 只能保存在本地的磁盘和内存中;Checkpoint 可以保存数据到 HDFS 这类可靠的存储上。
  • 生命周期:Cache和Persist的RDD会在程序结束后会被清除或者手动调用unpersist方法
    Checkpoint的RDD在程序结束后依然存在,不会被删除。

最佳实践:可以对频繁使用且重要的数据,先做缓存/持久化,再做checkpint操作。

四、内存管理

Spark集群会启动Driver和Executor两种JVM进程,前者为主控进程,负责创建Spark上下文,提交Spark作业(Job),并将作业转化为计算任务(Task),在各个Executor进程间协调任务的调度,后者负责在工作节点上执行具体的计算任务,并将结果返回给Driver,同时为需要持久化的RDD提供存储功能。

Executor的内存管理建立在JVM的内存管理之上,Spark对JVM的堆内(On-heap)空间进行了更为详细的分配,以充分利用内存。同时,Spark引入了堆外(Off-heap)内存,使之可以直接在工作节点的系统内存中开辟空间,进一步优化了内存的使用

静态内存管理

很容易造成"一半海水,一半火焰"的局面,即存储内存和执行内存中的一方剩余大量的空间,而另一方却早早被占满,不得不淘汰或移出旧的内容以存储新的内容

动态内存管理

统一内存管理—动态占用机制

五、Spark任务调度

DAGScheduler通过TaskScheduler接口提交任务集,这个任务集最终会触发TaskScheduler构建一个TaskSetManager的实例来管理这个任务集的生命周期,对于DAGScheduler来说,提交调度阶段的工作到此就完成了。

Exector 进程专属

每个Application获取专属的Executor进程,该进程在Application期间一直驻留,并以多线程方式运行Tasks。

支持多种资源管理器

Job提交就近原则

移动程序而非移动数据的执行原则

移动程序而非移动数据的原则执行,Task采用了数据本地性和推测执行的优化机制。

六、业界生态对比

  • 尽管Spark相对于Hadoop而言具有较大优势,但Spark并不能完全替代Hadoop,Spark主要用于替代Hadoop中的MapReduce计算模型。存储依然可以使用HDFS,但是中间结果可以存放在内存中;调度可以使用Spark内置的,也可以使用更成熟的调度系统YARN等。
  • 实际上,Spark已经很好地融入了Hadoop生态圈,并成为其中的重要一员,它可以借助于YARN实现资源调度管理,借助于HDFS实现分布式存储。
  • 此外,Hadoop可以使用廉价的、异构的机器来做分布式存储与计算,但是,Spark对硬件的要求稍高一些,对内存与CPU有一定的要求。

七、参考资料与书籍

GitBook:

spark源码剖析

spark架构设计与实现