《大数据处理框架Apache Spark设计与实现》
概念
组件
运行架构
Driver执行用户程序的main方法创建SparkContext,与Cluster Manager通信,Cluster Manager分配资源,返回可用的Executor列表,有了Executor之后Spark将用户的application jar包传给Executor,再将解析后的tasks传给Executor执行。
关于Spark的部署,也是有常见的几种如Standalone、Yarn、Kubenetes等。
提交流程
首先提交的命令是要会的,而且一般是shell命令提交。
${Spark_HOME}/bin/spark-submit \
--name "${job_name}" \
--class "${class_name}" \ # 入口!用户程序main函数所在
--master yarn \ # 指定 Spark 应用的运行模式,可以是 local、yarn、spark://master:7077 等
--deploy-mode cluster \ # 指定 Spark 应用的部署模式,可以是 client 或 cluster,默认为 client
--conf xxx
--jars "${custom_jar}"/xxx.jar
yarn cluster模式下的流转:
Application Master是Spark特定程序,是分配资源和调度任务的组件。它实际运行的位置,可能在work node,也可能在master node。一般集群好像只有一个master node,高可用的时候会备份resourceManager。
RDD
Resilient Distribute DataSet,弹性分布式数据集,是spark中最重要的一个抽象概念,代表一个不可变、可分区,支持并行计算的数据集。弹性体现在spark的设计中,在存储、计算、容错、分片等方面。
- 不可变(只读):RDD 是只读的,创建后不可修改,只能通过转换操作(
map、join等)生成新的 RDD - 可分区:RDD 可以分成多个分区,每个分区对应大数据集群上的一个数据片段
- 支持并行计算:RDD 的不同分区可对应集群上不同节点的数据片段,进而支持并行计算
分区
分区是RDD中的核心属性。假设有100个并行度(支持并行计算的任务数,通常希望与核数对应),则有100个分区就比较好了,一个任务跑一个分区。
RDD算子
转换算子
| 操作 | 含义 |
|---|---|
| filter(func) | 筛选出满足条件的元素,并返回一个新的数据集 |
| map(func) | 将每个元素传递到函数 func 中,返回一个新的数据集,每个输入元素会映射到 1 个输出结果 |
| flatMap(func) | 与 map 相似,但每个输入元素都可以映射到 0 或多个输出结果 |
| mapPartitions(func) | 与 map 相似,但是传递给函数 func 的是每个分区数据集对应的迭代器 |
| distinct(func) | 对原数据集进行去重,并返回新的数据集 |
| groupByKey( [numPartitions] ) | 应用于 (K, V) 形式的数据集,返回一个新的 (K, Iterable) 形式的数据集,可通过 numPartitions 指定新数据集的分区数 |
| reduceByKey(func, [numPartitions] ) | 应用于 (K, V) 形式的数据集,返回一个新的 (K, V) 形式的数据集,新数据集中的 V 是原有数据集中每个 K 对应的 V 传递到 func 中进行聚合后的结果 |
| aggregateByKey(zeroValue)(seqOp, combOp, [numPartitions] ) | 应用于 (K, V) 形式的数据集,返回一个新的 (K, U) 形式的数据集,新数据集中的 U 是原有数据集中每个 K 对应的 V 传递到 seqOp 与 combOp 的联合函数且与 zeroValue 聚合后的结果 |
| sortByKey( [ascending] , [numPartitions] ) | 应用于 (K, V) 形式的数据集,返回一个根据 K 排序的数据集,K 按升序或降序排序由 ascending 指定 |
| union(func) | 将两个数据集中的元素合并到一个新的数据集 |
| join(func) | 表示内连接,对于给定的两个形式分别为 (K, V) 和 (K, W) 的数据集,只有在两个数据集中都存在的 K 才会被输出,最终得到一个 (K, (V, W)) 类型的数据集 |
| repartition(numPartitions) | 对数据集进行重分区,新的分区数由 numPartitions 指定 |
行动算子
行动算子(actions)不返回新的 RDD,会触发计算任务的执行。
| 操作 | 含义 |
|---|---|
| count() | 返回数据集中的元素个数 |
| countByKey() | 仅适用于 (K, V) 形式的数据集,以 (K, Int) 形式的 Map 返回每个 K 的元素个数 |
| collect() | 以数组的形式返回数据集中的所有元素 |
| first() | 返回数据集中的第一个元素 |
| take(n) | 以数组的形式返回数据集中的前 n 个元素 |
| reduce(func) | 通过函数 func(输入两个参数并返回一个值)聚合数据集中的元素 |
| foreach(func) | 将数据集中的每个元素传递到函数 func 中运行 |
| saveAsTextFile(path) | 将数据集以文本格式写到本地磁盘或 HDFS 的指定目录下 |
| saveAsSequenceFile(path) | 将数据集以 SequenceFile 格式写到本地磁盘或 HDFS 的指定目录下,仅适用于 (K, V) 形式且 K 和 V 均实现了 Hadoop Writable 接口的数据集 |
| saveAsObjectFile(path) | 将数据集序列化成对象保存至本地磁盘或 HDFS 的指定目录下 |
窄依赖与宽依赖与血缘关系
窄依赖通常存在于 map、filter、union 等转换操作中,这些转换操作的共同点为一个输入分区对应于一个输出分区。
宽依赖通常发生在 groupByKey、reduceByKey 等转换操作中,它主要表现为 父 RDD 的每个分区对应子 RDD 的多个分区,这些转换操作通常会导致 shuffle 的发生。
血缘关系是 RDD 的重要特性之一,基于 RDD 核心属性 dependencies 实现,它描述了一个 RDD 是如何从初始 RDD 计算得来的。
RDD持久化
RDD 持久化是 Spark 中一个很重要的特性。通过这个特性,Spark 可以在内存或磁盘缓存某个 RDD,当其他行动算子需要这个 RDD 时直接复用它,以 避免重新计算,进而提高性能。
上面一句避免重复计算是指,基于 RDD B 拆解出的计算任务在重复发布到 Executor 执行时,会直接读取缓存中的计算结果,不会触发实际的计算。
RDD持久化使用persist()方法实现,可以传入 存储级别 的参数(部份):
| 持久化级别 | 含义 |
|---|---|
| MEMORY_ONLY | 将 RDD 以反序列化 Java 对象的形式存储在 JVM 中,如果大小超过可用内存,则超出部分不会缓存,需重新计算 |
| MEMORY_AND_DISK | 将 RDD 以反序列化 Java 对象的形式存储在 JVM 中,如果大小超过可用内存,则超出部分会存在在磁盘上,当需要时从磁盘读取 |
| DISK_ONLY | 将所有 RDD 分区存储到磁盘上 |
CheckPoint
与持久化有点像,但又有区别:
| 区别项 | RDD 持久化 | RDD 检查点 |
|---|---|---|
| 生命周期 | 应用结束便删除 | 永久保存 |
| 血缘关系 | 不切断 | 切断 |
| 使用场景 | 支持在同一个应用中复用计算结果 | 支持在多个应用中复用计算结果 |
任务调度
基本概念
| 名称 | 含义 |
|---|---|
| Application | 指用户提交的 Spark 应用程序 |
| Job | 指 Spark 作业,是 Application 的子集,由行动算子(action)触发 |
| Stage | 指 Spark 阶段,是 Job 的子集,以 RDD 的宽依赖为界 |
| Task | 指 Spark 任务,是 Stage 的子集,Spark 中最基本的任务执行单元,对应单个线程,会被封装成 TaskDescription 对象提交到 Executor 的线程池中执行 |
Driver与Executor
Driver是任务调度的关键部分,运行用户程序main()函数、创建SparkContext示例。
在一个完整的任务调度中,用户提交的程序会经历 Application → Job → Stage → Task 的转化过程,依赖下面3个scheduler:
| 模块名称 | 模块职责 |
|---|---|
| DAGScheduler | DAG 调度器,负责阶段(Stage)的划分并生成 TaskSet 传递给 TaskScheduler |
| TaskScheduler | Task 调度器,决定任务池调度策略,负责 Task 的管理(包括 Task 的提交与销毁) |
| SchedulerBackend | 调度后端,维持与 Executor 的通信,并负责将 Task 提交到 Executor |
Executor是执行计算任务的实例,是任务调度的终点,包含以下模块:
| 模块名称 | 模块功能 |
|---|---|
| ThreadPool | 任务执行线程池,用于执行 Driver 提交过来的 Task |
| BlockManager | 存储管理器,为 RDD 提供缓存服务,可提高计算速率 |
| ExecutorBackend | Executor 调度后端,维持与 Driver 的通信,并负责将任务执行结果反馈给 Driver |
整个流程大致如下:
Shuffle
它会对 ShuffleMapStage 中 RDD 的数据进行重分区,以实现不同分区间的数据分组。
借助 Shuffle,不同分区中属于同一个 key 的数据得以聚合到同一个下游分区中。如图中的 panda,它原本分散在 2 个 RDD 分区,经 Shuffle 后重组到了同一个分区中。
shuffle流程
分为两个阶段:shuffle write和suffle read:
具体的流程就要深入源码,spark的核心阶段。
Spark实践
个人spark在实际工作中主要用在两个地方:
1、大规模数据批处理(java + sql)
2、创建处理hive表与分区(shell + sql)
上面两者最终都需要一个shell脚本来提交执行spark任务。
大规模数据批处理
作用:替代传统的MapReduce,处理TB/PB级数据
场景:ETL流程、数据清洗、报表生成
优势:比Hadoop MapReduce快10-100倍
这里给出实际工作中的例子:
SparkConf sparkConf = new SparkConf();
// 设置一些参数
SparkSession sparkSession = SparkSession.builder().config(sparkConf)
.enableHiveSupport().getOrCreate();
DataSet<Row> dataset = sparkSession(sql);
Java<Row> rowJavaRDD = dataSet.toJavaRDD();
// foreachPartition是一个Action算子,会触发整个作业的执行
rowJavaRDD.foreachPartition(new voidFunction<Iterator<Row>>() {
@Override
public void call(Iterator<Row> iterator) throw Exception {
// 处理数据
}
});
import org.apache.spark.api.java.JavaRDD;
import org.apache.spark.sql.SparkSession;
public class LogCleaning {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder()
.appName("LogCleaning")
.master("local[*]")
.getOrCreate();
// 读取原始日志文件(格式:时间戳,请求路径,状态码)
JavaRDD<String> rawLogs = spark.sparkContext()
.textFile("logs/access.log", 2) // 不处理hdfs这样的大数据,处理文档表格也可以
.toJavaRDD();
// 过滤出状态码为200的请求
JavaRDD<String> validLogs = rawLogs.filter(line -> {
String[] parts = line.split(",");
return parts.length >= 3 && parts[2].equals("200");
});
// 保存清洗后的数据
validLogs.saveAsTextFile("output/clean_logs");
spark.stop();
}
}
操作hive表分区
执行hive DDL/DML
-- 创建 Hive 表(通过 Spark SQL)
spark.sql("""
CREATE EXTERNAL TABLE hive_db.user_logs (
user_id STRING,
action_time TIMESTAMP,
page_url STRING
) STORED AS PARQUET
LOCATION '/data/user_logs'
""")
-- 插入数据
spark.sql("INSERT INTO hive_db.user_logs VALUES ('u1001', CURRENT_TIMESTAMP(), '/home')")
兼容hive分区表
// 按分区查询(自动优化,仅扫描符合条件的分区)
spark.sql("SELECT * FROM hive_db.sales WHERE dt='2023-08-20' AND region='CN'")
// 动态分区写入
df.write.partitionBy("dt", "region").saveAsTable("hive_db.sales")