前言
服务器内存不足是运营中最常见的性能问题之一。
很多用户的感受是:服务器买来的时候跑得好好的,随着网站访问量增加、业务功能增多,内存使用率慢慢爬到80%、90%,然后在某个高峰期突然达到100%,网站开始响应极慢,SSH都登不上,最后只能强制重启。
但内存使用率高,不一定意味着内存真的不够——有时候是内存泄漏,有时候是缓存正常占用,有时候是配置问题。盲目升级内存,可能只是推迟了问题,没有从根本上解决。
本文帮你系统判断内存问题的根本原因,再决定该怎么处理。
一、先看懂内存使用情况
查看内存使用概况
free -h
典型输出:
total used free shared buff/cache available
Mem: 7.7Gi 5.2Gi 512Mi 248Mi 2.0Gi 2.3Gi
Swap: 2.0Gi 1.8Gi 200Mi
字段含义:
| 字段 | 说明 |
|---|---|
| total | 总内存 |
| used | 已使用内存(包含缓存) |
| free | 完全空闲的内存 |
| shared | 共享内存 |
| buff/cache | 系统缓存(可以被应用使用时自动释放) |
| available | 真正可用的内存(最重要的指标) |
| Swap used | Swap使用量(Swap高说明物理内存真的不足) |
关键判断:
available是评估内存是否真正紧张的正确指标,而不是freeSwap used高(超过总Swap的50%)说明系统已经在用磁盘补充内存,性能受影响严重buff/cache高是正常的,这是Linux的内存优化机制,不需要担心
查看哪些进程占用最多内存
# 按内存使用量排序显示进程(实时更新)
top -o %MEM
# 或使用ps命令(更详细)
ps aux --sort=-%mem | head -20
# 查看特定进程的内存详情
cat /proc/$(pgrep -f "php-fpm")/status | grep -i vmrss
二、判断问题根本原因
内存使用率高,可能有三种完全不同的原因,处理方式截然不同:
情况一:业务正常增长,内存需求真的增加了
判断依据:
- 内存使用率与访问量正相关,流量高时内存高,流量低时自然回落
- 没有单个进程异常占用,内存分布在多个业务进程之间
- Swap使用量不高或缓慢增长
处理方式: 这是正常现象,当available内存持续低于20%时,考虑升级内存配置。
情况二:内存泄漏
判断依据:
- 某个进程的内存占用持续单向增长,重启后恢复正常,运行一段时间后再次增长
- 内存增长与访问量无关,在低流量时也持续增长
- 最终必须定期重启来"释放"内存
典型场景:
- PHP-FPM进程内存泄漏(最常见)
- Node.js/Python应用内存泄漏
- MySQL长时间运行后内存缓慢增长
处理方式: 需要排查代码或配置,见下节。
情况三:配置不当,内存被过度分配
判断依据:
- 实际访问量不高,但内存使用率很高
- 单个进程(如MySQL、Redis)配置的缓存/缓冲区远超实际需求
典型场景:
- MySQL的
innodb_buffer_pool_size设置过大 - Redis的
maxmemory未设置,持续占用内存 - PHP-FPM的进程数
pm.max_children设置过多
处理方式: 调整配置参数,无需升级硬件。
三、内存泄漏排查方法
PHP-FPM内存泄漏排查(最常见)
PHP-FPM在处理大量请求后,每个Worker进程的内存可能逐渐膨胀(主要因为第三方扩展的内存管理问题)。
配置自动重启缓解泄漏:
在PHP-FPM配置文件(/etc/php/8.1/fpm/pool.d/www.conf)中:
; 每个子进程处理500个请求后自动重启(释放积累的内存泄漏)
pm.max_requests = 500
; 进程管理方式(dynamic适合大多数场景)
pm = dynamic
; 控制最大进程数(避免进程过多耗尽内存)
pm.max_children = 20 ; 根据内存计算:(可用内存MB / 单个进程MB)
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
计算合适的max_children值:
# 查看单个PHP-FPM进程占用内存
ps aux | grep php-fpm | awk '{print $6}' | sort -rn | head -5
# 输出单位是KB,如显示50000则每个进程约50MB
# 计算公式:
# max_children = (可用物理内存 * 0.7) / 单个进程平均内存
# 例:2G可用内存,每进程50MB:max_children = (2048 * 0.7) / 50 ≈ 28
Node.js内存泄漏排查
Node.js内存泄漏通常由以下原因引起:
- 全局变量持有对大对象的引用
- 事件监听器没有正确移除
- 缓存无限增长(没有大小限制的Map/Set)
- 闭包中的意外引用
使用heapdump生成内存快照:
// 安装heapdump
npm install heapdump
// 在代码中添加
const heapdump = require('heapdump');
// 当内存超过阈值时自动生成快照
const v8 = require('v8');
setInterval(() => {
const stats = v8.getHeapStatistics();
const usedPercent = stats.used_heap_size / stats.heap_size_limit;
if (usedPercent > 0.8) {
const filename = `/tmp/heapdump-${Date.now()}.heapsnapshot`;
heapdump.writeSnapshot(filename);
console.log(`内存使用超过80%,已生成快照:${filename}`);
}
}, 30000); // 每30秒检查一次
将生成的 .heapsnapshot 文件下载到本地,用Chrome DevTools Memory面板分析,找出占用内存最多的对象类型。
PM2配置内存自动重启(临时缓解方案):
// ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: 'app.js',
max_memory_restart: '400M', // 内存超过400MB自动重启
}]
}
MySQL内存持续增长排查
MySQL在运行期间内存持续增长,通常是以下原因:
# 查看MySQL内存分配详情
mysql -u root -p -e "
SELECT
event_name,
ROUND(current_alloc/1024/1024, 2) AS 'MB',
ROUND(high_alloc/1024/1024, 2) AS '峰值MB'
FROM performance_schema.memory_summary_global_by_event_name
WHERE current_alloc > 1024*1024
ORDER BY current_alloc DESC
LIMIT 20;"
常见原因和解决方案:
| 原因 | 排查方法 | 解决方案 |
|---|---|---|
| innodb_buffer_pool_size过大 | 查看实际命中率 | 按实际需求调整大小 |
| 临时表过多 | 查看Created_tmp_tables | 优化导致大量临时表的查询 |
| 连接数过多,连接缓冲积累 | 检查max_connections | 降低max_connections,使用连接池 |
| 长事务未提交 | SHOW PROCESSLIST | 找出并终止长事务 |
四、临时释放内存的方法
当内存使用率已经很高,需要立即缓解,可以采取以下临时措施:
方法一:释放Linux页面缓存
Linux的buff/cache虽然是可释放的,但系统不会主动释放它(因为保留缓存可以加速后续访问)。可以手动触发释放:
# 先同步文件系统(确保脏数据写入磁盘)
sync
# 释放页面缓存
echo 1 > /proc/sys/vm/drop_caches
# 释放目录项缓存和inode缓存
echo 2 > /proc/sys/vm/drop_caches
# 释放所有(页面缓存+目录项+inode)
echo 3 > /proc/sys/vm/drop_caches
注意: 释放缓存后,短时间内磁盘I/O会增加(因为原来从缓存读的数据现在要从磁盘读),不适合在高峰期执行。
方法二:重启占用内存多的服务