服务器内存耗尽宕机与资源调优分析
一、事件概述
前端服务器(CentOS 7 虚拟机,48GB 内存 / 24 vCPU / 5GB Swap,运行 Rails + Passenger + MySQL + NFS 挂载)发生整机卡死,SSH 无法连接、服务停止响应,最终通过硬重启恢复。排查后确认是内存耗尽(OOM)引发的连锁故障。
二、故障时间线
| 时间 | 事件 |
|---|---|
| T0 起 | 一个日志归档服务进入崩溃重启循环(约 25 秒一次),持续 1.5 小时以上 |
| T0+1.5h | 内核出现 page allocation failure,内存耗尽 |
| 随后 | NFS 客户端报 “server not responding”——连网卡收包都分配不到内存 |
| 随后 | OOM Killer 触发,Free swap = 0,48GB 内存 + 5GB swap 全部耗尽 |
| 随后 | OOM 连续杀多个进程仍无法恢复,系统彻底卡死(日志中断约 14 小时) |
| 次日 | 硬重启后恢复 |
三、根本原因
多因素叠加导致的内存耗尽:
1. Passenger Worker 过多(主因)
OOM 时进程列表显示 200+ 个 Ruby worker 进程,每个占用 60160MB,合计 3040GB。配置为固定常驻 110 个 worker(PassengerMaxPoolSize 110 + PassengerMinInstances 110),每个约 310MB,是 48GB 机器上最大的内存消耗来源。
2. 日志归档服务崩溃循环(加压因素)
该服务每 25 秒重启一次,每次都加载完整 Rails 环境(约 100-200MB),持续给已紧张的内存施压。
3. Swap 不足
5GB swap 对 48GB 内存比例过小,没有缓冲空间,很快用尽。
4. NFS 连锁卡死
内存耗尽后,虚拟网卡驱动无法分配页面接收网络包 → NFS 客户端无法完成写回 → 访问 NFS 挂载点的进程进入不可中断(D)状态 → SSH 也被拖住 → 整机卡死。
5. 内核保留内存过低(关键放大因素)
vm.min_free_kbytes 默认仅 66MB。这个参数决定内核为网卡收包、中断处理、磁盘 I/O 等”原子分配”保留的最低内存。66MB 在 48GB 机器上太小,当用户进程把内存吃到只剩几十 MB 时,内核关键路径(网络、NFS)先于 OOM Killer 崩溃,导致网络瘫痪。
四、恢复措施
- 硬重启 NFS 服务器(先恢复存储)
- 硬重启前端服务器
- 清理残留的状态文件(解决某个 rake 任务因残留文件报错的问题)
五、长期改进措施
已实施
1. 提高内核保留内存
vm.min_free_kbytes = 524288 # 66MB → 512MB
写入 /etc/sysctl.conf 持久化。保证内存紧张时内核关键路径(网络收包、NFS)不会先于 OOM Killer 崩溃,系统有机会正常回收内存而非直接网络瘫痪。
2. 限制 Passenger Worker 数量
基于一周监控数据(采样间隔 5 分钟),实际并发峰值仅 56 个 worker,请求队列始终为 0——说明 110 个配置严重过剩,其中约 88 个从未处理过任何请求。
| 配置项 | 调整前 | 调整后 |
|---|---|---|
| PassengerMaxPoolSize | 110 | 80 |
| PassengerMinInstances | 110 | 20 |
效果:峰值内存从约 41GB 降到约 25GB,低峰期只保留 20 个常驻进程(空闲时释放内存),有请求时自动扩容到最多 80。
3. 优化 Delayed Job Worker 配置
原配置 36 个 worker(72 进程,含监控进程),分池如下:
- tracking: 1
- log_import: 5
- log_export: 16 ← 一周零任务,代码中无任何引用
- 通配队列: 14
数据分析发现:log_import 是唯一的重负载队列(单日处理约 5000 次,凌晨 4-6 点为高峰),而 log_export 队列已废弃(日志归档改由其他机制处理)。调整为:
- tracking: 1
- log_import: 10(翻倍,缓解凌晨积压和慢导入)
- 通配队列: 5
- log_export: 删除
worker 从 36 降到 16(进程从 72 降到 32)。staging 实测验证:单进程内存一致,新配置省约 6.6GB(空闲态),运行态节省更多。
建议
- Swap 增加到 8-16GB,提供 OOM 前的缓冲
- NFS 挂载使用
soft,timeo=10选项,防止存储无响应时拖死整个系统 - 给应用中同步的 ping/getent 外部调用加超时和缓存,减少不必要的子进程
六、关键经验
内存监控要看并发峰值,不是配置值。 一周监控证明 110 配置里 80% 是浪费,实际峰值才 56。用数据说话,避免凭感觉配置。
vm.min_free_kbytes是容易被忽视的关键参数。 内存耗尽时,”内核先崩还是 OOM Killer 先动”决定了是”网络瘫痪整机卡死”还是”杀几个进程自愈”。保留内存太小会让故障从”可恢复”变成”不可恢复”。进程统计要小心 grep 把自己算进去。
ps | grep xxx | wc -l常把执行命令的 shell/grep 自身算入,统计进程数时应按进程名精确匹配或用pgrep。监控进程(monitor)的内存开销不能忽略。 每个 delayed_job worker 配一个 monitor,monitor 内存和 worker 几乎一样大。算内存账时漏掉 monitor 会严重低估。
七、通俗版复述
发生了什么
有一台服务器突然”死机”了——远程连不上,上面跑的服务也全停了,最后只能强制重启才救回来。
为什么会这样
打个比方,服务器的内存就像一个房间,里面要住很多”工人”(处理网页请求的进程)。问题出在几个方面凑一起:
工人雇太多了。 本来房间最多舒服地住 50 来个工人就够干活了,但当初设置成强制塞进 110 个,而且大部分工人整天闲着没活干,却一直占着床位(内存)。
有个坏掉的程序在不停重启。 一个负责打包日志的程序每 25 秒就崩溃重启一次,每次重启都要占一大块地方,持续给本就拥挤的房间添乱。
应急空间太小。 系统有个”备用仓库”(swap)但太小了,很快就塞满。
最致命的一点: 操作系统给自己留的”应急口粮”(内核保留内存)只有 66MB,太少了。当房间被工人挤爆时,操作系统连”收发网络消息”这种最基本的事都没内存干了。结果就是网络先瘫痪,连带存储(NFS)也卡死,整台机器彻底僵住——连”把多余工人赶出去”这个自救动作都做不了了。
怎么修好的
先强制重启,把机器救活。
把操作系统的应急口粮从 66MB 提到 512MB。 这样以后即使房间又挤,操作系统至少还能喘气、还能把多余的工人赶出去自救,不会直接僵死。
把工人数量从 110 砍到最多 80,平时只留 20 个。 用一周的实际数据证明了:就算最忙的时候也就用到 56 个,110 纯属浪费。平时留 20 个待命,忙起来再自动加人,闲下来自动遣散,省内存。
清理另一批后台工人(delayed job)。 发现有一组 16 个工人专门负责一个早就没人用的任务,白白占着内存;而真正忙的那个任务反而人手不够。于是把闲的裁掉、给忙的加人,总人数从 36 减到 16。
得到的教训
- 配多少资源要看实际用量,别拍脑袋。 数据显示 110 个工人里 80 个从没干过活。
- 操作系统的”应急口粮”这个小参数,关键时刻决定生死。 太小会让”本来能自救的故障”变成”彻底死机”。
- 数人数的时候别把自己数进去。 用命令统计进程数时,经常会把”执行统计这条命令”本身也算进去,导致数字虚高。