如何为 Spark 集群定容,以及如何避免错误做法
Source: Dev.to
请提供您希望翻译的文章正文内容(除代码块和 URL 之外的文本),我将为您翻译成简体中文并保持原有的 Markdown 格式。
面试题
你需要在 Spark 中处理 1 TB 的数据。如何确定集群规模?
大多数面试答案从一个简单的除法开始:
1 TB → choose 128 MB partitions → ~8 000 partitions → map to cores → decide number of nodes
这种方法简洁且合乎逻辑,但它也不完整。
集群规模并非仅仅由原始数据大小决定;它受工作负载行为驱动。
1. “1 TB” 实际意味着什么?
| 方面 | 为何重要 |
|---|---|
| 压缩的 Parquet 存储在对象存储中 | 仅提供存储效率,而不是运行时占用。 |
| 分区裁剪 | 根据过滤条件跳过整个目录分区。 |
| 谓词下推 | 将过滤条件下推到存储层,仅读取匹配的行组。 |
| 列裁剪 | 只读取所需的列。 |
示例: 一个按日期分区的 1 TB 表,在单日查询时可能只需要 150‑300 GB 扫描。
集群规模必须基于 实际扫描大小,而不是表的大小。
2. 列式数据的运行时膨胀
- 磁盘上:Parquet 已压缩并编码。
- 内存中:已解压、解码,并物化为 Spark 的内部行格式。
一个 1 TB 的压缩数据集在处理过程中可能会 膨胀至 2‑4 TB,遍布各执行器,导致:
- 执行器内存大小
- 溢写概率
- GC 压力
- 内存开销配置
注意: 磁盘大小很少是内存的锚点;内存才是真正的尺寸锚点。
3. Spark的执行模型
Spark 运行一个 DAG of stages,各阶段之间由 shuffle 分隔。
一个 1 TB 的作业可能如下:
- Filter → 400 GB
- Join & expand → 2.5 TB shuffle
- Aggregate → 50 GB
Spark 不关心 输入大小,而是关注它必须 shuffle、排序或溢写的最大中间状态。
如果一次 join 膨胀到 2.5 TB,那就是你的容量基准。
4. 可变性与尾部风险
- 稳定:每天 1 TB?
- 波动:正常日 800 GB,季末 1.4 TB。
生产系统在 第95百分位负载 失效,而不是平均值。
为尾部而非平均值进行容量规划。
5. Spark 的设计假设(以及何时失效)
| 假设 | 失效时会发生什么 |
|---|---|
| 数据可以均匀分区 | 数据倾斜会产生热点分区 → 成为瓶颈 |
| 大多数转换是窄的 | 宽 Shuffle 变得昂贵 |
| 网络比 CPU 慢 | 为网络饱和的节点添加核心也没有收益 |
| 内存是有限的 | OOM(内存溢出)、过度溢写、GC 压力 |
当这些假设成立时,Spark 能够可预测地扩展。
当假设不成立时,添加节点并不能解决根本原因。
6. 以工作负载为中心的瓶颈分类
6.1 CPU 受限(重 UDF、加密、压缩)
| 信号 | 操作 |
|---|---|
| 高 CPU 利用率、低溢写、最小 shuffle 等待 | 扩展 CPU 核心并使用计算优化实例 |
6.2 内存受限(大表连接、宽聚合、缓存)
| 信号 | 操作 |
|---|---|
| 高溢写指标、高 GC 时间、执行器 OOM | 增加执行器内存或降低每任务占用 |
6.3 I/O 受限(对象存储读取、小文件、慢磁盘)
| 信号 | 操作 |
|---|---|
| 低 CPU 利用率、高文件打开开销、高任务反序列化时间 | 在扩展计算资源前先修复文件布局和压缩 |
6.4 Shuffle 重
| 信号 | 操作 |
|---|---|
| 高 shuffle 读取获取等待、reduce 阶段 CPU 低、执行器等待远程块 | 请记住每节点网络带宽是固定的——向已饱和节点添加核心通常无效。 |
7. 常见的 “1 TB” 陷阱
- 忽略 shuffle 乘数 – Shuffle 量可能是输入大小的 2‑3×。
- 热点键 – 单个热点键会产生 200 GB 的分区,使单个 executor 成为瓶颈。
- Skew – 数据分布不均会导致并行度下降。
如何在 Spark UI 中检测 Skew
- 对比 max task duration 与 median。
- 检查 shuffle read size per task。
- 留意有 reducer 处理不成比例的大量数据。
如果某个任务的运行时间是其他任务的 10 倍 → 是分布问题,而非集群规模问题。
缓解策略
- 对热点键进行 Salting
- 在 Join 前进行 Pre‑aggregation
- 在可行时使用 Broadcast joins
- 对于中等程度的 Skew 增加 shuffle partitions
- 重新设计数据模型
8. 溢写与磁盘吞吐量
当 Shuffle 或排序过程中执行内存被占满时,Spark 会将数据溢写到本地磁盘,使 磁盘吞吐量 成为新的瓶颈。
本地磁盘慢的表现
- 任务执行时间增加
- Executor 生命周期延长
- 垃圾回收压力增大
- 阶段性能出现非线性下降
识别方法
- 高溢写指标
- Shuffle 阶段期间任务时长持续增长
- 垃圾回收时间升高
缓解措施
- 增加 Executor 内存
- 减小每个任务的分区大小
- 增加 Shuffle 分区数
- 使用更快的本地磁盘(NVMe)
- 在上游减少 Shuffle 数据量
9. 数据布局很重要
| Question | Impact |
|---|---|
| 1 TB 数据位于何处? | 5 个大 Parquet 文件 vs. 80 万 小文件 vs. 正确分区的数据 |
| 数据是否在连接键上进行了聚簇? | 对 shuffle 成本影响巨大 |
- 小文件 → 更高的任务调度开销、文件列出延迟、驱动器压力。
- 分区不佳 → 扫描规模更大。
- 聚簇错误 → 更高的 shuffle 成本。
10. “正确”回答 “集群应该多大?”
有时正确的答案是: 先修正数据布局。
只有在数据 良好分区、压缩且适当聚类 之后,才有意义讨论每个 executor 的节点数、CPU 核心数和内存大小。
TL;DR 检查清单
- 确定实际扫描大小(剪枝、下推、列选择)。
- 估算运行时数据膨胀(2‑4× 压缩比)。
- 找出最大的中间状态(shuffle、join、aggregation)。
- 分类瓶颈(CPU、内存、I/O、shuffle)。
- 验证数据布局(文件大小、分区、clustering)。
- 根据步骤 1‑4 而非原始 1 TB 来确定内存和 CPU 核心数。
- 规划尾部风险(第 95 百分位负载)。
遵循此系统化方法,你将给出一个 完整、可投入生产的答案,远超 “1 TB ÷ 128 MB = 8 000 partitions”。
Source: …
集群规模——结构化方法
1. 为什么先考虑 SLA 的规模很重要
- SLA 驱动计算 – 如果 SLA 为 2 小时,你不能为 20 分钟的完成时间来做规模规划,反之亦然。
- 以吞吐量为中心的公式
[ \text{Required throughput} = \frac{\text{Peak data volume}}{\text{SLA}} ]
[ \text{Node count} = \frac{\text{Required throughput}}{\text{Per‑node effective throughput}} ]
关键点: 集群规模是一个 吞吐量 问题,而 不是 存储问题。
2. 共享集群的现实
| 资源 | 共享集群上的实际情况 |
|---|---|
| CPU 核心 | 你 不会 获得完整的核心 |
| 内存 | 你 不会 获得完整的内存 |
| Shuffle 服务 | 在租户之间共享 |
| 并发度 | 直接影响可用性 |
忽视这些因素会导致在实际中不准确的“孤立集群数学”。
3. 需要回答的完整问题列表
- 峰值中间状态大小
- 瓶颈类型(CPU、内存、shuffle、I/O、网络)
- Shuffle 量
- 存储吞吐量
- SLA 目标
- 输入的变化(大小、模式、倾斜)
- 隔离模型(专用 vs. 共享)
4. 从答案到具体数字
| 计算 | 你得到的结果 |
|---|---|
| 目标分区大小 | 每个分区的期望大小(例如 128 MiB) |
| 所需分区数 | ceil(Peak intermediate size / Target partition size) |
| 所需并发任务数 | Required partitions / Executors per node |
| 每节点执行器数 | 基于 CPU 核心数和每个执行器的内存 |
| 每执行器内存 | Executor memory = (Node memory – overhead) / Executors per node |
| 节点数 | ceil(Required concurrent tasks / Executors per node) |
结果: 一个有数学依据的集群配置。
如果不先回答上面的这些问题,任何规模估算都只是纯猜测。
5. “如何为 1 TB 数据规模化集群?”——正确的答案
我不依据原始数据大小来做集群规模。
我依据 峰值中间状态、主要瓶颈 和 SLA 约束 来做规模规划。
数据大小只是 起点;工作负载的行为决定最终的集群规模。
6. 现代 Databricks Runtime(Spark 4.x)——有什么变化?
| Databricks 功能 | 功能说明 |
|---|---|
| 自适应查询执行 (AQE)(默认开启) | 合并 shuffle 分区,缓解中等倾斜 |
| Photon | 降低 SQL/DataFrame 工作负载的 CPU 压力 |
| Delta Lake 布局策略 | 减少扫描低效和小文件开销 |
| OPTIMIZE | 合并小文件 |
| Z‑ORDER | 提升多列数据局部性 |
| Liquid Clustering | 用动态聚类取代静态分区和 Z‑ORDER |
| Predictive Optimization | 自动化压缩和维护 |
好处
- 文件压缩
- 数据跳过
- 读取更快
- 元数据开销更低
仍然是挑战的方面
- Shuffle 成本
- 倾斜
- 网络上限
- Spill 行为
- 峰值中间状态压力
要点: 在 Databricks 上,集群规模往往是 最后的调节手段,而不是首要任务。抽象层可以提供帮助,但并不能消除底层分布式系统的物理限制。
7. 展望未来
在下一篇文章中,我们将探讨 无服务器 Spark ——当集群“消失”时会有什么变化,责任如何转移,而基本约束仍保持不变。