我的服务器是最低配的 2 核 2G,跑着 Nginx、PHP-FPM、MySQL 三套服务。配置这么紧,出问题是常事——最严重的一次是 MySQL 半夜被系统杀掉,第二天早上网站打不开。

这篇笔记整理排查这类问题要用的命令。不追求覆盖所有工具,只记我实际用过、并且真正帮我定位到问题的那几个。

第一步:先看整体

free —— 内存够不够

free -h

输出:

               total        used        free      shared  buff/cache   available
Mem:           1.6Gi       784Mi        91Mi       8.0Mi       733Mi       638Mi
Swap:          2.0Gi          0B       2.0Gi

这个命令看起来简单,但有个地方特别容易看错:free 那一列不是"可用内存"

上例里 free 只有 91Mi,看着快爆了。但真正的可用内存要看最后一列 available——638Mi,还算宽裕。

为什么差这么多?因为中间那 733Mi 的 buff/cache。Linux 会拿空闲内存做磁盘缓存,这部分内存随时可以被回收给应用程序用。所以:

可用内存 ≈ available 列,不是 free 列

只看 free 会得出"内存要满了"的错误结论。判断内存压力,看 available 和 Swap 这两列。

Swap 那一行是关键

Swap: 2.0Gi 0B 2.0Gi 表示有 2G 交换分区,当前用量 0。

如果这里的 used 开始持续增长,说明物理内存已经不够用了,系统在往磁盘上倒腾——这时候性能会明显下降,因为磁盘比内存慢几个数量级。

我的服务器上这个 swap 是后来补加的。原因就是下面要说的 OOM。

那次 OOM:完整排查过程

现象

网站白天正常,早上起来打不开。重启后恢复正常,过几天又来一次。

这种"偶发、重启就好"的故障最麻烦——等你去看的时候已经恢复正常了。

第一步:看内核日志

dmesg | grep -i "killed process"

也可以查 journal:

journalctl -k | grep -i oom

输出里找到了关键信息:

Out of memory: Killed process 1234 (mysqld) total-vm:xxxkB, anon-rss:xxxkB

MySQL 被内核杀掉了。这叫 OOM Killer——当系统内存耗尽时,Linux 内核会挑一个"最占内存"的进程杀掉来保命。它不是崩溃,是系统主动杀的。

这解释了两个现象:为什么是半夜(访问量低但定时任务在跑),为什么重启就好(进程重新起来了)。

第二步:为什么内存会耗尽

top 看各进程的内存占用:

top

在 top 界面里按 M(大写 M)可以按内存占用排序——默认是按 CPU 排序的,这个快捷键很实用。

q 退出。想看某个服务的详细情况可以直接查:

systemctl status mysql
ps aux --sort=-%mem | head -10

ps aux --sort=-%mem 直接按内存倒序列出前 10 个进程,比在 top 里按键更直接。

排查下来发现内存被三块吃掉了:

  • MySQL:默认配置下 performance_schema 就占一百多兆,加上 InnoDB 缓冲池和连接线程
  • PHP-FPM:每个进程处理图片时可能吃到几十兆,默认能开 5 个
  • Nginx:相对小,但也不是零

2G 内存(实际可用 1.6G)装这三样,任何一波并发上来就是压死骆驼的最后一根稻草。

第三步:两个解法

补 swap。2G 的机器上 swap 不是"性能优化",是保命措施——内存峰值时让内核有地方可以换出,而不是直接杀进程:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

# 写入 fstab 保证重启后自动挂载
echo '/swapfile none swap sw 0 0' >> /etc/fstab

最后那行 echo 千万别漏——漏了的话 swap 只在当前会话有效,服务器一重启就没了。而 OOM 往往正是发生在重启之后。

调小 MySQL 内存占用。新建一个独立配置文件:

# /etc/mysql/mysql.conf.d/zzz-typecho-optimization.cnf
[mysqld]
innodb_buffer_pool_size = 256M
performance_schema = OFF
innodb_flush_log_at_trx_commit = 2
innodb_flush_method = O_DIRECT
innodb_log_file_size = 64M

其中 performance_schema = OFF 省下的内存最多——这是个性能监控组件,个人博客根本用不上,但默认开着。

文件名以 zzz 开头是有意的:在 conf.d 目录里按字母序排在最后,会覆盖前面的默认值。以后升级 MySQL 覆盖了默认配置也不影响这些设置。

再看 CPU:top 怎么读

top

头部几行是全局信息,其中这一行最关键:

%Cpu(s):  2.3 us,  1.1 sy,  0.0 ni, 96.3 id,  0.3 wa,  0.0 hi,  0.0 si,  0.0 st

逐个字段:

字段含义
us用户态 CPU 占比——应用程序消耗的
sy内核态 CPU 占比——系统调用消耗的
id空闲
wa等待 I/O 的时间——这是最值得关注的一项
st被虚拟机偷走的时间(云服务器上能看到)

wa 高说明瓶颈在磁盘而不是 CPU。如果 id 很低但 us 也不高,同时 wa 很高,那就是磁盘拖慢了整个系统——这时候加 CPU 没用,得从 I/O 上找原因。

另外第二行的 load average 也常被误读:

load average: 0.15, 0.28, 0.21

三个数分别是 1 分钟、5 分钟、15 分钟的平均负载。判断标准是和 CPU 核数比:单核机器上 1.0 表示刚好跑满,2 核机器上 2.0 才算满。

我的机器是 2 核,所以负载到 2.0 以上才需要警惕。看到 0.15 说明很闲。

磁盘:df 和 du

磁盘满了是另一类常见故障——它不会让服务崩溃,但会让写入失败,表现出来往往是一堆莫名其妙的错误。

df -h

看各分区的使用率。注意关注 / 根分区——很多服务的数据、日志、上传文件都堆在这里。

du -sh /var/log/* | sort -rh | head -10

du 是统计目录占用,-s 只显示汇总(不递归每个子目录),-h 人类可读,sort -rh 按大小倒序。

这个命令用来找"是谁把磁盘占满了"。日志文件往往是元凶——尤其是没配置轮转(logrotate)的时候,一个 access.log 能长到几个 G。

另外提醒一个特殊情况:磁盘显示满了,但 dfdu 的数字对不上。这通常是因为有进程删除了文件但还持有文件句柄(文件没真正释放)。找出这类进程:

lsof | grep deleted

进程层面:找到是谁在跑

ps aux | grep nginx
systemctl status php8.1-fpm

我的 PHP-FPM 按这个配置运行:

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

max_children = 5 是关键——它限制同时最多 5 个 PHP 进程。

这个值我调小过。默认配置是给大机器准备的,5 个进程同时处理图片上传,每个吃几十兆,在 2G 内存的机器上很容易出事。加上 max_spare_servers = 3,待命进程不超过 3 个,不会一直占着内存。

代价是并发能力弱——同时来 6 个请求,第 6 个要排队。但博客的实际并发就是个位数,这个取舍划算。

排查思路小结

工具是次要的,重要的是排查顺序。我总结成四步:

第一步:确认现象。是打不开、变慢、还是报错?"偶发"还是"持续"?重启能恢复吗?这一步决定了往哪个方向查。

第二步:查日志。不要一上来就 top。

  • 服务日志:journalctl -u mysql -n 100
  • 内核日志(找 OOM):dmesg | grep -i killed
  • 应用日志:Nginx 的 error.log、PHP 的错误日志

日志通常直接告诉你答案。我那次就是 dmesg 一行就定位到了。

第三步:看资源。按"最可能出问题的顺序"查:内存(free)→ 磁盘(df)→ CPU 和 I/O(top)。2G 小机器上内存是第一嫌疑。

第四步:定配置。找到瓶颈之后再改配置,改完要验证——而且要想到"重启后还生效吗"。swap 那次如果只执行了 swapon 没写 fstab,下次重启就白干了。

这套排查方法里,真正救过我的是两个认知上的纠正。

内存要看 available,不是 freeLinux 会拿空闲内存做磁盘缓存,这部分随时可以回收。把它算成"已用",会得出完全错误的结论——我第一次看到 free 只剩 91Mi 的时候也慌了一下,其实 available 有 638Mi,离危险还远。

OOM 不是崩溃,是内核主动杀的。这个区别决定了去哪查日志:进程自己不知道被杀了,应用日志里什么都没记,必须去内核日志(dmesgjournalctl -k)里找 Killed process。我第一次遇到时把 PHP 和 Nginx 的日志翻了个遍,一点线索都没有。

至于工具本身,来来回回就那几个:free 看内存、df 看磁盘、top 看 CPU 和 I/O、ps aux --sort=-%mem 找内存大户。命令好记,难的是排查顺序——先确认现象,再查日志,然后看资源,最后才动配置。跳过前面几步直接改参数,往往是把问题改得更复杂。

最后说句实在的:这套调优是在 2G 内存的限制下做的妥协。参数调得再细,也不如加两 G 内存来得直接。只是眼下够用,就先这么跑着。