大数据学习(三):Spark

78 阅读8分钟

magicpenta.github.io/docs/

《大数据处理框架Apache Spark设计与实现》

概念

image.png

组件

image.png

运行架构

image.png

Driver执行用户程序的main方法创建SparkContext,与Cluster Manager通信,Cluster Manager分配资源,返回可用的Executor列表,有了Executor之后Spark将用户的application jar包传给Executor,再将解析后的tasks传给Executor执行。

image.png

关于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模式下的流转:

image.png

Application Master是Spark特定程序,是分配资源和调度任务的组件。它实际运行的位置,可能在work node,也可能在master node。一般集群好像只有一个master node,高可用的时候会备份resourceManager。

RDD

Resilient Distribute DataSet,弹性分布式数据集,是spark中最重要的一个抽象概念,代表一个不可变、可分区,支持并行计算的数据集。弹性体现在spark的设计中,在存储、计算、容错、分片等方面。

  • 不可变(只读):RDD 是只读的,创建后不可修改,只能通过转换操作(mapjoin 等)生成新的 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)(seqOpcombOp[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 的指定目录下

窄依赖与宽依赖与血缘关系

窄依赖通常存在于 mapfilterunion 等转换操作中,这些转换操作的共同点为一个输入分区对应于一个输出分区。

宽依赖通常发生在 groupByKey、reduceByKey 等转换操作中,它主要表现为 父 RDD 的每个分区对应子 RDD 的多个分区,这些转换操作通常会导致 shuffle 的发生。

血缘关系是 RDD 的重要特性之一,基于 RDD 核心属性 dependencies 实现,它描述了一个 RDD 是如何从初始 RDD 计算得来的。

image.png

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:

模块名称模块职责
DAGSchedulerDAG 调度器,负责阶段(Stage)的划分并生成 TaskSet 传递给 TaskScheduler
TaskSchedulerTask 调度器,决定任务池调度策略,负责 Task 的管理(包括 Task 的提交与销毁)
SchedulerBackend调度后端,维持与 Executor 的通信,并负责将 Task 提交到 Executor

Executor是执行计算任务的实例,是任务调度的终点,包含以下模块:

模块名称模块功能
ThreadPool任务执行线程池,用于执行 Driver 提交过来的 Task
BlockManager存储管理器,为 RDD 提供缓存服务,可提高计算速率
ExecutorBackendExecutor 调度后端,维持与 Driver 的通信,并负责将任务执行结果反馈给 Driver

整个流程大致如下:

image.png

Shuffle

它会对 ShuffleMapStage 中 RDD 的数据进行重分区,以实现不同分区间的数据分组。

image.png

借助 Shuffle,不同分区中属于同一个 key 的数据得以聚合到同一个下游分区中。如图中的 panda,它原本分散在 2 个 RDD 分区,经 Shuffle 后重组到了同一个分区中。

shuffle流程

分为两个阶段:shuffle write和suffle read:

image.png

具体的流程就要深入源码,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")