Linux

EC2实例OOM死机故障深度分析

2026/03/01 11:30 4 次阅读王梓
★ 打赏
✸ ✸ ✸

一、故障现象:规律性失联

先看故障的时间线:

2月20日 16:56 - 首次死机
2月24日 15:11 - 第二次死机
2月25日 14:52 - 第三次死机
2月26日 15:01 - 第四次死机
2月27日 15:34 - 第五次死机,最终定位问题

每次死机的症状完全一致:

  • SSH 连接突然断开,重连超时
  • 业务服务中断,监控告警
  • AWS Console 显示实例状态为 running,但 System Status Check 失败
  • 只能通过 AWS Console 强制重启(Stop → Start)恢复
  • 重启后一切正常,直到下一次死机

注意两个关键线索:时间集中在下午 15:00 左右,以及实例状态显示 running 但无法连接。前者暗示是定时任务触发,后者说明不是网络问题,而是操作系统层面的故障 -- EC2 的 hypervisor 认为虚拟机还在运行,但 OS 内部已经卡死或在反复重启。

二、实例配置:先摸清家底

排查任何性能问题,第一步都是搞清楚资源配置。登录实例后:

# 查看内存
$ free -h
               total        used        free      shared  buff/cache   available
Mem:           904Mi       398Mi       145Mi       0.0Ki       360Mi       464Mi
Swap:             0B          0B          0B

# 更详细的内存信息
$ cat /proc/meminfo | grep -E "MemTotal|MemAvailable|SwapTotal"
MemTotal:         926648 kB    # 约 904MB
MemAvailable:     475164 kB    # 约 464MB
SwapTotal:             0 kB    # 没有 swap

# 系统版本
$ uname -r
6.1.127-135.201.amzn2023.x86_64
$ cat /etc/os-release | head -2
NAME="Amazon Linux"
VERSION="2023"

这里需要解释几个关键概念:

free 命令各列的含义:

  • total(904MB):物理内存总量。t3.nano 标称 512MB,但 AWS 实际分配的物理内存略多,系统可见约 904MB
  • used(398MB):已被进程和内核使用的内存
  • free(145MB):完全空闲、未被任何东西使用的内存。这个数字看起来很小,但不用慌
  • buff/cache(360MB):被内核用作文件系统缓存的内存。这部分内存在需要时可以被回收
  • available(464MB):这才是真正可用的内存,等于 free + 可回收的 cache。当新进程需要内存时,内核会自动回收缓存

为什么没有 swap 是致命的?

Swap 是磁盘上的一块空间,当物理内存不够用时,内核会把不活跃的内存页写到 swap 里,腾出物理内存给急需的进程。没有 swap 意味着:当物理内存(包括可回收的缓存)全部耗尽时,内核唯一的选择就是启动 OOM Killer 杀进程。有了 swap,系统虽然会变慢(磁盘比内存慢几个数量级),但至少不会直接杀进程。

所以当前的状况是:904MB 总内存,可用 464MB,零 swap。这台机器的内存余量非常紧张。

三、日志分析:层层剥茧

3.1 内核日志:OOM Killer 的直接证据

Linux 内核在触发 OOM Killer 时会在内核日志(dmesg / journalctl)中留下详细记录。查看上一次启动前的错误日志:

$ journalctl -p err -b -1 --no-pager | grep -i "out of memory"

Feb 27 15:34:08 kernel: Out of memory: Killed process 559951 (dnf)
  total-vm:628324kB, anon-rss:357744kB, file-rss:0kB,
  shmem-rss:0kB, UID:0 pgtables:872kB oom_score_adj=0
p>strong>

逐字段解读这条 OOM 日志:

  • Killed process 559951 (dnf):被杀死的进程 PID 是 559951,进程名是 dnf(Fedora/RHEL 系的包管理器,类似 apt)
  • total-vm:628324kB(约 614MB):进程的虚拟内存总量。虚拟内存包括已映射但未必实际占用物理内存的部分(比如 mmap 的文件、未访问的堆空间)
  • anon-rss:357744kB(约 349MB):这是关键字段。RSS(Resident Set Size)是进程实际占用的物理内存。anon-rss 指匿名页(堆、栈等非文件映射的内存),349MB 就是 dnf 真正吃掉的物理内存
  • file-rss:0kB:文件映射占用的物理内存为 0(已被回收)
  • shmem-rss:0kB:共享内存占用为 0
  • pgtables:872kB:页表占用的内存。页表是内核用来管理虚拟地址到物理地址映射的数据结构
  • oom_score_adj=0:OOM 分数调整值。0 表示没有特殊调整,内核按默认算法计算该进程的 OOM 优先级

再看历史上每次 OOM 的记录:

Feb 20 16:56 - dnf 被杀, anon-rss: 346612kB (338MB)
Feb 24 15:11 - dnf 被杀, anon-rss: 374640kB (366MB)
Feb 25 14:52 - dnf 被杀, anon-rss: 368236kB (360MB)
Feb 26 15:01 - dnf 被杀, anon-rss: 361440kB (353MB)
Feb 27 15:34 - dnf 被杀, anon-rss: 357744kB (349MB)

规律非常明显:每次都是 dnf 进程被杀,每次占用 338-366MB 物理内存,每次都在下午 15:00 左右

3.2 理解 OOM Killer 的工作机制

在继续排查之前,有必要理解 OOM Killer 是怎么决定杀谁的。

当内核发现无法分配内存时(所有物理内存和 swap 都用完了),它会启动 OOM Killer。OOM Killer 的核心逻辑是:

  1. 计算每个进程的 oom_score:分数越高越容易被杀。计算依据主要是进程占用的内存量 -- 占用越多,分数越高
  2. 考虑 oom_score_adj 调整值:范围 -1000 到 1000。-1000 表示永远不杀(比如 sshd),1000 表示优先杀。可以通过 /proc/PID/oom_score_adj 查看和设置
  3. 选择分数最高的进程杀掉:发送 SIGKILL(信号 9),进程无法捕获或忽略这个信号

在我们的案例中,dnf 占用了 349MB,是当时系统中内存占用最大的进程,所以被选中杀掉。

一个常见的误解是:OOM Killer 杀掉进程后系统就恢复了。实际上不一定。如果被杀的进程是某个关键服务的子进程,父进程可能会尝试重启它,再次耗尽内存,形成"杀了又起、起了又杀"的死循环,最终导致系统完全卡死。这正是我们遇到的情况。

3.3 进程链分析:谁在运行 dnf?

知道 dnf 被杀了,下一个问题是:谁启动的 dnf?查看 OOM 触发时的完整内核日志:

$ journalctl -b -1 --no-pager | grep "Feb 27 15:34"

# 关键行:
kernel: ssm-agent-worke invoked oom-killer:
  gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0

kernel: oom-kill:constraint=CONSTRAINT_NONE,
  task_memcg=/system.slice/amazon-ssm-agent.service,
  task=dnf, pid=559951, uid=0

逐行解读:

  • ssm-agent-worke invoked oom-killer:是 ssm-agent-worker 进程在尝试分配内存时触发了 OOM Killer。注意"invoked"不是说它被杀了,而是说它的内存分配请求导致内核发现内存不够了
  • gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP):这是内核内存分配的标志位。GFP_HIGHUSER_MOVABLE 表示分配用户空间的可移动页面,这是最常见的用户进程内存分配类型
  • order=0:请求分配 2^0 = 1 个页面(4KB)。仅仅 4KB 的分配请求就失败了,说明系统内存已经完全耗尽
  • task_memcg=/system.slice/amazon-ssm-agent.service关键信息 -- dnf 进程属于 amazon-ssm-agent.service 的 cgroup。这证明 dnf 是被 SSM Agent 启动的
  • task=dnf, pid=559951, uid=0:被杀的目标进程是 dnf,以 root 身份运行

OOM 触发时的进程内存快照:

# 内核打印的进程列表(简化)
[  PID  ] uid  rss    pgtables  name
[ 488478]   0  2188   192512    amazon-ssm-agen   # SSM Agent 主进程
[ 488489]   0  4040   249856    ssm-agent-worke   # SSM Agent 工作进程
[ 559870]   0  3184   196608    ssm-document-wo   # SSM 文档执行器
[ 559951]   0  89436  892928    dnf               # 包管理器 -- 内存大户

这里的 rss 单位是页面数(每页 4KB),所以 dnf 的 89436 页 = 89436 x 4KB = 约 349MB,与 OOM 日志中的 anon-rss 吻合。

进程链非常清晰:amazon-ssm-agent → ssm-agent-worker → ssm-document-worker → dnf。是 AWS Systems Manager Agent 在执行某个文档(Document),这个文档调用了 dnf 进行软件包更新。

3.4 定时任务排查:为什么是下午 15:00?

既然知道是 SSM Agent 触发的,接下来要找到具体的定时任务:

# 先排除系统 cron
$ crontab -l
# 空的

$ ls /etc/cron.d/
# 空目录

# 查看 systemd 定时器
$ systemctl list-timers --all | head -15
NEXT                        LEFT     LAST                        PASSED    UNIT
Fri 2026-02-27 16:00:00 CST 6min     Fri 2026-02-27 15:50:14 CST 2min ago sysstat-collect.timer
Sat 2026-02-28 03:51:02 CST 11h      Fri 2026-02-27 07:06:52 CST 8h ago   update-motd.timer
...

系统层面没有 dnf 相关的定时器。那定时任务一定来自 AWS 侧 -- 也就是 AWS Systems Manager 的 Patch Manager 或 State Manager。

查看 SSM Agent 的执行记录:

# SSM Agent 的文档执行目录
$ ls /var/lib/amazon/ssm/*/document/orchestration/
... update/  # 这个目录记录了自动更新任务的执行历史

在 AWS Console 的 Systems Manager → State Manager 中可以看到,有一个关联(Association)配置了 AWS-UpdateSSMAgent 文档,每天在 UTC 07:00(北京时间 15:00)执行。这就是每天下午 15:00 触发 dnf 的根源。

四、根因分析:完整的故障链

把所有线索串起来,故障的完整链路如下:

AWS Systems Manager State Manager
  设置了每日 UTC 07:00 (北京时间 15:00) 执行关联
    ↓
amazon-ssm-agent.service 收到执行指令
    ↓
启动 ssm-document-worker 执行更新文档
    ↓
文档内部调用 dnf 进行软件包检查/更新
    ↓
dnf 加载仓库元数据、解析依赖关系
  需要 350-370MB 内存(解压 RPM 数据库、构建依赖树)
    ↓
系统总内存 904MB,已用约 550MB,可用约 350MB
  dnf 的内存需求刚好等于或超过可用内存
    ↓
物理内存耗尽,无 swap 可用
    ↓
内核触发 OOM Killer,杀死内存占用最大的 dnf
    ↓
SSM Agent 检测到子进程异常退出,可能尝试重试
  或者 OOM 后系统已经不稳定(内核数据结构损坏)
    ↓
系统完全卡死,SSH 无响应,只能强制重启

4.1 为什么 dnf 这么吃内存?

dnf(以及 yum)是 RPM 系发行版的包管理器,它在执行更新时需要:

  1. 下载并解析仓库元数据:Amazon Linux 2023 的仓库元数据(repodata)压缩后约 20MB,解压后可达 100MB+
  2. 加载 RPM 数据库:本地已安装包的数据库,通常 50-100MB
  3. 构建依赖解析树:dnf 使用 libsolv 库进行 SAT 求解器式的依赖解析,这个过程需要在内存中构建完整的包依赖图
  4. 下载和校验包文件:虽然可以流式处理,但 dnf 默认会缓存一部分在内存中

这些操作加在一起,350MB 的内存占用并不意外。在 2GB+ 内存的机器上这不是问题,但在 904MB 的 t3.nano 上就是致命的。

4.2 为什么杀掉 dnf 后系统还是崩了?

这是很多人的疑问:OOM Killer 不是已经杀掉了最大的进程吗?释放了 349MB 内存,系统应该恢复才对。

实际情况更复杂:

  • OOM 发生时系统已经极度不稳定:内核在内存极度紧张时,很多内部操作(如日志写入、进程调度、网络栈)都可能因为分配不到内存而失败
  • 杀进程本身也需要内存:发送 SIGKILL、回收进程资源、更新内核数据结构都需要少量内存分配,在极端情况下这些操作也可能失败
  • 连锁反应:dnf 被杀后,SSM Agent 可能尝试重新执行任务,再次启动 dnf,形成 OOM → kill → restart → OOM 的死循环
  • 内核 panic:如果 OOM Killer 无法释放足够内存,或者关键内核线程被杀,内核可能直接 panic 重启

可以通过内核参数确认系统的 OOM 行为:

# 查看 OOM 后是否自动 panic
$ cat /proc/sys/vm/panic_on_oom
0    # 0 = 不 panic,尝试杀进程恢复
     # 1 = 直接 panic 重启

# 查看 panic 后是否自动重启
$ cat /proc/sys/kernel/panic
0    # 0 = panic 后挂起
     # >0 = panic 后等待 N 秒自动重启

虽然 panic_on_oom=0 表示内核会尝试杀进程恢复,但在我们的案例中,系统在 OOM 后进入了不可恢复的状态。

4.3 内存使用全景

把正常运行时的内存使用画一张全景图:

组件内存占用说明
内核 + systemd~80MB内核自身、systemd、基础守护进程
SSM Agent~50MBamazon-ssm-agent 主进程 + worker
业务服务~200MBfilebeat、应用进程等
sshd + 其他~20MBSSH 守护进程、chronyd 等
文件系统缓存~200MB可回收,但回收需要时间
正常总计~550MB占总内存 60%
可用~350MB剩余 40%
dnf 更新需要350-370MB刚好等于或超过可用内存

这就是典型的"刚好不够"场景 -- 平时运行没问题,但一旦有额外的内存需求(比如 dnf 更新),就会触发 OOM。

五、解决方案:分层实施

5.1 紧急止血:禁用自动更新(5 分钟)

最紧急的是阻止 dnf 再次被触发:

# 方法一:在系统层面禁用 dnf 自动更新定时器
sudo systemctl disable --now dnf-automatic.timer 2>/dev/null
sudo systemctl disable --now dnf-makecache.timer 2>/dev/null

# 方法二:彻底屏蔽(mask 比 disable 更强,即使其他服务依赖也不会启动)
sudo systemctl mask dnf-automatic.service
sudo systemctl mask dnf-automatic.timer

# 验证
systemctl list-timers --all | grep dnf
# 应该没有输出

同时在 AWS Console 中:

  1. 进入 Systems Manager → State Manager
  2. 找到关联了 AWS-UpdateSSMAgentAWS-RunPatchBaseline 的 Association
  3. 编辑该 Association,将此实例从目标中移除,或直接删除该 Association

注意:仅在系统层面禁用 dnf 定时器是不够的,因为 SSM Agent 是通过自己的进程直接调用 dnf 的,不走 systemd timer。必须同时在 AWS 侧禁用。

5.2 紧急止血:增加 swap 空间(10 分钟)

即使禁用了自动更新,也建议加上 swap 作为安全网:

# 创建 2GB swap 文件
# 为什么用 2GB?经验法则是物理内存的 1-2 倍,但至少 1GB
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
sudo chmod 600 /swapfile    # 安全要求:swap 文件只能 root 读写
sudo mkswap /swapfile       # 格式化为 swap
sudo swapon /swapfile       # 立即启用

# 验证
$ free -h
               total    used    free    shared  buff/cache  available
Mem:           904Mi   398Mi   145Mi   0.0Ki   360Mi       464Mi
Swap:          2.0Gi     0B    2.0Gi   # swap 已启用

# 永久生效(写入 fstab,重启后自动挂载)
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 调整 swappiness
# swappiness 控制内核使用 swap 的倾向:
#   0  = 尽量不用 swap(除非物理内存完全耗尽)
#   10 = 轻度使用(推荐服务器设置)
#   60 = 默认值(桌面系统)
#   100 = 积极使用 swap
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

为什么 swappiness 设为 10 而不是 0?

设为 0 并不是"完全不用 swap",而是"尽量不用"。但在极端情况下内核仍然会使用。设为 10 是一个平衡点:正常情况下几乎不用 swap(避免性能下降),但在内存紧张时会适度使用(避免 OOM)。

5.3 短期方案:优化 SSM Agent 配置

如果业务需要保留 SSM Agent(用于远程管理、补丁合规等),可以限制它的内存使用:

# 通过 systemd 限制 SSM Agent 的内存上限
sudo systemctl edit amazon-ssm-agent.service

# 添加以下内容:
[Service]
MemoryMax=200M
MemoryHigh=150M

MemoryMax 是硬限制,超过就触发 cgroup 级别的 OOM(只杀该 cgroup 内的进程,不影响整个系统)。MemoryHigh 是软限制,超过后内核会积极回收该 cgroup 的内存。

5.4 长期方案:升级实例类型

t3.nano 的 904MB 内存对于运行现代 Linux + 业务服务来说确实太紧张了:

实例类型vCPU内存月费用(us-east-1)建议
t3.nano20.5GB~$3.8不推荐
t3.micro21GB~$7.6勉强
t3.small22GB~$15.2推荐
t3.medium24GB~$30.4充裕

从 t3.nano 升级到 t3.small,月费用增加约 $11,但可以彻底避免 OOM 问题。在生产环境中,一次 OOM 导致的业务中断成本远超这个费用。

六、监控预警:防患于未然

6.1 内存监控脚本

在 OOM 发生之前收到告警,才能提前处理:

#!/bin/bash
# /usr/local/bin/monitor_memory.sh
# 每 5 分钟由 cron 执行,检查内存使用情况

THRESHOLD_WARN=80    # 警告阈值
THRESHOLD_CRIT=90    # 严重阈值

# 计算内存使用率(基于 available,而不是 free)
MEM_TOTAL=$(awk '/MemTotal/{print $2}' /proc/meminfo)
MEM_AVAIL=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
MEM_USED_PCT=$(( (MEM_TOTAL - MEM_AVAIL) * 100 / MEM_TOTAL ))
MEM_AVAIL_MB=$(( MEM_AVAIL / 1024 ))

if [ $MEM_USED_PCT -ge $THRESHOLD_CRIT ]; then
    logger -p local0.crit \
      "CRITICAL: 内存使用率 ${MEM_USED_PCT}%, 可用 ${MEM_AVAIL_MB}MB"
    # 记录当前内存占用 TOP 10 进程,方便事后分析
    ps aux --sort=-%mem | head -11 | logger -p local0.crit
elif [ $MEM_USED_PCT -ge $THRESHOLD_WARN ]; then
    logger -p local0.warning \
      "WARNING: 内存使用率 ${MEM_USED_PCT}%, 可用 ${MEM_AVAIL_MB}MB"
fi
# 添加到 crontab
sudo crontab -e
# 每 5 分钟执行
*/5 * * * * /usr/local/bin/monitor_memory.sh

6.2 CloudWatch 内存告警

默认情况下 CloudWatch 不采集内存指标(只有 CPU、网络、磁盘 IO)。需要安装 CloudWatch Agent:

# 安装 CloudWatch Agent
sudo yum install -y amazon-cloudwatch-agent

# 使用向导生成配置(或手动编辑)
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard

# 关键配置项(/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json):
{
  "metrics": {
    "metrics_collected": {
      "mem": {
        "measurement": ["mem_used_percent", "mem_available"],
        "metrics_collection_interval": 60
      },
      "swap": {
        "measurement": ["swap_used_percent"],
        "metrics_collection_interval": 60
      }
    }
  }
}

然后在 CloudWatch 中创建告警:

  • 指标:mem_used_percent
  • 条件:大于 85% 持续 5 分钟
  • 动作:发送 SNS 通知(邮件 / Slack / PagerDuty)

七、排查方法论:遇到 OOM 怎么查

总结一套通用的 OOM 排查流程,下次遇到类似问题可以直接套用:

# 第一步:确认是否发生了 OOM
journalctl -p err -b -1 --no-pager | grep -i "out of memory"
dmesg | grep -i "oom\|out of memory"

# 第二步:查看被杀的进程和内存占用
journalctl -b -1 | grep "Killed process"
# 关注 anon-rss 字段,这是实际物理内存占用

# 第三步:查看谁触发了 OOM
journalctl -b -1 | grep "invoked oom-killer"
# 关注 task_memcg 字段,这是进程所属的 cgroup/service

# 第四步:查看 OOM 时的内存全景
journalctl -b -1 | grep -A 50 "invoked oom-killer" | head -80
# 内核会打印所有进程的内存使用情况

# 第五步:查看当前内存配置
free -h                          # 内存和 swap 概览
cat /proc/meminfo                # 详细内存信息
cat /proc/sys/vm/swappiness      # swap 使用倾向
cat /proc/sys/vm/panic_on_oom    # OOM 后是否 panic
swapon --show                    # swap 设备信息

# 第六步:查看当前内存占用 TOP 进程
ps aux --sort=-%mem | head -20
# 或者更精确的:
smem -t -k -s rss | tail -20    # 需要安装 smem

八、最佳实践清单

实践说明优先级
配置 swap 空间至少 1GB,为内存紧张时提供缓冲必须
小实例禁用自动更新2GB 以下内存的实例不要跑 dnf 自动更新必须
设置内存监控告警85% 警告,95% 严重,在 OOM 前介入强烈建议
限制服务内存上限用 systemd 的 MemoryMax 限制非核心服务强烈建议
选择合适的实例类型生产环境至少 2GB 内存(t3.small)强烈建议
保护关键进程对 sshd 等设置 oom_score_adj=-1000建议
定期检查内核日志关注 journalctl -p err 中的 OOM 记录建议

九、写在最后

这次故障的本质是资源配置不足(904MB 内存 + 无 swap)遇上了不合理的自动化策略(小实例上跑 dnf 全量更新)。两个因素单独存在都不会出问题,但叠加在一起就是一颗定时炸弹。

排查的关键在于:从 OOM 日志中读出被杀进程的身份(dnf)、归属(amazon-ssm-agent.service)和触发时间(每天 15:00),然后反向追溯到 AWS Systems Manager 的自动更新配置。

最后分享一个经验:在小型实例上,每一个自动化任务都要评估它的资源消耗。大实例上无感的操作(比如 dnf update),在小实例上可能就是压垮骆驼的最后一根稻草。自动化是好帮手,但前提是你了解它在做什么、需要多少资源。

tr>
✸ ✸ ✸

📜 版权声明

本文作者:王梓 | 原文链接:https://www.bthlt.com/note/14994465-LinuxEC2实例OOM死机故障深度分析

出处:葫芦的运维日志 | 转载请注明出处并保留原文链接

📜 留言板

留言提交后需管理员审核通过才会显示