一、引言:为什么我们需要Spark?
在之前的大数据系列博客中,我们深入学习了Hadoop的两大核心:HDFS (分布式存储)和MapReduce(分布式计算)。MapReduce的出现,第一次让人类拥有了处理海量数据的能力。
然而,随着互联网的飞速发展,人们对大数据计算提出了更苛刻的要求:实时性、迭代计算、机器学习、图计算。传统的MapReduce受限于其"读写磁盘"的机制,在迭代计算(如机器学习算法)和交互式查询场景下,性能极其低下。
2009年,加州伯克利AMP实验室诞生了Spark 。它汲取了MapReduce的设计经验,基于内存计算 和DAG(有向无环图),实现了比MapReduce快几十倍甚至上百倍的性能。今天,我们就来揭开Spark Core的神秘面纱。
二、Spark 运行架构与核心角色
Spark 采用了主从架构(Master-Worker)。结合架构图,我们可以清晰看到其核心角色:
| 角色 | 职责 | 类比 |
|---|---|---|
| Driver(驱动器) | 执行 main 方法,创建 SparkContext;将程序转化为 Job;调度 Task;跟踪 Executor 执行情况。 | 包工头/项目经理 |
| Executor(执行器) | 运行在 Worker 节点上的 JVM 进程,负责执行具体的 Task,并将结果返回给 Driver;提供内存缓存。 | 搬砖工人 |
| Master / ResourceManager | 负责资源的调度和分配,进行集群监控。 | 资源调度中心 |
| Worker / NodeManager | 负责管理当前节点的资源,启动 Executor。 | 工地负责人 |
Spark On Yarn 执行流程
当我们将 Spark 提交到 Yarn 上时,流程如下:
-
任务提交:Client 提交 Application 到 ResourceManager。
-
启动 Driver:RM 分配一个 Container,在其中启动 AppMaster(在 Spark 中即 Driver)。
-
申请资源:Driver 向 RM 申请 Executor 资源。
-
启动 Executor :RM 分配资源,Driver 与 NodeManager 通信启动 Executor。Executor 启动后反向注册给 Driver。
-
执行任务:Driver 执行 main 函数,遇到 Action 算子时触发 Job,划分 Stage,生成 Task 并分发到 Executor 执行。
三、Spark核心抽象:RDD(弹性分布式数据集)
RDD(Resilient Distributed Dataset)是 Spark 中最基本的数据抽象。代码中有一句经典的话:RDD 代表着一个不可变、带有分区的集合,可以被并行操作。
3.1 RDD 的五大特性
-
A list of partitions(分区):RDD 逻辑上被划分成多个分区,数据分布在不同的节点上。
-
A function for computing each split(计算函数):每个分区独立运行计算函数。
-
A list of dependencies on other RDDs(血缘/依赖):RDD 是不可变的,转换会产生新的 RDD,形成血缘关系,用于故障恢复。
-
Partitioner for key-value RDDs(分区器):Key-Value 类型的 RDD 可以指定分区策略(如 Hash 分区)。
-
Preferred locations(最优位置):Spark 会将计算任务分配到数据所在的节点(数据本地性)。
3.2 RDD 的创建
python
from pyspark import SparkContext, SparkConf
conf = SparkConf().setMaster("local[*]").setAppName("RDD_Demo")
sc = SparkContext(conf=conf)
# 1. 从集合(内存)中创建
rdd1 = sc.parallelize([1, 2, 3, 4, 5])
# 2. 从外部存储(HDFS、本地文件等)创建
rdd2 = sc.textFile("hdfs://master:9000/wc.txt")
# 3. 从其他 RDD 转换创建
rdd3 = rdd1.map(lambda x: x * 2)
四、RDD 编程:转换算子与行动算子
Spark RDD 的算子分为两类:
-
Transformations(转换算子) :返回一个新的 RDD,是惰性 的(Lazy),遇到行动算子才会执行。如
map、flatMap、filter。 -
Actions(行动算子) :触发计算,返回非 RDD 结果(如数组、数字)或保存数据。如
collect、count、reduce、saveAsTextFile。
4.1 常见的转换算子
-
map(func):对每个元素应用函数。 -
mapPartitions(func):以分区为单位处理数据,效率比map高,但容易 OOM。 -
flatMap(func):先将数据扁平化,再 map(一行转多行)。 -
filter(func):过滤数据。 -
reduceByKey(func):针对 (K, V) 结构的 RDD,按 Key 聚合。(大数据最常用) -
groupByKey():按 Key 分组,会产生大量网络传输,尽量少用。
python
# WordCount 经典案例
rdd = sc.textFile("hdfs://master:9000/wc.txt")
result = rdd.flatMap(lambda line: line.split(" ")) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda a, b: a + b) \
.collect()
print(result)
4.2 常见的行动算子
-
collect():拉取所有数据到 Driver 端(数据量大时慎用,易 OOM)。 -
count():返回元素个数。 -
take(n):取前 n 个元素。 -
reduce(func):聚合数据集。 -
foreach(func)与foreachPartition(func):遍历数据,常用于向外部数据库写入数据。
写入 MySQL :
python
def write_to_mysql(iterator):
import pymysql
conn = pymysql.connect(host='master', user='root', password='123456', database='bigdata')
cursor = conn.cursor()
for row in iterator:
cursor.execute("insert into t_student values(%s, %s)", row)
conn.commit()
conn.close()
# 使用 foreachPartition 减少连接创建次数(每个分区创建一个连接)
data.foreachPartition(write_to_mysql)
五、RDD 的依赖关系与 DAG 划分
5.1 宽依赖与窄依赖
Spark 在执行时,会根据算子推导出依赖关系。
-
窄依赖(Narrow Dependency) :父 RDD 的一个分区只对应子 RDD 的一个分区。例如:
map、filter、union。不会产生 Shuffle。 -
宽依赖(Wide Dependency / ShuffleDependency) :父 RDD 的多个分区对应子 RDD 的一个分区。例如:
groupByKey、reduceByKey。会产生 Shuffle(大量磁盘 IO 和网络传输)。
5.2 Stage 的划分(黄金法则)
Spark 会根据宽依赖进行 Stage(阶段) 的划分。
-
规则:遇到一个宽依赖,就切一刀,划分出一个新的 Stage。
-
公式 :
Stage 总数 = 宽依赖个数 + 1 -
DAG(有向无环图):所有的 RDD 转换过程构成了 DAG。
5.3 任务层级
Spark 任务的层级包含四层:
-
Application :一个
SparkContext对应一个应用(一个建筑公司)。 -
Job:一个 Action 算子触发一个 Job(一个工程项目)。
-
Stage:根据宽依赖划分(一个施工阶段)。
-
Task:Stage 中最后一个 RDD 的分区数,决定了 Task 的数量(一个工人)。
六、RDD 的高级特性
6.1 持久化(Cache / Persist)与 Checkpoint
将多次使用的 RDD 缓存到内存中,避免重复计算。
-
存储级别:
MEMORY_ONLY(默认)、MEMORY_AND_DISK等。 -
生命周期:程序结束或调用
unpersist()后释放。 -
特点:保留了血缘关系(血统)。
python
rdd2.cache() # 默认缓存到内存
rdd2.count() # 第一次触发,真正计算并缓存
rdd2.collect() # 第二次使用,直接从内存读取,速度极快
Checkpoint(检查点)机制:
将数据写入 HDFS 等可靠存储。
-
存储级别:磁盘(高可靠)。
-
生命周期:永久保存,除非手动删除。
-
特点:斩断了血缘关系。一旦切断了血缘,如果 Checkpoint 后的数据丢失,Spark 将从 Checkpoint 的 HDFS 文件重新读取,而不是从头开始算。
Cache vs Checkpoint:
| 对比维度 | Cache / Persist | Checkpoint |
|---|---|---|
| 存储位置 | 内存 / 磁盘(Executor端) | HDFS等可靠文件系统 |
| 生命周期 | 程序结束或 unpersist 后释放 |
永久保存,需手动清理 |
| 保留血缘 | 保留血缘关系 | 斩断血缘关系 |
6.2 共享变量(广播变量与累加器)
1. 广播变量(Broadcast Variables)
当算子里引用了一个大型的只读数据集(如字典、白名单),默认会为每个 Task 拷贝一份。使用广播变量,可以将变量发给每一个 Executor,Executor 里的所有 Task 共享该变量。极大节省了内存。
python
bc_set = sc.broadcast({"hello", "world"})
rdd.filter(lambda x: x in bc_set.value)
2. 累加器(Accumulators)
类似于分布式环境下的全局计数器。
注意:在 Executor 中只能调用累加器的 add 方法,不能读取 其 value。只有在 Driver 端才能读取最终值。
python
acc = sc.accumulator(0)
def count_item(x):
global acc
if x.startswith("10"):
acc.add(1) # 累加操作
rdd.foreach(count_item)
print(acc.value)
七、总结
Spark Core 是 Spark 体系的绝对基石。通过本文的学习,我们掌握了:
| 核心模块 | 关键知识点 |
|---|---|
| 架构 | Driver、Executor、Spark On Yarn 流程 |
| RDD | 五大特性(分区、计算函数、依赖、分区器、最优位置) |
| 算子 | Transformations(惰性求值) vs Actions(触发执行) |
| 依赖与DAG | 窄依赖 vs 宽依赖;Stage = 宽依赖数 + 1 |
| 持久化 | Cache / Persist vs Checkpoint |
| 共享变量 | 广播变量(节省内存)、累加器(分布式计数) |