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 的处理顺序:
- 确认是哪个进程被杀——dmesg 里写得明明白白;
- 先治标:给重要容器加内存限制(见下),让内核有明确的上限可依,而不是临场挑软柿子捏;
- 再治本:被杀的进程为什么会吃这么多内存——MySQL 就是缓存参数,应用就是泄漏,该调调该升升级;
- 最后才是加内存——给机器加内存之前先把前两条做完,不然新内存照样被吃光。
给应用套上内存上限
在 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 容器隔几天就重启一次,面板监控里内存曲线像过山车。
排查过程:
free -h—— available 只剩 300M,物理内存确实紧张;dmesg | grep -i oom—— 赫然写着Killed process ... (mysqld),MySQL 被 OOM 杀过;docker stats --no-stream—— MySQL 容器正常就占 1.2G,没到内存上限,那问题不在容器配置;- 打开数据库慢日志,发现同一条统计查询每分钟跑一次,单次执行 8 秒——没有索引的全表扫描。
修复:
- 给那条查询对应的字段加上索引,单次执行降到几十毫秒;
- 给 MySQL 容器设置一个合理的内存上限,避免再被 OOM 误杀;
- 建一块 2G 的 swap 作为缓冲;
- 观察一周,内存曲线平稳,网站再没 502。
复盘要点:表面上是“内存不够”,实际上是“一条 SQL 在烧内存”。如果一上来就加内存,这条慢查询会把新内存也吃掉,问题原样再来一遍。性能问题永远是这样:找到根因再花钱,钱才花在刀刃上。
本节要点
- 判断内存只看
free -h的 available 列,used 高不一定有问题; - 查大户用
ps aux --sort=-%mem和docker stats; - 应用莫名被杀,先
dmesg | grep oom确认是不是 OOM; - 慢查询是内存隐形杀手,治内存先看慢日志;
- swap 是缓冲垫不是替代品,长期吃 swap 说明该加内存了。
数据库的详细配置说明见 1Panel 官方文档:MySQL,以官方文档为准。
下一节:日志和备份文件越长越大——4.4 节教你怎么用日志切割和计划任务从源头治理。
