一、故障现象:规律性失联
先看故障的时间线:
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 实际分配的物理内存略多,系统可见约 904MBused(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:共享内存占用为 0pgtables: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 的核心逻辑是:
- 计算每个进程的 oom_score:分数越高越容易被杀。计算依据主要是进程占用的内存量 -- 占用越多,分数越高
- 考虑 oom_score_adj 调整值:范围 -1000 到 1000。-1000 表示永远不杀(比如 sshd),1000 表示优先杀。可以通过
/proc/PID/oom_score_adj查看和设置 - 选择分数最高的进程杀掉:发送 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 系发行版的包管理器,它在执行更新时需要:
- 下载并解析仓库元数据:Amazon Linux 2023 的仓库元数据(repodata)压缩后约 20MB,解压后可达 100MB+
- 加载 RPM 数据库:本地已安装包的数据库,通常 50-100MB
- 构建依赖解析树:dnf 使用 libsolv 库进行 SAT 求解器式的依赖解析,这个过程需要在内存中构建完整的包依赖图
- 下载和校验包文件:虽然可以流式处理,但 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 | ~50MB | amazon-ssm-agent 主进程 + worker |
| 业务服务 | ~200MB | filebeat、应用进程等 |
| sshd + 其他 | ~20MB | SSH 守护进程、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 中:
- 进入 Systems Manager → State Manager
- 找到关联了
AWS-UpdateSSMAgent或AWS-RunPatchBaseline的 Association - 编辑该 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.nano | 2 | 0.5GB | ~$3.8 | 不推荐 |
| t3.micro | 2 | 1GB | ~$7.6 | 勉强 |
| t3.small | 2 | 2GB | ~$15.2 | 推荐 |
| t3.medium | 2 | 4GB | ~$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死机故障深度分析
出处:葫芦的运维日志 | 转载请注明出处并保留原文链接


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