服务器突然变卡,free 一看内存快被吃光,我顺着 ps 和 dmesg 揪出了真凶
周三下午三点多,正在跑的一台 2G 内存 ECS 突然卡得不行。SSH 连上去要等十几秒,敲个 ls 都要顿一下,业务接口直接超时。同事在群里问是不是机器挂了,我先登录上去看了一眼:
free -h
输出大概是这样:
total used free shared buff/cache available
Mem: 1.9Gi 1.7Gi 46Mi 12Mi 180Mi 120Mi
Swap: 0B 0B 0B
available 只剩 120M,free 只有 46M。第一反应是内存被什么东西吃光了,但到底是谁吃的,还得一层层扒。
先被 free 的数字唬了一下
刚学 Linux 那会儿我看到 used 1.7G 就慌,其实这里面有个老坑:buff/cache 那 180M 是内核拿来做文件缓存的,内存紧张的时候它会自己吐出来,真正要看的是 available 那一列。available 120M 是真的告急了,不是缓存虚胖。
还有一个细节:这台机器 Swap 是 0。2G 内存的小机器没配 Swap,等于一点缓冲余地都没有,内存一满就只能靠 OOM killer 动手。
ps 排序,差点被共享内存骗了
接着找吃内存的大户:
ps aux --sort=-%mem | head -15
排第一的是个 Python 进程,RSS 900M 左右。第二名是个 Java 进程,RSS 600M 左右。两家加起来 1.5G,基本对得上。
但这里有个坑我以前踩过:ps 显示的 RSS 里包含共享内存,同一个共享库被十个进程映射,就会重复算十次。所以光看 RSS 排序,只能当线索,不能直接定罪。想看得准一点,得看 PSS(按比例分摊后的共享内存):
# smem 没装的话先装一个,Debian/Ubuntu
sudo apt install -y smem
smem -s pss -r | head -10
或者直接翻 /proc:
cat /proc/<pid>/status | grep -E 'VmRSS|VmSize|VmSwap'
grep Pss /proc/<pid>/smaps | awk '{s+=$2} END {print s " kB"}'
我翻了那个 Python 进程的 smaps,PSS 也有 800M 出头,共享内存那点水分影响不大——这家伙确实是真凶,不是被冤枉的。
dmesg 里找到了 OOM killer 的记录
正准备 kill 掉它之前,我想确认一下它到底在干什么,顺手翻了下内核日志:
dmesg -T | grep -i -A 5 'oom-killer\|out of memory'
还真有:
[Wed Nov 11 02:14:33 2026] python3 invoked oom-killer: gfp_mask=0x140cca, order=0
[Wed Nov 11 02:14:33 2026] Out of memory: Killed process 18432 (python3) total-vm:1823456kB
有意思,凌晨两点多 OOM killer 已经杀过一次这个 Python 进程了,早上它又被 systemd 拉起来,继续吃内存,下午终于把机器拖垮。也就是说这不是突发事件,是个反复发作的老毛病。
顺着进程的启动时间往回查:
ps -o pid,lstart,cmd -p <pid>
systemctl status <对应的服务名>
对应的 systemd 服务凌晨两点被拉起来过一次,服务文件里 Restart=always,被杀了就自动重启,重启了继续吃内存,等于一个死循环。更离谱的是,这个任务本来只是每天凌晨跑一次数据清洗的定时脚本,不知道什么时候被人改成了常驻服务。
揪出内存泄漏的那几行代码
把脚本翻出来看,问题一眼就看出来了:
# 问题代码:把全表数据一次性读进内存再处理
rows = cursor.fetchall() # 表里 200 多万行
results = []
for r in rows:
results.append(process(r))
200 多万行全塞进一个 list,再一份份处理,内存不爆才怪。而且这个表最近两个月涨得很快,以前数据量小的时候跑得好好的,没人发现这个写法有问题。
改成流式处理,改动其实很小:
# 改后:一次只取一批,处理完就扔
cursor.execute("SELECT ... FROM ...")
while True:
batch = cursor.fetchmany(1000)
if not batch:
break
for r in batch:
process(r)
改完之后 RSS 从 900M 掉到 80M 左右。顺手还把 Restart=always 改成 Restart=on-failure,并且加了 RestartSec=60,避免被杀了立刻原地复活继续吃内存。
治标的两招:Swap 和 earlyoom
代码修了,但 2G 内存的机器还是太薄,我又补了两招兜底。
第一招是加 Swap。注意一个坑:用 fallocate 建出来的文件可能有空洞,mkswap 会直接报错,稳妥的办法是用 dd:
# 2G Swap,用 dd 写实数据,别用 fallocate
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
顺手把 vm.swappiness 调到 10,让内核不到万不得已别去碰 Swap:
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
第二招是装 earlyoom,让它在内存耗尽之前先动手杀掉最肥的进程,别等内核的 OOM killer 在半夜乱杀:
sudo apt install -y earlyoom
sudo systemctl enable --now earlyoom
事后复盘,我记了三条
第一,free 要看 available,不是 used,缓存那部分是内核借去用的,不算真占用。第二,定位吃内存的进程别只看 RSS 排序,共享内存会重复算,PSS 或者 /proc/<pid>/smaps 更准,dmesg 里的 OOM 记录是最直接的证据。第三,Restart=always 配上有内存泄漏的程序,等于给机器装了个自动复活的炸弹,定时任务就老老实实用 cron 或者 Restart=on-failure。
这台机器现在跑了三天,内存稳在 600M 上下,available 没再掉下过 1G。2G 的小机器,代码写得省一点,照样能撑住。
- 点赞
- 收藏
- 关注作者
评论(0)