服务器内存不足怎么办

结论: 服务器内存不足要先判断是否"真的"不足——Linux 会用空闲内存做磁盘缓存(cache),free -m 里 available 才是真正可用的量,只有它低于总量 10% 且 swap 开始被使用时才算真紧张。定位到进程用 ps aux --sort=-%mem 与 smem,Java 看堆配置、MySQL 看 buffer pool、 Nginx 看 worker 连接数;若已被系统杀进程则在 /var/log/messages 或 journalctl -k 找 "Out of memory: Kill process";应急可调低 vm.swappiness 或临时清 drop_caches,长期方案是限容器/JVM 内存、加内存、或迁出高耗服务。

为什么"内存快满了"常常是误会

Linux 有一条被反复验证的设计哲学:空闲的内存是浪费的内存。系统会把暂时用不到的物理内存拿去做页缓存(Page Cache)和缓冲区(Buffer),用于加速文件读写。所以你在一台健康的服务器上执行 free -m,看到已用 15GB / 总共 16GB,是完全正常的——那 10GB 的 cache 在系统需要时会被立刻回收给应用程序。

这就是为什么判断内存是否真的不足,标准流程的第一句话永远是"看 available 而不是看 used"。

bash
$ free -m
               total        used        free      shared  buff/cache   available
Mem:           15918        9821        1024          96        5072        5630
Swap:           2047           0        2047

上例中 used 看起来高达 9821MB(含 5072MB 缓存),但真正能被新进程使用的是 available 列的 5630MB,说明这台机器还有余量。只有当 available 很低(低于总量 10%)且 swap 的 used 开始增长、同时出现 si/so(换入换出)不为 0 时,才是真的内存压力。

真正需要区分的两个概念:

概念全称存的内容能否被回收典型释放方式
BufferBuffer Cache块设备的裸块数据、文件元数据(inode、目录项)可以内存压力下自动回写释放
CachePage Cache文件页内容,读写文件时缓存的文件数据可以drop_caches 或自动回收
匿名页Anonymous Pages进程堆、栈、malloc 分配的内存不可以(除非有 swap 换出)只能靠 kill 进程或 swap

一句话概括:Buffer 是对磁盘块(元数据)的缓存,Cache 是对文件内容(数据)的缓存,两者都是为了少读盘,都属于可回收内存。真正"吃掉"内存不可让出的是进程的匿名页。

第一步:确认内存水位与趋势

结论先行:用 free -m、/proc/meminfo、vmstat 三个命令交叉验证,既看当前值也看换页趋势。

bash
# 人类可读格式 + 每秒刷新
free -m
free -h
watch -n 1 free -m

# 详细内存信息(sar/vmstat/monitor 工具的数据源)
cat /proc/meminfo | grep -E 'MemTotal|MemAvailable|MemFree|Buffers|Cached|SReclaimable|Slab|SwapTotal|SwapFree'

# 换页趋势,关键在 si/so 两列是否持续非 0
vmstat 1 5
# procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
#  r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
#  2  1  10240 120000   1200 3000000   0    0     0   120  800 1200 15  8 70  7  0

# 查看谁在用共享内存
ipcs -m

关键字段速查:

  • MemAvailable:内核估算的、不需要换出就能分配给新进程的内存,最重要的单一指标。
  • Slab / SReclaimable:内核对象缓存。若 Slab 高达数 GB 且 SReclaimable 很小,可能是 dentry/inode 缓存泄漏,常见于海量小文件场景。
  • si / so(swap in / swap out):一旦长期非 0,说明物理内存已经不够,系统正在用磁盘换血,性能会出现数量级下降。

第二步:找出吃内存的进程

结论先行:进程级定位用 ps 排序,进程内部结构看 pmap 与 smem,容器场景用 docker stats 与 cgroup 文件。

bash
# 按内存占用倒序,关注 RSS 列
ps aux --sort=-%mem | head -15
ps -eo pid,ppid,user,%mem,rss,vsz,comm --sort=-rss | head -15

# RSS 单位转换到 MB 显示
ps -eo pid,user,comm,rss --sort=-rss | awk '{$4=$4/1024"MB"; print}' | head -15

# 查看某进程的内存映射详情
pmap -x 12345 | tail -5       # 最后一行是总量
pmap -XX 12345 | sort -k2 -rn | head -10

# smem 能给出更公平的 PSS/USS(考虑共享库分摊)
sudo apt install -y smem
smem -t -k -p | head -20

# Docker 容器占用
docker stats --no-stream
cat /sys/fs/cgroup/memory.max      # cgroup v2
cat /sys/fs/cgroup/memory/memory.limit_in_bytes   # cgroup v1

三个必须理解的口径差异,否则会误判:

  • VSZ(虚拟内存):包含 mmap 映射、共享库、已申请未使用的空间,不代表真实物理占用,Java 进程 VSZ 常常是 RSS 的几倍,属正常。
  • RSS(常驻内存):进程实际占用的物理页,含共享库部分。多进程时共享库会被重复计算。
  • PSS(比例分摊内存):把共享页按使用进程数分摊,是多进程场景最公平的算法,smem 的价值就在这。

常见高耗服务的定位要点:

服务正常基线主要内存参数调整方向
MySQL 8.4物理内存的 50%~70%innodb_buffer_pool_size通常设为可用内存的 60%,多实例要留余量
Redis 7由数据集决定maxmemory必须显式设置(如物理内存的 70%),否则会吃满
Java 应用由 -Xms/-Xmx 决定-Xmx、-XX:MaxMetaspaceSize容器环境务必设 -XX:MaxRAMPercentage=70
Nginx极少,每 worker 数十 MBworker_connections、proxy_buffers大并发时才增长
PHP-FPM每进程 30~80MBpm.max_childrenmax_children × 单进程内存 不应超过可用内存

第三步:读懂 OOM Killer 日志

结论先行:当内存彻底耗尽时,内核的 OOM Killer(Out-Of-Memory Killer)会按 oom_score 挑一个进程杀掉。这不一定是占用最大的进程,而是综合了内存占用、运行时长、优先级后的最高分者——所以 MySQL 常常先于泄漏的那个进程被杀。

查日志的命令:

bash
# systemd 系统(推荐)
journalctl -k --since "2 hours ago" | grep -i -A 20 'out of memory'

# 传统 syslog
grep -i -A 20 'out of memory' /var/log/messages
grep -i 'killed process' /var/log/syslog

# 最近一次 OOM 的完整现场
journalctl -k | grep -iE 'oom|killed process' | tail -40

一段典型日志的读法:

text
Out of memory: Killed process 28451 (mysqld) total-vm:8388608kB, anon-rss:6291456kB, file-rss:0kB, shmem-rss:0kB
oom-kill: constraint=CONSTRAINT_MEMCG, task=mysqld, pid=28451
Memory cgroup out of memory: Killed process 28451 (mysqld)

字段含义:total-vm 是虚拟内存,anon-rss 是匿名页占用(真实的罪魁),shmem-rss 是共享内存。constraint=CONSTRAINT_MEMCG 说明这次 OOM 是在 cgroup 容器内触发的,即使宿主机还有余量也一样会被杀——这是容器内存限制没配好的典型信号。

保护关键进程不被优先杀(临时手段):

bash
# 查看当前 oom_score(-1000 表示永不被杀)
cat /proc/12345/oom_score
cat /proc/12345/oom_score_adj

# 保护 mysqld 不被 OOM Killer 优先选中
echo -1000 > /proc/$(pgrep mysqld)/oom_score_adj

# systemd 服务里持久化
# [Service]
# OOMScoreAdjust=-1000

如果允许服务在 OOM 后重启,配合 systemd 的 Restart=on-failure 能减少业务中断时间,但要同时配置告警,否则会陷入"杀掉重启、再杀掉"的循环而无人知晓。

swap 配置与 swappiness 调优

结论先行:swap 不是"内存不够的替代品",而是"给内核留出回收余地的缓冲"。完全禁用 swap 会让内核在内存紧张时直接 OOM 杀进程,反而不安全;但过度依赖 swap(swappiness 过高)会让系统在还有余量时就先换出,导致性能抖动。

bash
# 创建一个 4GB 的 swap 文件
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

# 开机自动挂载
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

# 验证
swapon --show
free -h

参数说明:

参数默认值推荐值说明
vm.swappiness60数据库/Redis 机器设 1~10,通用机器 10~30值越高越倾向换出匿名页;设 0 表示尽量不用(Linux 5.x 后 0 不代表完全禁用)
vm.vfs_cache_pressure10050~100低于 100 表示更倾向保留 dentry/inode 缓存
vm.min_free_kbytes自动计算不超过总量的 5%保留给内核的关键分配,设置过大反而容易 OOM
vm.overcommit_memory0数据库建议 22 表示禁止过量分配,配合 overcommit_ratio 使用

生效方式:

bash
sudo tee -a /etc/sysctl.d/99-memory.conf > /dev/null <<'EOF'
vm.swappiness = 10
vm.vfs_cache_pressure = 60
EOF
sudo sysctl -p /etc/sysctl.d/99-memory.conf
sysctl vm.swappiness

MySQL 官方建议把 swappiness 设为 1,因为 InnoDB 的 Buffer Pool 一旦被换出到磁盘,查询延迟会从微秒级掉到毫秒级,表现为"数据库突然变慢"。

应急手段与长期优化

结论先行:临时止血用释放缓存、限制进程、扩容 swap;长期解决必须把上限管起来——给容器设 memory limit、给 JVM 设 Xmx、给 Redis 设 maxmemory,并且留 20% 给操作系统和文件缓存。

短期应急(注意是止血不是治疗):

bash
# 1) 释放可回收的页缓存(先同步确保脏页落盘)
sync
echo 1 > /proc/sys/vm/drop_caches     # 仅 page cache
# echo 2  -> dentries/inodes
# echo 3  -> page cache + dentries + inodes

# 2) 清理 slab 中的 dentry/inode 缓存
echo 2 > /proc/sys/vm/drop_caches

# 3) 清理僵尸般的陈旧共享内存段
ipcs -m | awk '$2 ~ /^0x/ {print $2}' | xargs -r -n1 ipcrm -m

# 4) 临时关闭非核心服务腾内存
systemctl stop elasticsearch

长期治理的三件事:

  • 设上限:Docker 用 --memory=2g --memory-swap=2g;K8s 在 Pod 里配 resources.limits.memory;Java 用 -Xmx 或 -XX:MaxRAMPercentage=70.0(容器内比 Xmx 更省心);Redis 用 maxmemory 4gb 加 maxmemory-policy allkeys-lru。
  • 控并发:PHP-FPM 的 pm.max_children、Nginx 的 worker_connections、Tomcat 的 maxThreads 都与内存线性相关,按 可用内存 / 单请求内存 计算,别照抄网上的默认值。
  • 加监控:用 Prometheus 的 node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.15 作为告警规则,提前一周收到通知,比半夜被 OOM 叫醒强得多。

内存还涉及到 THP(Transparent Huge Pages),Redis 和 MongoDB 官方都建议关闭它以降低内存碎片与延迟抖动:

bash
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化:在 /etc/rc.local 或 systemd unit 里执行

常见误区 / 排错提示

  1. 看到 free 里 used 占了 90% 就慌:这是把可回收的 cache 当成了占用。判断标准是 available 列和 swap 是否被使用,绝大多数"内存告警"其实是误报。
  2. 直接 echo 3 > /proc/sys/vm/drop_caches 常规定期执行:清缓存会让后续所有文件读取重新落盘,造成 IO 尖峰与业务抖动。它只是临时止血手段,生产环境不应做成定时任务。
  3. 完全关闭 swap:没有 swap 时内核在内存紧张时没有任何缓冲,只能直接 OOM Kill,反而更危险。正确做法是保留少量 swap(2~4GB)并把 swappiness 调低。
  4. 在容器里看 free 判断可用内存:容器内 free 常常显示宿主机的全部内存,完全不准。应以 cgroup 的 memory.current / memory.max 为准,或 docker stats。
  5. 给 JVM 设 Xmx 时按宿主机内存算:容器里 JVM 可能不识别 cgroup 限制(JDK 8u191 之前),导致 Xmx 超过容器限额而被 OOM Kill。应使用 JDK 11+ 并加 -XX:+UseContainerSupport -XX:MaxRAMPercentage=70。
  6. 只加内存不改配置:MySQL 的 buffer pool、Redis 的 maxmemory 都是按比例或固定值配置的,加了物理内存而不调参数,性能完全不会提升,只是让缓存可以更大而已。