您是否曾尝试调试一个突然失败的 Spark 任务,却发现自己完全迷失在 Spark 的“兔子洞”里?
我最初使用 Apache Spark 时,以为只要写几步 PySpark 转换,Spark 就会“神奇地”在集群上横向扩展。我错了。Spark 的性能完全取决于您是否理解幕后发生了什么。
本指南适合任何不想把 Spark 当作黑盒的人。我们将从主从模型与执行流程,到内存管理与容错机制,逐步解析 Spark 的架构设计。
如果您想构建快速、具备容错能力且高效的大数据应用,那就来对地方了!
Apache Spark 的基础架构
在您写下第一行 PySpark 代码之前,Spark 已经为您做了一些架构层面的决策。Spark 之所以快,不仅因为内存计算,更因为它建立在可扩展且能应对现实世界混乱场景的主从架构之上,比如节点崩溃hes, Java Virtual Machine (JVM) 问题,以及不稳定的数据量。
让我们拆解 Spark 的核心架构,并看看它为何在现代大数据工作流中依然强大且常用。
主从范式
Spark 的核心是主从 模型。可以这样理解:
- Driver(主节点):这是 Spark 的大脑。它运行您的
main() 函数,创建 Spark 上下文,处理 DAG 调度,并指挥集群执行。
- Executor(工作节点):这是“肌肉”。它们执行任务、在内存中保留数据,并向 driver 汇报。
这种设置让您专注于定义转换,而由 Spark 决定在 executors 上如何并行运行它们。
我喜欢这套设计的一个原因是它与部署方式无关。同一段代码可在本机、Kubernetes 或 Mesos 上运行,无需改动。这使得本地开发测试更简单,随后即可无缝扩展到集群而无需重写代码。
Spark 的 driver‑worker 分离还有一个强大的好处:提升故障隔离。如果某个工作节点在执行任务时宕机,Spark 可以把该任务重新分配给其他工作节点,而不会导致应用崩溃。
核心组件
让我们拆解 driver 和各节点内部发生了什么。

Spark 架构。作者自绘。
Spark 上下文
当您调用 SparkContext() 或使用 SparkSession.builder.getOrCreate() 时,您就打开了通往 Spark 内部“魔法”的大门。
Spark 上下文会:
- 连接到集群管理器
- 分配 executors
- 跟踪作业状态与执行计划
Spark 会在幕后构建一个有向无环图(DAG)来表示转换。该 DAG 会被拆分为阶段和任务,并行执行。
DAG 调度器负责确定哪些任务可以一起运行,任务调度器则把它们分配给 executors。同时,块管理器(Block manager)确保数据按需缓存、shuffle 或重载。
这种分层设计让 Spark 弹性极强,您可以分别微调内存、存储与计算。
如果您在做 Spark 转换或特征工程,可以查看PySpark 特征工程课程,看看这套架构如何实际运作。
Executor 运行时
Executor 是实际完成工作的地方。
每个 executor 会运行:
- 一个或多个任务(多线程)
- 一块用于缓存数据与输出 shuffle 的内存
- 各自独立的 JVM 实例
您可以配置每个 executor 获得多少内存、使用多少核心、内存不足时是否应写入磁盘。
但要小心:内存分配不足会频繁触发内存溢出错误;而分配过多又会浪费资源。监控与调优在这里至关重要。
执行工作流:从代码到集群
写 PySpark 代码看起来很简单。您过滤一个 DataFrame、做个连接、再聚合一下,然后运行。但在这套干净的 API 背后,Spark 正悄悄启动一个执行引擎,把工作分发到多台节点上。
让我们走一遍幕后发生的流程。
从逻辑计划到物理计划
多数 Spark 用户起初并不了解:当您编写 PySpark 代码时,并不会立刻执行。您是在构建计划,而 Spark 的 Catalyst 优化器会接手该计划,将其转化为高效的执行策略。
它分四个阶段工作:
- 解析:Spark 解析列名、数据类型与表引用,确保一切有效。
- 逻辑优化:应用谓词下推与常量折叠等规则;优化过滤并合并投影。
- 物理规划:基于数据规模、分区等因素评估多种执行策略并选出最优方案。
- 代码生成:最终通过全阶段代码生成(whole-stage codegen)产出 JVM 字节码。

Spark 的 Catalyst 优化器。图源 Databricks。
因此,那串 .select()、.join()、.groupBy() 并不是逐行执行,而是先被分析、优化,再编译成可在集群上高速运行的代码。
如果想快速查阅常用 PySpark 命令,可以看看这份PySpark 速查表。
DAG 调度器与阶段创建
计划确定后,DAG 调度器开始接管。
它会根据 shuffle 边界把作业拆分为阶段,决定哪些需要顺序执行,哪些可以并行执行。
主要有两类阶段:
- ShuffleMapStage:涉及 shuffle,通常由
groupBy() 或 join() 等宽依赖转换引起。随后数据会被分区并在网络中传输。该阶段用于为 ResultStage 计算所需数据。
- ResultStage:这些阶段产生输出,比如写入磁盘或将结果返回给 driver。
我学到的一个关键点是尽量减少 shuffle。shuffle 必须在阶段结束前完成,且代价昂贵。您需要了解它们在 DAG 中的出现位置,并思考是否能进一步优化代码以减少 shuffle 次数。
任务执行生命周期
当 DAG 调度器创建好所有阶段后,它们就可以在不同的 executors 上执行了。
任务执行的生命周期大致如下:
- 任务序列化:driver 将任务指令序列化并发送到 executors。
- shuffle 写阶段:Spark 将分区后的输出写入本地磁盘。
- 拉取阶段:下一阶段的 executors 会从集群中的其他节点获取相关的 shuffle 文件。
- 反序列化与执行:executors 对数据反序列化,运行您的逻辑,并可能进行缓存或写出结果。
- 垃圾回收:JVM 自动回收不再被 Spark 应用使用的内存。这一步对防止内存泄漏、确保应用稳定运行至关重要。
基于我的经验给个小提示:如果某个 Spark 作业之前运行正常、如今却卡住,常见原因是垃圾回收或 shuffle 拉取延迟。务必检查您的代码,并确保理解 Spark 架构,从而有效优化这些问题。
内存管理架构
Spark 的内存管理非常复杂,如果没弄明白,可能会让您在调试上耗费大量时间。
因此我们来看看 Spark 在底层是如何管理内存的,以便您心中有数,避免为慢代码或内存溢出问题而长时间调试。
统一内存模型
在 Spark 1.6 之前,内存被严格划分为执行(用于 shuffle 和 join)与存储(用于缓存)。Spark 1.6 引入了统一内存模型,改变了这一点。
在统一内存模型中,内存大致分为三个关键池:
- 保留内存:少量内存用于 Spark 内部与系统。
- Spark 内存:用于存放执行数据与缓存,且是动态共享的。如果作业在 shuffle 上需要更多内存、缓存上需要更少(或反之),Spark 会做出自适应。
- 用户内存:用于存放用户自定义的数据结构,以支持在 Spark 应用中执行用户代码。
Spark 内存池进一步划分为两部分:
- 执行器内存:在处理任务的各个阶段用于存放临时数据(如 shuffle、join、聚合等)。
- 存储内存池:用于缓存数据与存储内部数据结构。
这种弹性让 Spark 能更灵活地应对不可预测的数据量。
但这也意味着在您不了解内部状况时,控制力会有所下降。比如,如果您对一个大型 DataFrame 调用 cache(),同时在同一阶段还进行昂贵的聚合,Spark 可能会为了给 shuffle 腾地方而驱逐已缓存的数据。
堆外与列式存储
在 Spark 的堆外与列式存储中,Tungsten 引擎发挥了作用。
Tungsten 引入了多项优化,大幅提升了 Spark 性能:
- 堆外内存管理:Spark 可将部分数据存放在 JVM 堆外,降低垃圾回收开销,让内存管理更可预测。
- 二进制格式存储:数据以紧凑、利于缓存的二进制形式存储,提升 CPU 利用率并启用向量化执行。
- 感知缓存的算法:Spark 能更有效利用 CPU 缓存,避免不必要的 RAM 或磁盘读取。
如果您在使用 DataFrame,这些优化已经在幕后为您工作了。这也是我鼓励大家使用 DataFrame 与 SQL API 而不是原生 RDD 的原因之一。无需额外调优,就能充分受益于 Catalyst 与 Tungsten。
如果您在做数据清洗流程,可在用 PySpark 清洗数据课程中看到这些优化的实际应用。
容错机制
如果您从事分布式系统,就有一个铁律:它们会失败。节点会崩溃,网络会出错,executor 会内存不足并退出。
但 Spark 天生为这些问题而建,确保您的作业依然能成功完成。
让我们深入了解 Spark 如何在出现不稳定性时,仍能保证作业成功。
RDD 血统追踪
RDD(弹性分布式数据集)是 Spark 的基础数据结构,“弹性”绝非空谈。
Spark 使用血统(lineage)来确保在节点故障、数据丢失时可以重算每个 RDD。
因此当某个节点失败时,Spark 会基于血统图重算丢失的数据。
实际工作原理如下:
- 窄依赖(如
map() 或 filter()):Spark 只需重算丢失的分区。
- 宽依赖(如
groupBy() 或 join()):Spark 可能需要跨多个分区拉取数据,因为可能涉及多个阶段的输出。
血统使您无需手动处理故障。然而,如果血统图过长(包含上百个转换),重算代价会变大。这时就需要用到检查点(checkpointing)。
检查点与预写日志
在复杂工作流或流式作业中,Spark 不能只依赖血统,这就是检查点发挥作用的地方。
您可以调用 rdd.checkpoint() 将当前 RDD 状态持久化到可靠存储(如 HDFS)。
随后 Spark 会截断血统。如发生错误,它会直接加载数据而非重算。
在结构化流处理中,Spark 还使用预写日志(WAL)来确保数据在传输中不丢失。
其稳定性体现在:
- 可靠接收器:在处理前将输入数据写入日志。
- Executor 心跳:定期上报以确认 executors 存活且健康。
- 检查点目录:对流式作业,保存偏移量、元数据与输出状态,以便断点续跑。
对批处理作业,检查点是可选的;对流式管道,则是必需的。
想象一下:一个运行了 10 小时后失败的 Spark 作业,借助检查点和 WAL,您可以从中断处继续执行。
高级架构特性
到现在,您已经看到了 Spark 如何处理作业,以及它的内存与容错机制。
本节我们将探讨一些高级架构升级,使 Spark 更具动态性、更实时、更具适应性。
自适应查询执行(AQE)
AQE 于 Spark 3.0 引入,通过在运行时基于收集到的统计信息动态调整执行计划来提升查询性能。
AQE 的特性包括:
- 动态切换连接策略:如果广播连接内存放不下,AQE 会切换为排序合并连接(sort-merge join)。
- 合并 shuffle 分区:将过小的 shuffle 分区合并为更大的分区,降低开销。
- 处理数据倾斜:AQE 可拆分倾斜分区,平衡执行时间。
这一特性能带来颠覆性提升,使原本需要大量手动调优与反复试错的作业,能够在运行时自适应优化。
只需通过配置显式启用(spark.sql.adaptive.enabled = true)。如果您运行的是 Spark 3.0 及以上版本,没有不启用的理由。
结构化流架构
结构化流将 Spark 引擎扩展到实时领域,而无需您学习全新的 API。
在幕后,它依然采用微批处理,但会处理:
- 偏移量管理:Spark 精确跟踪从数据源(Kafka、socket、文件等)读取了哪些数据。配置正确时可提供强有力的“恰好一次”保证。
- 水位线:在基于时间的聚合中,Spark 使用水位线决定“过晚数据”的丢弃时机,这对事件时间处理至关重要。
- 状态存储:进行窗口聚合或流式连接时,Spark 会在微批之间维护状态。该状态存储于磁盘,并通过检查点避免数据丢失。
强大之处在于:流处理的体验像批处理。您写个 groupBy() 或 filter(),其余交给 Spark 处理,让流式分析无需专门的工具链也能上手。
安全架构
如果您在生产环境运行 Spark,尤其是在金融、医疗或类似领域,您需要了解 Spark 如何处理认证、加密与审计。
下面我们深入看看这些主题,以及 Spark 的实现方式。
认证与加密
Spark 拥有许多安全特性,需要您先行启用。但启用后,Spark 能为安全通信与认证提供一套可靠工具:
- 认证(SASL):Spark 使用简单认证与安全层(SASL)来验证,只有授权的用户与服务才能提交作业或连接集群。
- 传输加密(AES-GCM,SSL/TLS):Spark 通过 AES-GCM(带认证的加密)或 TLS 加密节点间通信,防止作业数据被窃听,尤其适用于多租户或云环境。
- Kerberos 集成:在 Hadoop/YARN 环境下,Spark 可与 Kerberos 集成实现安全的用户认证,从而与企业身份与访问管理系统打通。
- UI 访问控制:Spark Web UI 可能泄露敏感信息(如日志、输入路径、SQL 查询),请设置
spark.acls.enable=true 以及 spark.ui.view.acls 和 spark.ui.view.acls.groups 进行限制。
您可以在 Spark 的官方文档中查看全部安全特性。请务必检查并启用所需功能,以保护您的 Spark 应用。
审计与合规
记录“谁在何时做了什么”同样关键。
Spark 支持:
- 事件日志:启用后(
spark.eventLog.enabled=true),Spark 会将每个作业、阶段与任务事件记录到磁盘。您可用它们回放作业历史或满足审计要求。
- 基于角色的访问控制(RBAC):Spark 本身不提供 RBAC,但如果您通过 Databricks、EMR 或 OpenShift 等平台使用 Spark,通常可在基础设施层获得 RBAC。Spark 将以定义的身份提交作业,从而控制对数据与计算资源的访问。
- 数据源侧的脱敏与访问控制:Spark 可从多种源读取(Parquet、Delta Lake、Hive 等),访问控制应在这些源头强制执行。
性能优化模式
Spark 很强大也很快,如果知道该在哪些方面下手,还能更快。
有若干方向可以尝试优化,从而充分发挥 Spark 的能力。我们分别展开。
Shuffle 优化
如果说 Spark 有弱点,那就是 shuffle。当数据需要在分区间移动时(通常在 groupByKey()、distinct() 或 join() 等宽转换之后)就会发生 shuffle。
而一旦 shuffle 出问题,可能会出现巨量磁盘 I/O、冗长的垃圾回收暂停,或某些倾斜任务迟迟无法完成。
改进 shuffle 的方法包括:
- 优先使用
reduceByKey() 而非 groupByKey():reduceByKey() 会在本地聚合后再进行 shuffle;groupByKey() 则会把所有数据都发往网络。
- 聪明地重新分区:使用
.repartition(n) 提升并行度,或用 .coalesce(n) 降低并行度。不要完全依赖 Spark 的默认分区。
- 明智地使用广播连接:如果一侧数据集足够小,可以广播到所有工作节点。通过
spark.sql.autoBroadcastJoinThreshold 控制大小阈值。
- 避免
collect():尽量避免把数据拉回 driver,这会严重影响性能。
内存配置指南
调优 Spark 内存颇具门道,但可参考下列清单简化流程:
- 分配足够内存:为 Spark 集群至少分配 6 GB 内存,并根据具体需求调整。
- 考虑 Spark 内存占比:默认 60% 是 Spark 的内存比例。如果您的应用高度依赖 DataFrame/Dataset 操作,或需要更多用户内存,可适当提高。
- 为每个 executor 选择合适核心数:一般 3-5 个较优。太少会导致资源未充分利用,太多会引发任务争用。
- 启用动态分配(如受支持):Spark 可根据负载按需扩缩 executors。
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20技术支持
- 调整存储占比:如果需要更多缓存,可增大
spark.memory.storageFraction 值。
- 监控与剖析内存使用:利用 Spark UI 或 VisualVM 等工具跟踪内存消耗并定位瓶颈。
调整内存配置的收益可能非常可观。我曾仅通过调整内存配置(不改一行代码),就把 30 分钟的作业缩短到 8 分钟。
集群规模估算公式
多数团队在这一点上会犯错:凭感觉而非估算来决定集群大小。
您可以借助以下公式做得更好:
- 确定分区数量:
- 根据数据总量与期望的分区大小计算所需分区数。
- 通行建议是每 128 MB 至 256 MB 未压缩数据对应一个分区。
- 公式:分区数 = 向上取整(数据总量 ÷ 分区大小)。
- 计算总核心数:
- 核心数应足以并行处理全部分区。
- 公式:总核心数 = 向上取整(分区数 ÷ 每核心并行分区数)。
- 确定每个 executor 的内存:
- 基于核心数、分区大小与开销计算每个 executor 所需内存。
- 公式:每个 executor 内存 = 基础内存 ×(1 + 开销百分比)。
- 计算 executor 数量:
- 根据总核心数与每个 executor 的核心数确定 executor 数量。
- 公式:executor 数量 = 向上取整(总核心数 ÷ 每个 executor 核心数)。
- 计算总内存:
- 基于 executor 数量与每个 executor 内存计算集群总内存。
- 公式:总内存 = executor 数量 × 每个 executor 内存。
例如:
- 输入:500GB 数据,分区大小约 128MB
- 分区数:约 4,000 个分区
- 核心数:4,000 分区 / 每核心 4 个分区 = 1,000
- 每个 executor 内存:假设 8 GB,开销 20%。8 GB * 1.20 = 9.6 GB
- executor 数量:1,000 核心 / 每个 executor 4 核心 = 250
- 总内存:250 个 executor * 9.6GB = 2,400 GB
但请记住:这只是估算。可作为起点,随后通过剖析进一步优化。
新兴架构趋势
Spark 已有十年历史,但依然非常“新潮”。得益于云原生平台、GPU 加速与更紧密的机器学习集成,它的演进从未如此迅速。
如果您还在用三年前的方式使用 Spark,很可能在性能与新特性上损失不小。
让我们看看其中一些最新进展。
Photon 引擎(Databricks)
如果您使用 Databricks,大概率已经接触并听说过 Photon。
如果您想了解更多 Databricks,推荐Databricks 入门课程。
Photon 是 Databricks Lakehouse 平台上的新一代引擎,以更低成本提供极速查询性能。它兼容 Spark API,因此无需改动 Spark 代码即可受益。
它能显著加速您的 SQL 与 PySpark 代码。
Photon 包含以下特性:
- 向量化执行:Photon 以列式批处理数据,利用 SIMD(单指令多数据)CPU 指令同时对多个值执行操作。传统 Spark 以行式执行为主,并在内存分配与垃圾回收上严重依赖 JVM。
- C++ 运行时(无 JVM 开销):没有 Java 垃圾回收这一大作业瓶颈;内存由 C++ 精细管理。
- 更优的查询优化:Photon 深度集成 Spark 的 Catalyst 优化器,并在执行时加入自身优化(如运行时过滤、自适应代码路径、连接与聚合优化)。
- 硬件加速:支持现代硬件(如 NVIDIA GPU、Intel CPU 的 AVX-512 指令集、AWS 的 Graviton(ARM)处理器)。
无服务器 Spark
无服务器非常棒,因为这意味着您无需管理集群、无需预置资源,且只为 Spark 实际运行的时间付费。
面向 Spark 的无服务器已在 Databricks Serverless、AWS Glue 与 GCP Dataproc Serverless 等服务中落地。
其优势在于:
- 自动伸缩:平台会根据作业的实际需求扩缩算力,您无需再“拍脑袋”估算节点数。
- 成本效益:按用量计费,不再为闲置服务器买单。
- 简化运维:无需处理集群搭建、配置或维护,这些都由平台代劳。
- 高性能:由于配置与环境已为您优化,执行可能更快。
无服务器 Spark 适合交互式分析、临时性作业或不可预测的负载。
但需注意:长时间、稳定运行的流水线,在固定集群上可能更便宜。务必同时衡量成本与延迟。
MLflow 集成
随着行业演进,数据工程与人工智能的界限日益模糊。正如 Azurelib Academy 的 CEO & 创始人 Deepak Goyal 在 DataFramed 播客中所探讨的
您是否曾尝试调试一个突然失败的 Spark 任务,却发现自己完全迷失在 Spark 的“兔子洞”里?
我最初使用 Apache Spark 时,以为只要写几步 PySpark 转换,Spark 就会“神奇地”在集群上横向扩展。我错了。Spark 的性能完全取决于您是否理解幕后发生了什么。
本指南适合任何不想把 Spark 当作黑盒的人。我们将从主从模型与执行流程,到内存管理与容错机制,逐步解析 Spark 的架构设计。
如果您想构建快速、具备容错能力且高效的大数据应用,那就来对地方了!
Apache Spark 的基础架构
在您写下第一行 PySpark 代码之前,Spark 已经为您做了一些架构层面的决策。Spark 之所以快,不仅因为内存计算,更因为它建立在可扩展且能应对现实世界混乱场景的主从架构之上,比如节点崩溃hes, Java Virtual Machine (JVM) 问题,以及不稳定的数据量。
让我们拆解 Spark 的核心架构,并看看它为何在现代大数据工作流中依然强大且常用。
主从范式
Spark 的核心是主从 模型。可以这样理解:
main()函数,创建 Spark 上下文,处理 DAG 调度,并指挥集群执行。这种设置让您专注于定义转换,而由 Spark 决定在 executors 上如何并行运行它们。
我喜欢这套设计的一个原因是它与部署方式无关。同一段代码可在本机、Kubernetes 或 Mesos 上运行,无需改动。这使得本地开发测试更简单,随后即可无缝扩展到集群而无需重写代码。
Spark 的 driver‑worker 分离还有一个强大的好处:提升故障隔离。如果某个工作节点在执行任务时宕机,Spark 可以把该任务重新分配给其他工作节点,而不会导致应用崩溃。
核心组件
让我们拆解 driver 和各节点内部发生了什么。
Spark 架构。作者自绘。
Spark 上下文
当您调用
SparkContext()或使用SparkSession.builder.getOrCreate()时,您就打开了通往 Spark 内部“魔法”的大门。Spark 上下文会:
Spark 会在幕后构建一个有向无环图(DAG)来表示转换。该 DAG 会被拆分为阶段和任务,并行执行。
DAG 调度器负责确定哪些任务可以一起运行,任务调度器则把它们分配给 executors。同时,块管理器(Block manager)确保数据按需缓存、shuffle 或重载。
这种分层设计让 Spark 弹性极强,您可以分别微调内存、存储与计算。
如果您在做 Spark 转换或特征工程,可以查看PySpark 特征工程课程,看看这套架构如何实际运作。
Executor 运行时
Executor 是实际完成工作的地方。
每个 executor 会运行:
您可以配置每个 executor 获得多少内存、使用多少核心、内存不足时是否应写入磁盘。
但要小心:内存分配不足会频繁触发内存溢出错误;而分配过多又会浪费资源。监控与调优在这里至关重要。
执行工作流:从代码到集群
写 PySpark 代码看起来很简单。您过滤一个 DataFrame、做个连接、再聚合一下,然后运行。但在这套干净的 API 背后,Spark 正悄悄启动一个执行引擎,把工作分发到多台节点上。
让我们走一遍幕后发生的流程。
从逻辑计划到物理计划
多数 Spark 用户起初并不了解:当您编写 PySpark 代码时,并不会立刻执行。您是在构建计划,而 Spark 的 Catalyst 优化器会接手该计划,将其转化为高效的执行策略。
它分四个阶段工作:
Spark 的 Catalyst 优化器。图源 Databricks。
因此,那串
.select()、.join()、.groupBy()并不是逐行执行,而是先被分析、优化,再编译成可在集群上高速运行的代码。如果想快速查阅常用 PySpark 命令,可以看看这份PySpark 速查表。
DAG 调度器与阶段创建
计划确定后,DAG 调度器开始接管。
它会根据 shuffle 边界把作业拆分为阶段,决定哪些需要顺序执行,哪些可以并行执行。
主要有两类阶段:
groupBy()或join()等宽依赖转换引起。随后数据会被分区并在网络中传输。该阶段用于为 ResultStage 计算所需数据。我学到的一个关键点是尽量减少 shuffle。shuffle 必须在阶段结束前完成,且代价昂贵。您需要了解它们在 DAG 中的出现位置,并思考是否能进一步优化代码以减少 shuffle 次数。
任务执行生命周期
当 DAG 调度器创建好所有阶段后,它们就可以在不同的 executors 上执行了。
任务执行的生命周期大致如下:
基于我的经验给个小提示:如果某个 Spark 作业之前运行正常、如今却卡住,常见原因是垃圾回收或 shuffle 拉取延迟。务必检查您的代码,并确保理解 Spark 架构,从而有效优化这些问题。
内存管理架构
Spark 的内存管理非常复杂,如果没弄明白,可能会让您在调试上耗费大量时间。
因此我们来看看 Spark 在底层是如何管理内存的,以便您心中有数,避免为慢代码或内存溢出问题而长时间调试。
统一内存模型
在 Spark 1.6 之前,内存被严格划分为执行(用于 shuffle 和 join)与存储(用于缓存)。Spark 1.6 引入了统一内存模型,改变了这一点。
在统一内存模型中,内存大致分为三个关键池:
Spark 内存池进一步划分为两部分:
这种弹性让 Spark 能更灵活地应对不可预测的数据量。
但这也意味着在您不了解内部状况时,控制力会有所下降。比如,如果您对一个大型 DataFrame 调用
cache(),同时在同一阶段还进行昂贵的聚合,Spark 可能会为了给 shuffle 腾地方而驱逐已缓存的数据。堆外与列式存储
在 Spark 的堆外与列式存储中,Tungsten 引擎发挥了作用。
Tungsten 引入了多项优化,大幅提升了 Spark 性能:
如果您在使用 DataFrame,这些优化已经在幕后为您工作了。这也是我鼓励大家使用 DataFrame 与 SQL API 而不是原生 RDD 的原因之一。无需额外调优,就能充分受益于 Catalyst 与 Tungsten。
如果您在做数据清洗流程,可在用 PySpark 清洗数据课程中看到这些优化的实际应用。
容错机制
如果您从事分布式系统,就有一个铁律:它们会失败。节点会崩溃,网络会出错,executor 会内存不足并退出。
但 Spark 天生为这些问题而建,确保您的作业依然能成功完成。
让我们深入了解 Spark 如何在出现不稳定性时,仍能保证作业成功。
RDD 血统追踪
RDD(弹性分布式数据集)是 Spark 的基础数据结构,“弹性”绝非空谈。
Spark 使用血统(lineage)来确保在节点故障、数据丢失时可以重算每个 RDD。
因此当某个节点失败时,Spark 会基于血统图重算丢失的数据。
实际工作原理如下:
map()或filter()):Spark 只需重算丢失的分区。groupBy()或join()):Spark 可能需要跨多个分区拉取数据,因为可能涉及多个阶段的输出。血统使您无需手动处理故障。然而,如果血统图过长(包含上百个转换),重算代价会变大。这时就需要用到检查点(checkpointing)。
检查点与预写日志
在复杂工作流或流式作业中,Spark 不能只依赖血统,这就是检查点发挥作用的地方。
您可以调用
rdd.checkpoint()将当前 RDD 状态持久化到可靠存储(如 HDFS)。随后 Spark 会截断血统。如发生错误,它会直接加载数据而非重算。
在结构化流处理中,Spark 还使用预写日志(WAL)来确保数据在传输中不丢失。
其稳定性体现在:
对批处理作业,检查点是可选的;对流式管道,则是必需的。
想象一下:一个运行了 10 小时后失败的 Spark 作业,借助检查点和 WAL,您可以从中断处继续执行。
高级架构特性
到现在,您已经看到了 Spark 如何处理作业,以及它的内存与容错机制。
本节我们将探讨一些高级架构升级,使 Spark 更具动态性、更实时、更具适应性。
自适应查询执行(AQE)
AQE 于 Spark 3.0 引入,通过在运行时基于收集到的统计信息动态调整执行计划来提升查询性能。
AQE 的特性包括:
这一特性能带来颠覆性提升,使原本需要大量手动调优与反复试错的作业,能够在运行时自适应优化。
只需通过配置显式启用(
spark.sql.adaptive.enabled = true)。如果您运行的是 Spark 3.0 及以上版本,没有不启用的理由。结构化流架构
结构化流将 Spark 引擎扩展到实时领域,而无需您学习全新的 API。
在幕后,它依然采用微批处理,但会处理:
强大之处在于:流处理的体验像批处理。您写个
groupBy()或filter(),其余交给 Spark 处理,让流式分析无需专门的工具链也能上手。安全架构
如果您在生产环境运行 Spark,尤其是在金融、医疗或类似领域,您需要了解 Spark 如何处理认证、加密与审计。
下面我们深入看看这些主题,以及 Spark 的实现方式。
认证与加密
Spark 拥有许多安全特性,需要您先行启用。但启用后,Spark 能为安全通信与认证提供一套可靠工具:
spark.acls.enable=true以及spark.ui.view.acls和spark.ui.view.acls.groups进行限制。您可以在 Spark 的官方文档中查看全部安全特性。请务必检查并启用所需功能,以保护您的 Spark 应用。
审计与合规
记录“谁在何时做了什么”同样关键。
Spark 支持:
spark.eventLog.enabled=true),Spark 会将每个作业、阶段与任务事件记录到磁盘。您可用它们回放作业历史或满足审计要求。性能优化模式
Spark 很强大也很快,如果知道该在哪些方面下手,还能更快。
有若干方向可以尝试优化,从而充分发挥 Spark 的能力。我们分别展开。
Shuffle 优化
如果说 Spark 有弱点,那就是 shuffle。当数据需要在分区间移动时(通常在
groupByKey()、distinct()或join()等宽转换之后)就会发生 shuffle。而一旦 shuffle 出问题,可能会出现巨量磁盘 I/O、冗长的垃圾回收暂停,或某些倾斜任务迟迟无法完成。
改进 shuffle 的方法包括:
reduceByKey()而非groupByKey():reduceByKey()会在本地聚合后再进行 shuffle;groupByKey()则会把所有数据都发往网络。.repartition(n)提升并行度,或用.coalesce(n)降低并行度。不要完全依赖 Spark 的默认分区。spark.sql.autoBroadcastJoinThreshold控制大小阈值。collect():尽量避免把数据拉回 driver,这会严重影响性能。内存配置指南
调优 Spark 内存颇具门道,但可参考下列清单简化流程:
spark.memory.storageFraction值。调整内存配置的收益可能非常可观。我曾仅通过调整内存配置(不改一行代码),就把 30 分钟的作业缩短到 8 分钟。
集群规模估算公式
多数团队在这一点上会犯错:凭感觉而非估算来决定集群大小。
您可以借助以下公式做得更好:
例如:
但请记住:这只是估算。可作为起点,随后通过剖析进一步优化。
新兴架构趋势
Spark 已有十年历史,但依然非常“新潮”。得益于云原生平台、GPU 加速与更紧密的机器学习集成,它的演进从未如此迅速。
如果您还在用三年前的方式使用 Spark,很可能在性能与新特性上损失不小。
让我们看看其中一些最新进展。
Photon 引擎(Databricks)
如果您使用 Databricks,大概率已经接触并听说过 Photon。
如果您想了解更多 Databricks,推荐Databricks 入门课程。
Photon 是 Databricks Lakehouse 平台上的新一代引擎,以更低成本提供极速查询性能。它兼容 Spark API,因此无需改动 Spark 代码即可受益。
它能显著加速您的 SQL 与 PySpark 代码。
Photon 包含以下特性:
无服务器 Spark
无服务器非常棒,因为这意味着您无需管理集群、无需预置资源,且只为 Spark 实际运行的时间付费。
面向 Spark 的无服务器已在 Databricks Serverless、AWS Glue 与 GCP Dataproc Serverless 等服务中落地。
其优势在于:
无服务器 Spark 适合交互式分析、临时性作业或不可预测的负载。
但需注意:长时间、稳定运行的流水线,在固定集群上可能更便宜。务必同时衡量成本与延迟。
MLflow 集成
随着行业演进,数据工程与人工智能的界限日益模糊。正如 Azurelib Academy 的 CEO & 创始人 Deepak Goyal 在 DataFramed 播客中所探讨的