结论: 服务器内存不足要先判断是否"真的"不足——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"。
$ 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 时,才是真的内存压力。
真正需要区分的两个概念:
| 概念 | 全称 | 存的内容 | 能否被回收 | 典型释放方式 |
|---|---|---|---|---|
| Buffer | Buffer Cache | 块设备的裸块数据、文件元数据(inode、目录项) | 可以 | 内存压力下自动回写释放 |
| Cache | Page Cache | 文件页内容,读写文件时缓存的文件数据 | 可以 | drop_caches 或自动回收 |
| 匿名页 | Anonymous Pages | 进程堆、栈、malloc 分配的内存 | 不可以(除非有 swap 换出) | 只能靠 kill 进程或 swap |
一句话概括:Buffer 是对磁盘块(元数据)的缓存,Cache 是对文件内容(数据)的缓存,两者都是为了少读盘,都属于可回收内存。真正"吃掉"内存不可让出的是进程的匿名页。
第一步:确认内存水位与趋势
结论先行:用 free -m、/proc/meminfo、vmstat 三个命令交叉验证,既看当前值也看换页趋势。
# 人类可读格式 + 每秒刷新
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 文件。
# 按内存占用倒序,关注 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 数十 MB | worker_connections、proxy_buffers | 大并发时才增长 |
| PHP-FPM | 每进程 30~80MB | pm.max_children | max_children × 单进程内存 不应超过可用内存 |
第三步:读懂 OOM Killer 日志
结论先行:当内存彻底耗尽时,内核的 OOM Killer(Out-Of-Memory Killer)会按 oom_score 挑一个进程杀掉。这不一定是占用最大的进程,而是综合了内存占用、运行时长、优先级后的最高分者——所以 MySQL 常常先于泄漏的那个进程被杀。
查日志的命令:
# 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一段典型日志的读法:
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 容器内触发的,即使宿主机还有余量也一样会被杀——这是容器内存限制没配好的典型信号。
保护关键进程不被优先杀(临时手段):
# 查看当前 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 过高)会让系统在还有余量时就先换出,导致性能抖动。
# 创建一个 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.swappiness | 60 | 数据库/Redis 机器设 1~10,通用机器 10~30 | 值越高越倾向换出匿名页;设 0 表示尽量不用(Linux 5.x 后 0 不代表完全禁用) |
vm.vfs_cache_pressure | 100 | 50~100 | 低于 100 表示更倾向保留 dentry/inode 缓存 |
vm.min_free_kbytes | 自动计算 | 不超过总量的 5% | 保留给内核的关键分配,设置过大反而容易 OOM |
vm.overcommit_memory | 0 | 数据库建议 2 | 2 表示禁止过量分配,配合 overcommit_ratio 使用 |
生效方式:
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.swappinessMySQL 官方建议把 swappiness 设为 1,因为 InnoDB 的 Buffer Pool 一旦被换出到磁盘,查询延迟会从微秒级掉到毫秒级,表现为"数据库突然变慢"。
应急手段与长期优化
结论先行:临时止血用释放缓存、限制进程、扩容 swap;长期解决必须把上限管起来——给容器设 memory limit、给 JVM 设 Xmx、给 Redis 设 maxmemory,并且留 20% 给操作系统和文件缓存。
短期应急(注意是止血不是治疗):
# 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 官方都建议关闭它以降低内存碎片与延迟抖动:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化:在 /etc/rc.local 或 systemd unit 里执行常见误区 / 排错提示
- 看到
free里 used 占了 90% 就慌:这是把可回收的 cache 当成了占用。判断标准是available列和 swap 是否被使用,绝大多数"内存告警"其实是误报。 - 直接
echo 3 > /proc/sys/vm/drop_caches常规定期执行:清缓存会让后续所有文件读取重新落盘,造成 IO 尖峰与业务抖动。它只是临时止血手段,生产环境不应做成定时任务。 - 完全关闭 swap:没有 swap 时内核在内存紧张时没有任何缓冲,只能直接 OOM Kill,反而更危险。正确做法是保留少量 swap(2~4GB)并把 swappiness 调低。
- 在容器里看
free判断可用内存:容器内free常常显示宿主机的全部内存,完全不准。应以 cgroup 的memory.current/memory.max为准,或docker stats。 - 给 JVM 设 Xmx 时按宿主机内存算:容器里 JVM 可能不识别 cgroup 限制(JDK 8u191 之前),导致 Xmx 超过容器限额而被 OOM Kill。应使用 JDK 11+ 并加
-XX:+UseContainerSupport -XX:MaxRAMPercentage=70。 - 只加内存不改配置:MySQL 的 buffer pool、Redis 的 maxmemory 都是按比例或固定值配置的,加了物理内存而不调参数,性能完全不会提升,只是让缓存可以更大而已。
企业QQ咨询




