Spark 内存管理
概述
Spark 的内存管理经历了静态分区到统一内存的演进,统一内存模型下执行内存与存储内存可以动态互借,大幅提升内存利用率。本文讲清堆内/堆外内存、MemoryManager 演进、统一内存模型与调优参数。
一、内存区域总览
Executor 进程内存(堆内 heap)
┌──────────────────────────────────────────────┐
│ Reserved(系统保留,300MB 左右) │
├──────────────────────────────────────────────┤
│ User Memory(用户数据结构,spark.user.memory)│
├──────────────────────────────────────────────┤
│ Spark 内存(spark.memory.fraction 占比) │
│ ┌─────────────┬──────────────────────────┐ │
│ │ Storage │ Execution │ │
│ │ 存储(缓存) │ 执行(Shuffle/聚合) │ │
│ │ MemoryStore│ 算子临时数据 │ │
│ └─────────────┴──────────────────────────┘ │
│ (动态借用:谁缺谁用,借用可被收回) │
└──────────────────────────────────────────────┘
堆外内存(off-heap):spark.memory.offHeap.enabled| 区域 | 默认占比 | 用途 |
|---|---|---|
| Reserved | 300MB | JVM/Spark 保留 |
| User Memory | 1 - memory.fraction | 用户自定义数据结构 |
| Spark Memory | memory.fraction(默认 0.6) | 存储 + 执行 |
| 存储 : 执行 | 各占 memory.storageFraction(默认 0.5) | 可动态借用 |
二、堆内与堆外
2.1 堆内(On-Heap)
| 特点 | 说明 |
|---|---|
| JVM 管理 | 对象在堆中,受 GC 影响 |
| 默认使用 | 无需额外配置 |
| 缺点 | GC 开销大、内存碎片 |
2.2 堆外(Off-Heap)
| 特点 | 说明 |
|---|---|
| 绕开 JVM GC | 直接内存,GC 压力小 |
| 需显式序列化 | 存的是二进制数据 |
| 配置 | spark.memory.offHeap.enabled=true |
| 对比 | 堆内 | 堆外 |
|---|---|---|
| GC 影响 | 大 | 小 |
| 序列化 | 可存对象 | 需二进制 |
| 大对象缓存 | 易 OOM | 更稳定 |
| 配置复杂度 | 低 | 高 |
三、MemoryManager 演进
3.1 静态内存管理(Spark 1.5 前)
静态划分,各区域比例固定:
执行内存:spark.shuffle.memoryFraction(默认 0.2)
存储内存:spark.storage.memoryFraction(默认 0.6)
其他:spark.storage.unrollFraction 等问题:区域不能互借,缓存多时执行内存不足,执行多时缓存被驱逐。
3.2 统一内存管理(Spark 1.6+ 默认)
| 特性 | 说明 |
|---|---|
| 存储与执行共用 Spark 内存池 | 动态借用 |
| 执行可驱逐存储 | 执行要内存时把缓存块逐出(落盘/丢弃) |
| 存储借用执行 | 执行空闲时缓存可暂借,执行需要时归还 |
规则:
- 执行内存不足 → 可把存储块逐出(缓存的块写盘或丢弃)
- 存储内存不足 → 只能借用执行空闲部分,不能驱逐执行
- 执行优先级高于存储(保证任务完成)四、执行内存与存储内存细节
4.1 执行内存用途
| 场景 | 说明 |
|---|---|
| Shuffle 聚合 | map 端与 reduce 端的聚合缓冲 |
| Join 构建侧 | broadcast join 的构建表 |
| 排序 | 溢写前的内存排序 |
| 序列化 | 临时对象 |
4.2 存储内存用途
| 场景 | 说明 |
|---|---|
| RDD 缓存 | persist/cache 的数据块 |
| 广播变量 | 序列化后的广播数据 |
| Shuffle 输出读缓存 | reduce 拉取块的临时存放 |
4.3 存储驱逐(Eviction)
| 方式 | 说明 |
|---|---|
| 落盘 | MEMORY_AND_DISK 的块写本地磁盘 |
| 丢弃 | MEMORY_ONLY 的块直接丢弃,靠血统重算 |
| 阻塞 | 不能立即腾出内存时阻塞等待 |
五、内存调优参数
| 参数 | 默认 | 说明 |
|---|---|---|
spark.executor.memory | 1g | Executor 堆内存 |
spark.memory.fraction | 0.6 | Spark 内存占比 |
spark.memory.storageFraction | 0.5 | 存储初始占比 |
spark.memory.offHeap.enabled | false | 是否启用堆外 |
spark.memory.offHeap.size | 0 | 堆外大小 |
spark.storage.memoryMapThreshold | 2m | 块读用 mmap 的阈值 |
spark.executor.memoryOverhead | 0.1 | 堆外 JVM 开销(YARN 下) |
5.1 调优思路
1. 缓存类任务:调高 storageFraction,保证缓存常驻
2. 计算类任务(Shuffle 重):执行内存要足,可降低 storageFraction
3. 频繁 GC:增大 executor.memory 或考虑堆外
4. 堆内存不够:memoryOverhead 给足(默认 10%)5.2 常见误区
| 误区 | 正确理解 |
|---|---|
memoryFraction 越大越好 | 要留给 JVM/用户空间,默认 0.6 通常合适 |
| 缓存越多越好 | 缓存挤占执行内存会被驱逐,浪费 |
| 堆外内存无限制 | 堆外是进程外内存,过量导致系统内存不足 |
| OOM 就加内存 | 先看是堆内还是堆外、执行还是存储、是否倾斜 |
六、OOM 定位思路
1. Executor OOM:
看日志定位是执行内存(Shuffle/Join)还是存储(缓存)
2. 执行内存 OOM:
数据倾斜、并行度低、单 key 聚合过大
3. 存储内存 OOM:
storageFraction 过小、缓存数据过大
4. Driver OOM:
collect 大结果集、广播变量过大| 场景 | 处理 |
|---|---|
| Shuffle 聚合 OOM | 提高并行度、治倾斜、降并发拉取 |
| 缓存 OOM | 改 MEMORY_AND_DISK、调大 storageFraction |
| collect OOM | 不 collect,分批 take 或写文件 |
| 广播变量 OOM | 缩小广播数据、考虑持久化 |
常见问题速查
| 问题 | 原因与处理 |
|---|---|
| 缓存被频繁驱逐 | storageFraction 太小或执行压力大,调高存储占比 |
| 全 GC 频繁 | Executor 内存偏小或对象过多,加内存/用堆外 |
| 堆外 OOM | offHeap.size 过大超过物理内存,减小或关闭 |
| Executor 反复 OOM 退出 | 单任务内存需求大,提高并行度分散负载 |