RDD可以说是Spark Core最核心的内容,这一篇开始我将根据自己的理解针对RDD进行深入的剖析。
注意一些关于Spark Core的简单的概念理解,请参考一下我的这篇博客:
Spark学习笔记二之Spark Core核心概念一网打尽
RDD(Resilient Distributed Datasets) ,直译成中文就是:弹性分布式数据集。
本质上,RDD 其实就是一系列元数据构成的,包含着数据分区列表,计算逻辑, RDD 之间的依赖转换关系等。
RDD和Spark的关系:
Spark建立在抽象的RDD上,使得它可以用一致的方式处理大数据不同的应用场景,把所有需要处理的数据转化成RDD,然后对RDD进行一系列的算子运算,从而得到结果。
Spark统一平台的根基就是RDD,RDD提供了统一的方式支持下列软件库:结构化查询语言Spark SQL,流处理框架Spark Streaming,结构化流处理框架Structured Streaming,机器学习库MLLib,图处理库GraphX等,也就是说上面说的软件库底层的数据抽象都是RDD。
RDD 里面存储着分区列表信息,分区Partition是一个逻辑概念,本质上就是为了分布式计算,分区数决定了并行计算的力度,一个分区就对应了一个 Task 任务。RDD 的并行度默认会从父 RDD 传给 子 RDD。
可以在创建 RDD 时指定分区数,默认分区数,当从集合创建时,为分配到的资源 CPU 核数,当从 HDFS 创建时,为文件的 Block 数。
Partition 和 Block 的关系:
这里的 Block 不是 HDFS 里面的概念,而是 Spark 独有的概念。每个 RDD 的数据都是以 Block 的形式存储在多台机器上面的,可以说,Partition 是数据的逻辑形式,Block 就是数据的物理形式。Partition 和 Block 并不是严格的一一对应关系,Block 是和真实存储的数据强相关,而 Partition 为了高效的分布式计算,肯定会做一些优化的工作,比如某些 Block 过小,会合并起来对应一个 PartitIon,某个Block 过大,也会拆分开来对应多个 Partition。
每个分区都有计算函数,计算函数其实就是分区里面的数据要按照怎样的计算逻辑来处理,函数处理是以 Task 任务的形式进行的。
RDD每次转换都会生成新的 RDD,所以 RDD 会形成类似于流水线的前后依赖关系。这里之所以存储这些依赖信息,是为了系统的容错性考虑的,某个 RDD 出了问题,其他的 RDD 可以通过这些依赖关系转换成出问题的 RDD。RDD 的依赖关系分为窄依赖和宽依赖。
窄依赖 VS 宽依赖
- 之所以搞出来窄依赖和宽依赖的划分,是出于 Pipeline/流水线 的角度考虑,通俗点讲,就是我同一个流水线里面的数据可以在同一个进程里面连续进行处理,不需要跨进程,要知道跨进程往往意味着网络传输,不仅仅慢的一批而且还容易丢数据。
这个控制着分区策略与分区数,每个 key-value 形式的 RDD 都有 Partitioner 属性,它决定了 RDD 如何分区。只针对 kv 形式的 RDD,用户可以自定义分区策略。
RDD 存储了每个 Partition 的优先位置,对于 HDFS 来讲,就是每个 Partition对应的 Block 块的位置。Spark 在进行计算之前,就是通过这个信息找到具体的节点,将计算逻辑发送到这个节点,在存储数据的服务器本地进行计算。
移动计算比移动数据更划算
对于大数据来讲,参与计算的数据往往是 GB/TB 级别的,而计算程序本身则很小(往往KB级别),将大数据通过网络传输到计算程序所在节点上太不划算,网络开销能耗死人。所以将计算程序移动到存储数据的服务器节点上面才是正道。
这个思想实际上我们很早就见过,大家电脑上面都装过杀毒软件,杀毒软件杀毒的时候会从远程下载病毒库到本地,这就是典型的移动计算思想。
弹性在RDD里面是一个很牛逼的概念,很多刚学Spark的人都不太理解弹性究竟是啥意思。实际上弹性有七层含义:
这个是最基本也是最好理解的,内存和磁盘的区别在哪呢——内存又贵又小但是速度比磁盘快好几个数量级!
Spark出于快的考虑肯定支持内存处理数据,但是内存的容量是有限的,Spark支持内存放不下的情况下,自动地放到磁盘里面。当然,这一步还会考虑到放置策略和优化算法,比如尽量连续存储等。
RDD是一个抽象的或者说是一个逻辑上的概念,它根本不存储实际数据(指的需要经过集群分布式计算的那些数据),它存储了RDD的由来:包括父RDD是谁,父RDD是怎样转化成当前RDD的即所谓的Lineage(血统)。
这样做有什么好处呢?
当发生错误的时候,比如某个RDD计算过程中发生了节点故障造成了数据丢失,那么我们根据父RDD以及转化关系就很容易找回丢失的那份数据。
注意:
- 这里的数据不是存储在RDD里面的,而是RDD下面的分区所对应的物理存储,在HDFS集群上面就是block块。
- 这里要理解对于分布式集群来说,单节点故障,不会影响到其他节点上面存储的备份冗余数据。
Task是一整个计算任务(Application)的最底层逻辑,对应的就是一个线程,同时一个分区(partition)就对应一个Task。
Task的生命周期由TaskScheduler的实现类TaskSchedulerImpl掌控,TaskSchedulerImpl来从DAGScheduler获取到TaskSet(Task的集合),会运行这些Task,运行失败自动重试,默认重试次数为4次。
Stage本意就是阶段的意思,对应Spark来讲,如果能一直在一个节点上计算无疑效率最高,但是很多时候没有那么理想化,故Spark以Shuffle为分割点(Shuffle意味着跨节点),切分成了一个个阶段,在每个Stage中,计算任务以流水线的形式进行(Pipeline)。
Stage的生命周期由DAGScheduler来掌控,默认重试次数也是4次。
checkpoint是对RDD进行的标记,会产生一系列的文件,且所有父依赖都会被删除,是整个依赖的终点。
checkpoint 也是 lazy 级别的,可以通过主动或者被动的触发。cache/persist 后 RDD 工作时每个工作节点都会把计算的分片结果保存在内存或者磁盘中,下次如果对相同的 RDD 进行其他的 Action 计算,就可以重用。
checkpoint对比 cache/persist
checkpoint是一个job来完成的,是执行完一个job之后,新建一个新的 job 来完成的,并不像 cache,是 job 执行过程中进行。
Spark 自动管理(包括创建和回收)cache 和 persist 持久化的数据,而 checkpoint 持久化的数据需要由用户自己管理
checkpoint 会清除 RDD 的 Lineage,避免 Lineage 过长导致序列化开销增大,而 cache 和 persist 不会清除 RDD 的 Lineage
checkpoint针对整个 RDD 计算链条中特别需要数据持久化的环节,进行基于 HDFS 等的数据持久化复用策略,而 cache/persist 只是存储在本地内存或者磁盘上,checkpoint 明显可靠性和容错性更高。
对比 | checkpoint | cache/persist |
---|---|---|
自动管理 | 是 | 否 |
清除 Lineage | 是 | 否 |
存储 | HDFS | 本地内存/磁盘 |
可靠性 | 高 | 低 |
同一个 Job | 否 | 是 |
Spark 将执行模型抽象为通用的有向无环图(DAG),DAG 的生成与具体的资源管理无关,当发生节点运行故障的时候,可有其他的可用节点代替故障节点运行(例如 HDFS 上的副本机制)。
在 DAG 中,每个点就是一个任务,每一条边就是对应了 Spark 中的依赖关系。
在 Spark 中,DAG 生成的流程关键在于回溯。在程序提交后,高层调度器(DAGScheduler)会将所有的 RDD 看成一个 Stage,然后对这个 Stage 进行从后往前的回溯,遇到 Shuffle 就断开,遇到窄依赖就归并到同一个 Stage 里面,等到所有的步骤回溯完成,便生成了一个 DAG 图。
在计算过程中,可能会产生很多的数据碎片,这时产生一个 Partition 可能会非常小,一个 Partition 要一个 Task 去处理,一个 Task 对应一个线程,这时候就会降低系统的处理效率,所以,Spark 会讲许多小的 Partition 合并成一个较大的 Partition 去处理,同时,如果一个 Block 很大,也会考虑把 Partition 拆分成更小的数据分片,这样 Spark 就能够处理更多的批次,不容易 OOM。
参考:
《Spark 大数据商业实战三部曲:内核解密|商业案例|性能调优》 -王家林/段智华/夏阳 著
个人博客:
https://www.shockang.com