Spark 逻辑处理流程与物理执行计划

一直以来都想了解一下 Spark 的运行原理, 但一直都浮于表面, 难以深入. 去年买了一本 《大数据处理框架 Apache Spark 设计与实现》, 但是一直没时间好好看看, 最近抽时间过了一下这本书, 在此记录一下.

这一篇主要讲 Spark 中逻辑处理流程时怎么生成的, 以及在逻辑处理流程的基础上如何生成物理执行计划.

1. 逻辑处理流程

Spark 应用的逻辑处理流程指应用的计算过程在 Spark 内部的表示, 主要包括四部分:

  • 数据源. 即原始数据, 可以存放在本地文件系统, 分布式文件系统, 或者来自网络流等;
  • 数据模型. 即输入 / 输出, 中间数据在 Spark 内的表示, 其中最基础的一个便是 RDD (Resilient Distributed Datasets), 其可以包含多个数据分区, 不同数据分区可以由不同的任务在不同节点处理;
  • 数据操作. 即 Spark 中对 RDD 的各种操作, 分为两种: transformation, action. action 类的操作会触发 Spark 提交 job 真正执行数据处理任务. transformation 类的操作是单向的, 即 RDD 进行 transformation 后会生成新的 RDD, 而不是对 RDD 本身进行修改;
  • 计算结果处理. 由于 RDD 是分布在不同机器上的, 应用的结果计算方式分为两种: 1) 直接将计算结果保存到分布式文件系统中; 2) 将计算结果汇集到 Driver 端进行集中计算.

逻辑处理流程等价于计算过程中涉及到的 RDD 及其之间的依赖关系. 除了输入数据转化成的 RDD, Spark 中的数据操作也会生出 RDD, 不同的操作会产生不同的依赖关系, 但总的可以分为两大类: 窄依赖和宽依赖.

RDD 中的依赖关系

1.1. 窄依赖

子 RDD 中每个分区都依赖于父 RDD 中的一部分分区, 如 RDD-Dependency(a) 所示. 可以分成四种窄依赖:

  • 一对一依赖 (OneToOneDependency). 子 RDD 和父 RDD 的分区个数相同, 且存在一一映射关系, 典型的 transformation 包括 map(), filter();
  • 区域依赖 (RangeDependency). 子 RDD 和父 RDD 的分区经过区域化后存在一一映射关系, 如 union();
  • 多对一依赖 (ManyToOneDependency). 子 RDD 中的一个分区依赖多个父 RDD 中的分区, 如 join();
  • 多对多依赖 (ManyToManyDependency). 子 RDD 中的一个分区依赖父 RDD 中的多个分区, 同时发父 RDD 中的一个分区被子 RDD 中的多个分区依赖, 如 cartesian().

ManyToOneDependency 和 ManyToManyDependency 是我参考《大数据处理框架 Apache Spark 设计与实现》引入的, 实际在 Spark 代码中并没有对这两种依赖进行命名.

1.2. 宽依赖

子 RDD 中的分区依赖父 RDD 中每个分区的一部分, 如 RDD-Dependency(b) 所示. 典型的宽依赖 transformation 包括 groupByKey(), partitionBy(), reduceByKey() 等.

窄, 宽依赖的区别在于子 RDD 的各个分区是否完全依赖 (指父 RDD 中的一个分区不需要进行划分就可以流入子 RDD 的分区中)父 RDD 的一个或多个分区.

为什么要进行数据以来关系进行分类呢? 1) 明确 RDD 分区之间的数据依赖关系, 执行时可以确定从哪里获得数据, 输出数据到哪; 2) 有利于生成物理执行计划. 同一 stage 内的窄依赖的分区可以并行执行, 宽依赖需要进行 Shuffle.

1.3. 数据分区方法

RDD 内部的数据是如何分区的. Spark 中的分区方法:

  • 水平划分. 按照 record 的索引进行划分;
  • Hash 划分 (HashPartitioner). 使用 record 的 Hash 值对数据进行划分;
  • Range 划分 (RangePartitioner). 按照元素的序关系进行划分;
  • 自定义划分. 按照自己的需求实现 Partitioner.

2. 物理执行计划

物理执行计划即 Spark 是如何完成计算的. Spark 以逻辑执行 (即 RDD 组成的数据依赖有向无环图) 计划为基础生成物理执行计划. HOW?

先上结论. Spark 引用由 job 组成, 每个 action 操作对应一个 job. 一个 job 可以分成多个串行的 stage, 每个 stage 由若干个 task 组成, 同一 stage 内的 task 可以并行执行. 因此重点就落到了 stage, task 的划分.

2.1. job \(\longrightarrow\) stage

从输入到输出 (action 操作), 一个 job 的处理流程会经过多次的处理 (transformation 操作), 每次操作产生的 RDD 间形成依赖关系. 对于每个 job, 从其最后的 RDD 往前回溯整个逻辑处理流程, 如果遇到窄依赖, 则将当前 RDD 的父 RDD 纳入, 并继续往前回溯; 若遇到宽依赖, 则停止回溯, 将当前以纳入的所有 RDD 按照其依赖关系组成一个 stage. 简单而言: 在 RDD 组成的依赖图中, 在宽依赖处断开, 得到的每个零散的部分就是一个 stage. 如果把逻辑处理流程看成一个有向无环图 (DAG), 每个 stage 就是一个连通分量. 一个 job 内的 stage 一般是串行的, 但是有些情况下也可以并行, 如逻辑处理流程中存在读个输入分支的情况.

2.2. stage \(\longrightarrow\) task

Spark 并不是以 stage 为粒度执行的, 还可以划分为 task. 注意到 Spark 中每个分区的计算逻辑是相同的且独立的, 因此每个分区上的计算可以独立成为一个 task. Spark 按照每个 stage 中最后一个 RDD 中的分区个数决定生成的 task 数. 同一 stage 内的task 可以并行执行.

posted @ 2022-10-31 15:29  Milkha  阅读(178)  评论(0编辑  收藏  举报