一、引言
Apache Spark 诞生于 MapReduce 难以高效支撑多轮迭代、交互式分析和状态型流处理的背景下,它是一个面向大规模数据处理的统一分析引擎,它通过内存计算、DAG 执行、弹性分布式数据集和统一上层库,把过去割裂的批、流、SQL、机器学习和图计算能力整合到一个引擎中。
lua
传统 MapReduce 多轮计算:
Input
|
v
MapReduce Job 1 ---> 写 HDFS
|
v
MapReduce Job 2 ---> 写 HDFS
|
v
MapReduce Job 3 ---> 写 HDFS
|
v
Output
问题:每一轮都频繁读写稳定存储,适合简单批处理,但不适合低延迟多轮计算。
Spark 多轮计算:
Input
|
v
RDD / DataFrame
|
+--> Transformation 1
|
+--> Transformation 2
|
+--> Transformation 3
|
v
Action / Output
特点:中间数据可缓存,执行计划形成 DAG,失败分区可通过 lineage 重算。
Spark 并没有否定 Hadoop 生态。Spark 可以读取 HDFS、对象存储和其他 Hadoop 支持的数据源,也可以运行在 YARN 之上;它更像是替代 MapReduce 计算引擎的一种通用执行层,而不是替代整个 Hadoop 生态。
二、Spark 架构原理
一个 Spark 应用本质上是一组运行在集群上的独立进程,由主程序中的SparkContext 协调;主程序所在进程称为 Driver Program。Driver 会连接 Cluster Manager(Spark Standalone、Hadoop YARN 和 Kubernetes),由后者在集群中为应用分配资源并启动 Executor,Executor 负责执行任务并保存应用运行过程中的数据。
lua
Spark 集群运行架构:
+----------------------+
| Driver Program |
| SparkContext / DAG |
+----------+-----------+
|
| 申请资源 / 提交任务
v
+----------------------+
| Cluster Manager |
| Standalone/YARN/K8s |
+----------+-----------+
|
+-----------------+-----------------+
| |
v v
+-------------------+ +-------------------+
| Worker Node A | | Worker Node B |
| +---------------+ | | +---------------+ |
| | Executor | | | | Executor | |
| | Task / Cache | | | | Task / Cache | |
| +---------------+ | | +---------------+ |
+-------------------+ +-------------------+
从执行逻辑看,Spark 会把一个用户动作拆成 Job、Stage 和 Task。Job 是由 Action 触发的一次并行计算,Stage 是 Job 根据依赖关系拆分出的任务集合,Task 是发送到某个 Executor 上运行的最小工作单元。
arduino
一次 Spark 作业的逻辑拆分:
Action
|
v
Job
|
+-------------------+
| |
v v
Stage 1 Stage 2
| |
+----+----+ +----+----+
| | | | | |
Task Task Task Task Task Task
| | | | | |
Executor 并行执行 Executor 并行执行
Spark 的执行模型里有一个很重要的概念:Transformation 和 Action。Transformation 描述数据如何变换,比如map 、filter 、join ;Action 触发真正执行,比如count 、collect 、save 。这种延迟执行机制让 Spark 有机会在真正运行前构建 DAG,并尽可能优化任务划分和数据流。
三、核心抽象
RDD 是 Spark 最早的核心抽象,定义为一个可容错、可并行操作的元素集合;它可以从外部存储系统读取,也可以由 Driver 中已有集合并行化创建。
RDD 的三个关键词是"分区、血缘、容错"。分区让数据能够分布到不同节点上并行计算;血缘记录一个 RDD 如何从父数据集变换而来;容错则依赖血缘在节点失败时重算丢失分区。这个设计让 Spark 在很多分析场景中不必把所有中间结果都复制多份。
scss
RDD 血缘关系示意:
textFile("logs")
|
v
filter(error)
|
v
map(toPair)
|
v
reduceByKey
|
v
saveAsTextFile
如果某个分区丢失:
Spark 可根据 textFile -> filter -> map -> reduceByKey 的链路重算该分区。
随着 Spark SQL 的成熟,DataFrame 和 Dataset 成为更常见的开发接口。相比直接操作 RDD,DataFrame 更接近关系型表,包含 Schema 信息,适合 SQL、ETL、数据湖处理和 BI 查询,Spark 官方也将 Spark SQL、DataFrame、Dataset 描述为结构化数据处理的重要入口。
对于新项目,一般建议优先使用 DataFrame / SQL API。RDD 仍适合表达底层、非结构化、需要精细控制的计算逻辑,但大多数数仓 ETL、湖仓加工和分析型作业会从 Catalyst 优化器、列式执行、谓词下推等结构化处理能力中受益。
四、功能特性
Spark 的定位是"统一分析引擎",上层工具包括 Spark SQL、pandas API on Spark、MLlib、GraphX 和 Structured Streaming, Spark 的能力范围已经超过传统批处理框架。
| 能力 | 说明 | 常见场景 |
|---|---|---|
| Spark Core | 提供任务调度、内存管理、RDD、容错等底层能力 | 自定义分布式计算 |
| Spark SQL | 支持 SQL、DataFrame、Dataset 和结构化数据处理 | 数仓 ETL、湖仓查询、报表加工 |
| Structured Streaming | 基于 DataFrame/Dataset 的流式计算模型 | 实时指标、日志流处理、增量计算 |
| MLlib | Spark 内置机器学习库 | 特征处理、分类、回归、聚类、推荐 |
| GraphX | 图计算框架 | PageRank、关系网络、连通分量 |
| pandas API on Spark | 面向 pandas 工作负载的分布式扩展 | Python 数据分析迁移到大规模数据 |
Spark 的优势之一是多语言支持。 Spark 提供 Java、Scala、Python 和 R 的高级 API;PySpark 也支持 Spark SQL、DataFrame、Structured Streaming、MLlib 和 Spark Core 等能力。
另一个优势是部署灵活。Spark 可以本地运行用于开发测试,也可以运行在 Standalone、YARN 和 Kubernetes 上;应用提交统一使用spark-submit ,它能以统一接口适配不同 Cluster Manager 和部署模式。
sql
Spark 功能栈简图:
+------------------------------------------------------+
| User APIs |
| Scala / Java / Python / SQL / R |
+------------------------------------------------------+
| Spark SQL | Structured Streaming | MLlib | GraphX |
+------------------------------------------------------+
| DataFrame / Dataset / RDD |
+------------------------------------------------------+
| Spark Core: DAG Scheduler / Task / Memory |
+------------------------------------------------------+
| Standalone / YARN / Kubernetes / Local |
+------------------------------------------------------+
| HDFS / S3 / Data Lake / Hive / Kafka / JDBC / Files |
+------------------------------------------------------+
这也是 Spark 被广泛用于数据平台的原因:它把底层分布式计算复杂度隐藏起来,同时保留了足够多的性能调优入口。Spark 不只是一个"跑 SQL 的工具",也是构建数据湖、实时数仓、特征工程和离线训练管道的通用基础设施。
五、适用场景
Spark 适合数据规模较大、计算逻辑较复杂、需要分布式并行处理的场景。典型例子包括离线 ETL、宽表加工、用户画像、日志分析、数据湖表清洗、复杂 Join、机器学习特征工程,以及需要批流一体开发体验的实时指标处理。
| 场景 | Spark 是否适合 | 原因 |
|---|---|---|
| TB/PB 级离线 ETL | 适合 | 分布式计算、SQL/DataFrame 生态成熟 |
| 复杂多表 Join | 适合 | 可通过 Shuffle、广播 Join、分区优化处理 |
| 机器学习特征工程 | 适合 | 适合大规模数据预处理,MLlib 可覆盖部分算法 |
| 交互式探索分析 | 较适合 | 可缓存数据,但仍受集群资源和查询复杂度影响 |
| 毫秒级 OLTP 查询 | 不适合 | Spark 不是事务型数据库 |
| 高频低延迟消息处理 | 视场景而定 | Structured Streaming 适合微批/连续处理类场景,但极低延迟系统需评估 |
| 小数据脚本处理 | 通常不适合 | 启动和调度开销可能超过收益 |
| 作为存储系统 | 不适合 | Spark 是计算引擎,数据仍需存储在 HDFS、对象存储、湖表或数据库中 |
Spark 的优势在"吞吐"和"通用性",不是所有场景都追求最低延迟。一个实用判断是:如果数据能在单机内存里轻松处理,Spark 可能过重;如果数据需要跨节点计算、需要容错、需要调度、需要与湖仓生态集成,Spark 的价值就会明显上升。
六、部署使用
理解 Spark 部署时,需要区分两个容易混淆的概念:
- 集群管理器(Cluster Manager) :负责为 Spark 应用分配 CPU、内存和运行节点。
- 部署模式(Deploy Mode) :决定 Driver 运行在提交客户端,还是运行在集群内部。
6.1.Cluster Manager
常见的Cluster Manager如下:
| 集群管理器 | 资源管理方式 | 主要特点 | 典型场景 |
|---|---|---|---|
| Local | 单进程内的多个线程 | 无需集群,启动简单 | 本地开发、单元测试、学习 |
| Standalone | Spark 自带 Master/Worker | 部署简单,对 Spark 支持直接 | 独立 Spark 集群、中小规模平台 |
| YARN | Hadoop ResourceManager/NodeManager | 与 Hadoop、HDFS 和企业权限体系结合紧密 | 传统 Hadoop 数据平台 |
| Kubernetes | Pod、Scheduler、容器镜像 | 容器化、资源隔离、云原生调度 | 云平台、容器化数据平台 |
无论使用哪种集群管理器,Spark 应用的基本运行结构都没有改变:Driver 负责协调应用,Executor 负责执行 Task 和保存缓存、Shuffle 等数据。每个 Spark Application 拥有自己独立的一组 Executor,不同应用通常不能直接共享 Executor 内存中的数据,只能通过外部存储交换数据。
markdown
Spark 应用
│
│ spark-submit
▼
┌──────────────────────────────┐
│ 选择运行环境 │
└──────────────────────────────┘
│ │ │
┌───────────┘ │ └────────────┐
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌────────────────┐
│ Standalone │ │ YARN │ │ Kubernetes │
│ Master/Worker│ │ RM / NM / AM │ │ Driver/Executor│
│ │ │ │ │ Pod │
└──────┬───────┘ └───────┬────────┘ └───────┬────────┘
│ │ │
└────────────────────┴─────────────────────┘
│
▼
┌─────────────────────────┐
│ Driver + 多个 Executor │
└─────────────────────────┘
6.2.Deploy Mode
Deploy Mode 决定 Driver 的位置。
在 Client 模式下,Driver 运行在执行spark-submit 的客户端进程中,Cluster Manager 只负责在集群节点上启动 Executor。
arduino
Client 模式
┌─────────────────────── 客户端或网关机 ──────────────────────┐
│ │
│ spark-submit │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Driver │◄──────── Task 状态、结果、心跳 ───────┐ │
│ └──────┬───────┘ │ │
└─────────┼───────────────────────────────────────────────┼───┘
│ 申请资源、提交 Task │
▼ │
┌────────────────────── 集群内部 ─────────────────────────┼───┐
│ ┌─────────────────┐ │ │
│ │ Cluster Manager │ │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ┌─────┴──────────────┐ │ │
│ ▼ ▼ │ │
│ ┌────────────┐ ┌────────────┐ │ │
│ │ Executor 1 │ │ Executor 2 │────────────────────┘ │
│ │ Task/Cache │ │ Task/Cache │ │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
Client 模式的优点是交互方便,Driver 日志直接出现在提交终端,适合开发测试交互场景。
在 Cluster 模式下,提交客户端只负责发送应用;Cluster Manager 在集群内部启动 Driver,之后客户端可以退出。
arduino
Cluster 模式
┌────────────── 提交客户端 ──────────────┐
│ │
│ spark-submit │
│ │ │
│ └──── 提交应用后可退出 │
└───────────────┬───────────────────────┘
│
▼
┌──────────────────────── 集群内部 ────────────────────────────┐
│ │
│ ┌─────────────────┐ │
│ │ Cluster Manager │ │
│ └────────┬────────┘ │
│ │ 启动 Driver 和 Executor │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ Driver │ │ Executor 1 │ │
│ │ DAG Scheduler│◄────►│ Task / Cache │ │
│ └──────┬───────┘ └───────────────────┘ │
│ │ │
│ └─────────────►┌───────────────────┐ │
│ │ Executor 2 │ │
│ │ Task / Cache │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Cluster 模式更适合生产环境,不过Cluster 模式下的 Driver 日志通常需要从 YARN、Kubernetes、Standalone Master 或日志聚合系统中查看。
6.3.spark作业提交
典型的 YARN Cluster 提交命令:
sql
spark-submit \
--master yarn \
--deploy-mode cluster \
--class com.example.UserProfileJob \
--conf spark.executor.instances=10 \
--conf spark.executor.cores=4 \
--conf spark.executor.memory=8g \
user-profile-job.jar
Master 与 Deploy Mode 的组合:
bash
spark-submit
│
├── --master
│ ├── local[*] 本地模式
│ ├── spark://host:7077 Standalone
│ ├── yarn YARN
│ └── k8s://https://... Kubernetes
│
└── --deploy-mode
├── client Driver 在提交端
└── cluster Driver 在集群内
七、常见问题
- 作业为什么很慢
lua
作业慢排查路径:
先看 Web UI
|
+--> Job/Stage 是否有长尾 Task
|
+--> Shuffle Read/Write 是否过大
|
+--> Executor 是否频繁 GC
|
+--> 是否有数据倾斜
|
+--> 是否资源不足或并行度过低
|
+--> SQL 执行计划是否出现不合理 Join / Scan
如果慢在 Shuffle,重点看 Join、GroupBy、Distinct、Window 等算子;如果慢在 GC,重点看对象数量、缓存策略、序列化方式和 Executor 内存;如果慢在单个或少数 Task,则更可能是数据倾斜或分区设计不合理。
- 为什么会 OOM
OOM 常见于 Driver 端collect 大数据、Executor 内存不足、Shuffle 聚合过大、缓存过多、单分区数据过大或 UDF 创建大量对象。
Spark 内存主要分为执行内存和存储内存,执行内存用于 Shuffle、Join、Sort、Aggregation 等计算,存储内存用于缓存和内部数据传播;两者共享统一区域,执行内存可在必要时挤出部分存储内存。
避免 OOM 的常见方法包括:不要把大结果collect 到 Driver;控制单分区大小;减少宽依赖 Shuffle;必要时使用广播 Join;缓存后及时unpersist ;用 DataFrame 替代大量对象化 RDD;观察 Storage 页面确认缓存实际占用。
- 并行度应该怎么设
对于每个 CPU Core,通常建议设置 2 到 3 个 Task 来提高集群利用率;如果并行度太低,集群资源无法充分使用,如果并行度过高,则调度开销和小任务数量会增加。
makefile
并行度过低:
Core: 100
Task: 20
结果:大量 Core 空闲
并行度较合理:
Core: 100
Task: 200 ~ 300
结果:资源利用更充分,个别慢任务也更容易被调度平滑
实践中还要结合输入文件大小、Shuffle 后分区数、目标文件大小和下游系统承载能力。
- Web UI 看什么
Spark Web UI 是排障入口,每个 Spark 应用运行期间都会提供 Web UI,默认地址通常是http://:4040 ,可用于监控运行任务、诊断失败、分析慢 SQL 执行计划,以及检查内存和任务如何分布在 Executor 上。
| 页面 | 重点观察 |
|---|---|
| Jobs | Job 是否失败、持续时间、触发来源 |
| Stages | Stage 耗时、Task 分布、Shuffle、失败原因 |
| Storage | 缓存数据占用、分区数量、存储级别 |
| Environment | 实际生效配置、Classpath、运行环境 |
| Executors | Executor 内存、GC、Task 数、Shuffle 读写 |
| SQL | SQL/DataFrame 查询、执行计划、算子指标 |
| Structured Streaming | 流式查询状态、批次耗时、输入速率 |
Web UI 不是"出问题才看"的工具,上线前通过 Web UI 观察一次作业的 Stage 划分、Shuffle 量、GC 时间和 SQL 物理计划,往往能提前发现分区过大、广播失效、扫描过多或资源配置不合理的问题。