初识Spark Core

一、引言:为什么我们需要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 上时,流程如下:

  1. 任务提交:Client 提交 Application 到 ResourceManager。

  2. 启动 Driver:RM 分配一个 Container,在其中启动 AppMaster(在 Spark 中即 Driver)。

  3. 申请资源:Driver 向 RM 申请 Executor 资源。

  4. 启动 Executor :RM 分配资源,Driver 与 NodeManager 通信启动 Executor。Executor 启动后反向注册给 Driver。

  5. 执行任务:Driver 执行 main 函数,遇到 Action 算子时触发 Job,划分 Stage,生成 Task 并分发到 Executor 执行。

三、Spark核心抽象:RDD(弹性分布式数据集)

RDD(Resilient Distributed Dataset)是 Spark 中最基本的数据抽象。代码中有一句经典的话:RDD 代表着一个不可变、带有分区的集合,可以被并行操作。

3.1 RDD 的五大特性

  1. A list of partitions(分区):RDD 逻辑上被划分成多个分区,数据分布在不同的节点上。

  2. A function for computing each split(计算函数):每个分区独立运行计算函数。

  3. A list of dependencies on other RDDs(血缘/依赖):RDD 是不可变的,转换会产生新的 RDD,形成血缘关系,用于故障恢复。

  4. Partitioner for key-value RDDs(分区器):Key-Value 类型的 RDD 可以指定分区策略(如 Hash 分区)。

  5. 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 任务的层级包含四层:

  1. Application :一个 SparkContext 对应一个应用(一个建筑公司)。

  2. Job:一个 Action 算子触发一个 Job(一个工程项目)。

  3. Stage:根据宽依赖划分(一个施工阶段)。

  4. 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
共享变量 广播变量(节省内存)、累加器(分布式计数)
相关推荐
HRaitest9 小时前
秋招技术岗招聘提效:三类 AI 招聘方案的技术底层与选型逻辑
大数据·人工智能·求职招聘·ai-native
凯凯姜9 小时前
分布式锁场景问题
分布式
starzy199011 小时前
Kafka源码深度剖析:消息有异常是如何处理的
大数据·kafka
大大大大晴天11 小时前
每天认识一个组件:ClickHouse
大数据
每天一道题12 小时前
CAP、Raft 与多数派:分布式系统如何达成一致?
分布式·后端·微服务
zwjapple12 小时前
skill总结
大数据·elasticsearch·搜索引擎
百年江湖随意沉浮13 小时前
DATA AI · 数据可视化
大数据
SelectDB13 小时前
实时更新排错现场:导入了却查到旧数据,和 FINAL 越加越慢的两条路
大数据·数据库·数据分析
SelectDB13 小时前
拿 Planet 的账单一行行对下来,我总结出三个可自查的校验点
大数据·数据库·数据分析