
1. 问题现象
ERROR YarnScheduler: Lost executor 27 on 6.39.40.60:
Container container_e327_1778748497801_10300557_01_000028 on host: 6.39.40.60 was aborted.
Exit status: -100.
Diagnostics: The physical resource utilization threshold for NM service has been exceeded.
EXCEEDED_RESOURCE_TYPE_MEM.
关键信息:
- Exit status: -100:容器被 YARN NodeManager 强制终止
- EXCEEDED_RESOURCE_TYPE_MEM:物理内存使用超限
- Lost executor 27:第 27 号 Executor 被丢失
2. 两层内存监控机制
YARN 的 NodeManager 有两层内存监控,理解它们的区别是定位问题的关键:
| 层级 | 触发条件 | 典型报错 | 性质 |
|---|---|---|---|
| 容器级别 | 单个容器自身内存超过 executor.memory + memoryOverhead |
Container is running beyond physical memory limits |
自己超限,怪自己 |
| 节点级别 | 物理机整体内存超过 NM 服务阈值 | The physical resource utilization threshold for NM service has been exceeded |
被邻居连坐 |
本文讨论的是节点级别的情况------你的 Executor 自身内存使用正常,但因为同节点上其他容器占用了大量内存,导致物理机整体内存吃紧,NM 为了自保杀掉了部分容器,你的 Executor 不幸被选中。
3. 常见的错误应对:加大内存
很多人的第一反应是增大 spark.executor.memory,但这是一种浪费:
- 问题不在你的 Executor 自身内存超限,而是节点整体资源紧张
- 加大内存反而增加了单容器对节点资源的占用,可能加剧问题
- 你的任务实际需要的内存并没有变,多分配的部分完全浪费
4. 正确策略:分散 + 容错
既然根因是节点级别的资源争抢,应对思路应该是降低被连坐概率 和增强自动恢复能力。
4.1 减小 executor.cores
properties
spark.executor.cores=2
每台机器能容纳的 Executor 数量减少 → 同节点上并发容器更少 → 节点内存压力降低。内存总量不变,但减少了"邻居"密度。
4.2 开启动态分配
properties
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20
spark.dynamicAllocation.executorIdleTimeout=60s
空闲时自动释放 Executor,减少占坑时间,降低对集群资源的持续占用。
4.3 增大任务重试次数
properties
spark.task.maxFailures=6
默认值为 4。当 Executor 被杀后,运行在其上的 Task 会自动重试,增大重试次数可以提高最终成功率,让任务有机会被调度到其他健康的节点上。
4.4 启用黑名单机制
properties
spark.blacklist.enabled=true
spark.blacklist.maxTaskAttemptsPerNode=2
spark.blacklist.maxBlacklistFraction=0.5
当某个节点上连续出现 Executor 被杀的情况,将该节点加入黑名单,后续任务调度时自动避开,不再分配到问题节点。
5. 推荐配置组合
properties
# 不加内存,分散+容错
spark.executor.cores=2
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.minExecutors=2
spark.dynamicAllocation.maxExecutors=20
spark.dynamicAllocation.executorIdleTimeout=60s
spark.task.maxFailures=6
spark.blacklist.enabled=true
核心逻辑:问题在节点不在你,策略是"分散+容错"而非"加内存硬扛"。
6. YARN 内存监控机制详解
6.1 三种内存控制模式
YARN 提供了三种递进的内存控制模式,由不同的配置参数组合决定:
| 模式 | 配置 | 机制 | 优缺点 |
|---|---|---|---|
| Level 0:无控制 | P=false, V=false, CG=false, E=false | 不监控内存 | 容器可以无限制使用内存,危险 |
| Level 1:轮询监控(Legacy) | P=true 或 V=true | NM 线程定期采样容器内存,超限则杀 | 有延迟,可能导致节点 OOM |
| Level 2:cgroups 严格控制 | CG=true, C=true, P=true 或 V=true | 内核 OOM killer 直接杀超限容器 | 即时生效,但容器不能 burst |
| Level 3:cgroups 弹性控制 | CG=true, E=true, P=true 或 V=true | 允许 burst,仅节点整体超限时才杀 | 利用率最高,但实现复杂 |
参数含义:
- P =
yarn.nodemanager.pmem-check-enabled(物理内存检查) - V =
yarn.nodemanager.vmem-check-enabled(虚拟内存检查) - C =
yarn.nodemanager.resource.memory.enforced(cgroups 严格内存执行) - E =
yarn.nodemanager.elastic-memory-control.enabled(弹性内存控制) - CG = cgroups 前置条件(LinuxContainerExecutor + memory.enabled)
6.2 轮询监控的工作流程(最常见模式)
大多数 YARN 集群默认使用 Level 1 轮询监控 ,核心由 ContainersMonitorImpl 实现:
┌─────────────────────────────────────────────────────────────┐
│ ContainersMonitorImpl.monitoringThread │
│ (每隔 monitoringInterval 毫秒执行一次) │
│ │
│ 1. 遍历所有 trackingContainers │
│ 2. 对每个 Container: │
│ a. 读取 /proc/<pid>/stat 构建进程树 │
│ b. 计算进程树物理内存 (RSS) 和虚拟内存 (VSS) │
│ c. 与 Container 内存上限比较 │
│ d. 判断是否超限 → 决定是否杀除 │
│ 3. 汇总所有 Container 内存使用量 │
│ 4. 更新 NM 节点指标 │
│ 5. sleep(monitoringInterval) │
│ 6. 重复步骤 1-5 │
└─────────────────────────────────────────────────────────────┘
判杀规则:两轮确认机制
YARN 不会仅凭一次采样就杀容器,因为进程内存使用有波动。它引入了进程年龄概念来避免误杀:
- 进程刚启动时年龄 = 1
- 每次监控轮次,年龄 +1
- 判杀标准:
| 条件 | 判杀标准 |
|---|---|
| 年龄 > 0 的进程 | 进程树总内存 > 上限值 × 2 才杀(容忍瞬间波动) |
| 年龄 > 1 的进程 | 进程树总内存 > 上限值就杀(已稳定运行,不应波动) |
也就是说:刚启动的进程有一轮缓冲期(可以用到2倍上限),第二轮开始就必须严格在上限以内。
内存上限的计算
- 物理内存上限 =
executor.memory + spark.yarn.executor.memoryOverhead - 虚拟内存上限 = 物理内存上限 ×
yarn.nodemanager.vmem-pmem-ratio(默认 2.1)
6.3 cgroups 严格控制的工作流程
当启用 cgroups 严格控制(Level 2)时:
- NM 为每个 Container 创建一个 cgroup 子组
- 在 cgroup 中设置
memory.limit_in_bytes= 容器内存上限 - 当容器进程尝试使用超过上限的内存时,Linux 内核的 OOM killer 直接杀掉该容器进程
- 容器退出码为 137(128 + 9,即 SIGKILL)
- 可以在
/var/log/messages中验证 OOM 原因
优势 :即时生效,没有轮询延迟;劣势:容器不能 burst 使用更多内存。
6.4 cgroups 弹性控制的工作流程(你遇到的情况)
当启用弹性控制(Level 3)时,这正是你遇到的场景:
- NM 在所有 Container 的父 cgroup 上设置一个内存上限 =
yarn.nodemanager.resource.memory-mb(节点总可用内存) - 单个 Container 的 cgroup 不设限制,允许 burst
- 当节点整体内存使用量达到父 cgroup 上限时:
- 内核冻结所有 Container 进程
- 内核通过 cgroup 事件通知 NM
- NM 的
OOMHandler选择一个 Container 进行 preempt(杀除)
- 杀掉一个后检查 OOM 条件是否解除,若未解除则继续杀下一个
默认 OOMHandler 的选择逻辑
DefaultOOMHandler 的杀容器策略:
- 优先杀:最近一次超出自身内存限制的 Container(即 burst 了的那一个)
- 兜底杀 :如果没有 burst 的 Container,则杀最近启动的 Container(最小化损失)
- 持续杀除直到节点 OOM 条件解除
关键特性:如果你的 Container 没有 burst(内存使用在自身限制内),理论上不会被优先选中。但在兜底策略下,如果它是最近启动的,仍然可能被杀。
6.5 关键配置参数速查表
| 参数 | 默认值 | 作用 | 配置位置 |
|---|---|---|---|
yarn.nodemanager.pmem-check-enabled |
true | 是否检查物理内存 | yarn-site.xml |
yarn.nodemanager.vmem-check-enabled |
true | 是否检查虚拟内存 | yarn-site.xml |
yarn.nodemanager.vmem-pmem-ratio |
2.1 | 虚拟内存与物理内存比例阈值 | yarn-site.xml |
yarn.nodemanager.resource.memory-mb |
8192 | NM 节点可为容器分配的总内存 | yarn-site.xml |
yarn.nodemanager.resource.memory.enforced |
false | 是否启用 cgroups 严格内存执行 | yarn-site.xml |
yarn.nodemanager.elastic-memory-control.enabled |
false | 是否启用弹性内存控制 | yarn-site.xml |
yarn.nodemanager.container-monitor.interval-ms |
3000 | 轮询监控间隔(毫秒) | yarn-site.xml |
所有这些参数都是 YARN 集群级别配置,无法通过单个 Spark 任务修改。
6.6 你能控制什么
| 层面 | 你能控制的 | 你不能控制的 |
|---|---|---|
| 集群配置 | --- | NM 杀容器的阈值、节点内存上限、cgroups 设置 |
| Spark 任务参数 | executor.memory、memoryOverhead、executor.cores |
NM 的 pmem-check-enabled、vmem-check-enabled 等 |
| 任务容错 | maxFailures、blacklist、dynamicAllocation |
NM 选择杀哪个容器的策略 |
7. 补充说明
- 如果任务确实存在数据倾斜,某个 Executor 处理的数据量远超其他,应优先解决倾斜问题
- 如果频繁出现节点级别内存紧张,建议联系运维排查集群资源分配是否合理
- 以上参数调整后建议观察几个任务周期的运行情况,根据实际效果微调