4.3 内存不足、OOM 与数据库慢查询治理

先给一个判断

内存问题的核心是分清“真不够”和“看似不够”。 free -h 里 used 很大不代表内存紧张——Linux 会把闲下来的内存拿去做缓存,需要时自动还回来。只看 available 那一列:available 还有很多(比如 1G 以上),used 再高也不用管;available 只剩几百 M 甚至几十 M,才是真的紧张。

看懂 free 的三行

free -h
列 含义 怎么用
total 物理内存总量 机器到底多少 G
used 已使用(含进程 + 不算缓存的部分) 参考价值低
free 完全空闲 几乎永远是小的,正常
buff/cache 磁盘缓存 不用紧张,会自动让出来
available 真正能用的 这一列才是判断依据

一个生活中的类比:内存就像一个仓库,缓存是临时堆在过道上的货物。看着仓库满满的,但过道上的货随时能搬走,真正的可用空间看 available 就行。因为缓存的存在,Linux 内存“越用越多”是健康的表现,不是坏事。

谁在吃内存

# 按内存占用排序看前 10 个进程
ps aux --sort=-%mem | head -11

# top 交互模式按 M 切换到内存排序
top

常见的大户:

大户 特征 治法
MySQL / PostgreSQL 配置的缓存池按业务规模设 调配置参数
Java 应用 JVM 默认会吃不少 限制 JVM 堆大小
Redis / MongoDB 吃内存是设计如此 看业务要不要扩容
某个异常应用 占用远超正常水平 查应用内存泄漏
面板自身 正常占用很小 长期异常才需要查

应用容器内存占用看 docker stats:

# 实时看各容器内存占用(按 Ctrl+C 退出)
docker stats

# 不跟,只看一眼
docker stats --no-stream

注意一点:docker stats 显示的内存里包含页缓存,所以单看数字可能偏高。判断容器是否真的紧张,要看它是否接近了你设置的内存上限,以及有没有 OOM 倾向。

OOM:系统替你做的“残忍决定”

内存真不够时,内核会挑一个进程杀掉自救,这就是 OOM(Out Of Memory)。症状很有特点:某个应用突然挂了,重启后又活蹦乱跳,过一段时间又挂。

# 查内核日志里有没有 OOM 记录
dmesg | grep -i -E 'oom|killed process' | tail -20

# 或翻系统日志
grep -i -E 'oom|killed process' /var/log/messages 2>/dev/null | tail -20

看到 Killed process xxxx (mysqld) 之类的记录——那就实锤是 OOM 了。内核杀进程的策略是“挑占内存最大的”,所以死的往往是你最重要的服务。

OOM 的处理顺序:

  1. 确认是哪个进程被杀——dmesg 里写得明明白白;
  2. 先治标:给重要容器加内存限制(见下),让内核有明确的上限可依,而不是临场挑软柿子捏;
  3. 再治本:被杀的进程为什么会吃这么多内存——MySQL 就是缓存参数,应用就是泄漏,该调调该升升级;
  4. 最后才是加内存——给机器加内存之前先把前两条做完,不然新内存照样被吃光。

给应用套上内存上限

在 1Panel 的容器管理里,编辑容器可以设置内存限制。这条配置有两个好处:

  • 防止单个容器吃光内存拖垮全机;
  • OOM 时内核有明确目标,不会误杀别的服务。

反面提醒:上限不能拍脑袋设。MySQL 这类数据库,上限设得比它的缓存池还小,等于逼它崩。设上限前先 docker stats 观察几天正常占用,上限给到正常值的 1.5~2 倍。

swap:自救用的缓冲垫

# 看交换分区状态
swapon --show

# 看使用了多少(free 命令也行)
free -h

swap 是把硬盘当内存用——速度慢,但能扛住内存的瞬时高峰。云服务器默认往往没有 swap,建议设一块小容量的(内存的 1 到 2 倍即可,不用太大)当缓冲垫:

# 创建 2G 的 swap 文件(谨慎操作,注意磁盘空间)
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# 永久生效
echo '/swapfile swap swap defaults 0 0' >> /etc/fstab

一条经验:swap 是缓冲垫不是替代品。如果 swap 长期大量被用,说明物理内存真的不够了,该加机器加机器——靠硬盘扛内存只会把系统拖慢,从而进入“因为慢所以更卡”的恶性循环。

慢查询:内存的隐形杀手

内存被数据库吃光,十有八九是慢查询在作怪。 一条没有索引的全表扫描,能让 MySQL 的缓存疯狂膨胀、CPU 飙高、内存见底,最后被 OOM 干掉。

以 MySQL 为例,打开慢查询日志:

-- 看慢查询日志开没开、阈值是多少
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';

-- 临时开启(重启失效,永久生效改配置文件)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;

开启后,执行时间超过阈值(比如 2 秒)的语句会被记进慢日志。在 1Panel 的数据库管理里,也可以直接通过界面开启慢日志配置——它本质上也是帮你改这些数据库参数。

慢日志里找重点:

# 看慢日志里出现最多的语句(路径按实际情况替换)
mysqldumpslow -s c -t 10 /var/lib/mysql/慢日志文件名

-s c 按次数排序,-t 10 只看前十条。出现次数最多的那条,往往就是治好内存问题的钥匙。

慢查询的典型治法:

病因 特征 修法
缺索引 EXPLAIN 显示全表扫描(type 为 ALL) 加索引
索引失效 类型隐式转换、用了函数 改查询写法
返回行数巨大 没分页的统计查询 加 LIMIT / 分批
关联太多表 多表 JOIN 全表扫 拆查询、冗余字段
锁等待 查询本身不慢但一直排队 看有没有长事务

一个完整的复现与修复

现象:网站时不时 502,MySQL 容器隔几天就重启一次,面板监控里内存曲线像过山车。

排查过程:

  1. free -h —— available 只剩 300M,物理内存确实紧张;
  2. dmesg | grep -i oom —— 赫然写着 Killed process ... (mysqld),MySQL 被 OOM 杀过;
  3. docker stats --no-stream —— MySQL 容器正常就占 1.2G,没到内存上限,那问题不在容器配置;
  4. 打开数据库慢日志,发现同一条统计查询每分钟跑一次,单次执行 8 秒——没有索引的全表扫描。

修复:

  1. 给那条查询对应的字段加上索引,单次执行降到几十毫秒;
  2. 给 MySQL 容器设置一个合理的内存上限,避免再被 OOM 误杀;
  3. 建一块 2G 的 swap 作为缓冲;
  4. 观察一周,内存曲线平稳,网站再没 502。

复盘要点:表面上是“内存不够”,实际上是“一条 SQL 在烧内存”。如果一上来就加内存,这条慢查询会把新内存也吃掉,问题原样再来一遍。性能问题永远是这样:找到根因再花钱,钱才花在刀刃上。

本节要点

  • 判断内存只看 free -h 的 available 列,used 高不一定有问题;
  • 查大户用 ps aux --sort=-%mem 和 docker stats;
  • 应用莫名被杀,先 dmesg | grep oom 确认是不是 OOM;
  • 慢查询是内存隐形杀手,治内存先看慢日志;
  • swap 是缓冲垫不是替代品,长期吃 swap 说明该加内存了。

数据库的详细配置说明见 1Panel 官方文档:MySQL,以官方文档为准。

下一节:日志和备份文件越长越大——4.4 节教你怎么用日志切割和计划任务从源头治理。