1. 内存都去哪了:一个让运维人半夜惊醒的经典问题
你有没有遇到过这种场景:登录一台Linux服务器,随手敲一个free -h,发现可用物理内存只剩几百兆甚至几十兆,第一反应是“哪个进程在吃内存”,于是ps aux --sort=-%mem、top、htop轮番上阵,结果翻了三遍进程列表,最大的进程也就占了几百兆,加起来离总内存还差得远。再回头看free的输出,used那一栏高得离谱,但进程列表里就是找不到对应的“凶手”。
这不是灵异事件,也不是系统在骗你。这是Linux内存管理机制里最容易被误解的一个角落,几乎每一个刚接触Linux运维的人都会在这里栽一次跟头。我自己第一次遇到这个问题的时候,在一台2GB内存的测试机上折腾了整整一个下午,重启了三次服务,最后才发现问题根本不在进程身上。
这篇文章就是把这个坑彻底讲清楚。我会从free命令的输出到底该怎么读讲起,然后逐一拆解那些“看不见的内存”到底藏在什么地方,再给出完整的排查路径和实操命令。无论你是刚入门的运维新手,还是已经工作几年但一直没深究过内存机制的后端开发,看完之后应该都能建立起一套完整的排查思路。核心关键词就三个:free命令、可用内存、进程占用,围绕它们把Linux内存的账本算明白。
2. 先搞懂free到底在说什么:输出字段逐个拆解
2.1 free命令的默认输出与真实含义
很多人看free只看一个数字,就是free那一列,觉得这个数字小就是内存不够了。这个理解不能说完全错,但确实过于粗糙。先看一个典型的输出:
$ free -h total used free shared buff/cache available Mem: 7.6G 6.8G 156M 388M 668M 356M Swap: 2.0G 1.2G 800M大部分人看到free只有156M,available只有356M,立刻就慌了。但请注意,这里有两个关键列经常被忽略:buff/cache和available。
free这一列表示的是完全没有被使用的物理内存。Linux的设计哲学是“空闲的内存就是浪费的内存”,所以内核会尽可能地把没用到的内存拿去做磁盘缓存(page cache)和缓冲区(buffer)。这些缓存内存随时可以被回收给应用程序使用,所以它们虽然不显示在free里,但实际上是“可用”的。
available这一列才是真正值得关注的数字。它表示的是在不使用Swap的前提下,大概还能有多少内存可以分配给新启动的应用。这个数字是内核估算出来的,包含了可回收的缓存部分。所以判断内存是否真的紧张,应该看available,而不是看free。
2.2 buff/cache为什么能涨到几个G
buff/cache这一列合并了两个概念:buffers和cached。用free -w可以把它们分开显示:
$ free -wh total used free shared buffers cache available Mem: 7.6G 6.8G 156M 388M 120M 548M 356M Swap: 2.0G 1.2G 800Mbuffers是块设备读写时的元数据缓存,通常比较小,几十到几百兆。cache是页缓存,也就是文件内容的缓存。当你读取一个大文件、执行一次数据库全表扫描、或者解压一个压缩包,内核都会把读过的数据缓存在内存里,下次再读就不用走磁盘了。这个缓存会一直增长,直到内存快不够用了才会开始回收。
所以一台运行了很久的服务器,buff/cache涨到总内存的百分之六七十都是正常的。这部分内存不是“被占用了”,而是“被借用了”,需要的时候可以还回来。
2.3 available才是判断内存压力的核心指标
我个人的习惯是,排查内存问题第一步永远看available。如果available还有总内存的20%以上,基本不用太担心。如果available低于10%,那就需要认真排查了。如果available已经接近0并且Swap开始大量使用,那就是真正的内存危机。
这里有一个容易踩的坑:在某些老版本的Linux发行版上,free命令的输出格式不一样,可能没有available这一列。这种情况下可以用/proc/meminfo里的MemAvailable字段来替代:
$ grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached" /proc/meminfo MemTotal: 7985624 kB MemFree: 160324 kB MemAvailable: 365128 kB Buffers: 123456 kB Cached: 561024 kB/proc/meminfo是最权威的数据来源,free命令本质上就是从这里面读数据然后格式化输出的。当你对free的输出有疑问时,直接看/proc/meminfo永远不会错。
3. 进程列表里找不到的“幽灵内存”:六大隐藏去向
3.1 内核态内存:Slab分配器吃掉的份额
进程列表(ps、top)只统计用户态进程占用的内存,内核自己用的内存是不计入进程的RSS(Resident Set Size)的。内核态内存的大头是Slab分配器,它负责为内核对象分配内存,比如文件系统的inode缓存、目录项缓存(dentry)、网络连接跟踪表等。
查看Slab占用:
$ cat /proc/meminfo | grep -i slab Slab: 892456 kB SReclaimable: 756320 kB SUnreclaim: 136136 kBSReclaimable是可回收的Slab内存,SUnreclaim是不可回收的。如果SUnreclaim很大,说明内核中有大量不可回收的对象占着内存,这时候需要进一步排查是哪个子系统的问题:
$ sudo slabtop -o -s cslabtop会按占用大小排序列出所有Slab缓存对象。常见的占用大户包括dentry(目录项缓存)、inode_cache(inode缓存)、ext4_inode_cache等。这些缓存正常情况下是可回收的,但如果系统中有大量小文件频繁创建删除,dentry和inode缓存可能会涨得很快。
3.2 共享内存与tmpfs:挂在内存上的文件系统
tmpfs是一种基于内存的文件系统,它占用的内存在free里会算进shared和buff/cache,但不会体现在任何进程的RSS里。常见的tmpfs挂载点包括/dev/shm、/run、/tmp(某些发行版默认把/tmp挂成tmpfs)。
查看tmpfs占用:
$ df -h -t tmpfs Filesystem Size Used Avail Use% Mounted on tmpfs 3.9G 1.2G 2.7G 31% /dev/shm tmpfs 3.9G 8.0M 3.9G 1% /run tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup如果/dev/shm被某个应用写入了大量数据(比如数据库的共享内存段、某些应用的临时文件),这部分内存就会“消失”在进程列表之外。用lsof可以查看是谁在往/dev/shm里写东西:
$ sudo lsof /dev/shm | head -20另外,System V共享内存段和POSIX共享内存也会占用内存但不计入进程RSS。查看System V共享内存:
$ ipcs -m ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 12345 postgres 600 134217728 8这里的bytes就是共享内存段的大小,nattch是附加到该段的进程数。如果看到很大的段但nattch为0,说明是残留的共享内存段没有清理,可以用ipcrm删掉。
3.3 内核线程与驱动模块:不显示在普通ps里的消耗
普通用户执行ps aux只能看到用户态进程,内核线程(kernel thread)虽然也会显示,但它们的RSS通常是0,因为它们没有用户态地址空间。不过内核线程在运行过程中会通过kmalloc、vmalloc等接口申请内核内存,这些内存同样不会体现在进程列表里。
查看内核线程:
$ ps -ef | grep "\[" | head -20 root 1 0 0 某时 ? 00:00:01 systemd root 2 0 0 某时 ? 00:00:00 [kthreadd] root 3 2 0 某时 ? 00:00:00 [rcu_gp] ...方括号括起来的就是内核线程。它们本身不占多少内存,但它们触发的内核操作(比如网络包处理、文件系统操作)会消耗Slab内存。
3.4 内存碎片与预留:看得见但用不上的内存
Linux内核在启动时会预留一部分内存给特定用途,比如crashkernel(用于kdump崩溃转储)、硬件保留区域等。这些内存在free的total里可能都不显示,或者显示为已使用。
查看预留内存:
$ dmesg | grep -i "reserved" [ 0.000000] Memory: 7923456K/8388608K available (12345K kernel code, 2345K rwdata, 4567K rodata, 1234K init, 5678K bss, 465152K reserved)这里的reserved就是被预留的内存。另外,如果系统开启了crashkernel,会在启动参数里看到类似crashkernel=256M的配置,这部分内存从系统启动开始就被拿走了,永远不会出现在free的可用内存里。
3.5 页面缓存与匿名页的回收优先级差异
Linux内存回收有一个优先级顺序:先回收干净的页面缓存(clean page cache),然后回收脏页面缓存(需要先写回磁盘),再回收可回收的Slab,最后才考虑换出匿名页(进程的堆栈内存)到Swap。
这意味着,当系统内存紧张时,页面缓存会被优先回收。但如果你看到available很低而buff/cache也不高,说明页面缓存已经被回收得差不多了,剩下的都是匿名页和不可回收的内核内存,这时候内存压力就真的很大了。
查看内存回收情况:
$ cat /proc/vmstat | grep -E "pgsteal|pgscan|pswpout|pswpin" pgsteal_kswapd 123456 pgsteal_direct 7890 pgscan_kswapd 234567 pgscan_direct 12345 pswpin 5678 pswpout 9012pgsteal表示回收的页面数,pgscan表示扫描的页面数。如果pgscan远大于pgsteal,说明扫描了很多页面但回收效率低,内存碎片化可能比较严重。pswpin和pswpout表示Swap的换入换出,如果这两个数字在持续增长,说明系统已经在大量使用Swap了。
3.6 容器环境下的内存统计偏差
如果你在容器里执行free,看到的可能是宿主机而不是容器的内存信息。这是因为容器默认没有独立的/proc/meminfo,free读到的还是宿主机的数据。这种情况下,free显示的可用内存很少,但容器里的进程确实没占多少,因为内存是被宿主机上其他容器或宿主进程用掉的。
在容器里判断自己的内存限制,应该看cgroup的相关文件:
$ cat /sys/fs/cgroup/memory/memory.limit_in_bytes $ cat /sys/fs/cgroup/memory/memory.usage_in_bytes $ cat /sys/fs/cgroup/memory/memory.statmemory.limit_in_bytes是容器的内存上限,memory.usage_in_bytes是当前使用量。这两个数字才是容器视角下的真实内存情况。如果memory.usage_in_bytes接近memory.limit_in_bytes,即使free显示宿主机还有很多内存,容器也可能因为达到cgroup限制而触发OOM。
4. 完整排查路径:从free到根因的六步操作
4.1 第一步:确认available和Swap的真实状态
拿到一台内存“异常”的机器,第一步永远是看全局:
$ free -wh $ cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached|Slab|SReclaimable|SUnreclaim|Shmem|KernelStack|PageTables"把MemAvailable和MemTotal做个对比,算出可用比例。同时看Shmem(共享内存)、KernelStack(内核栈)、PageTables(页表)这几个字段。PageTables在内存密集型应用中可能会很大,比如运行了大量虚拟机的宿主机,页表占用几个G都是有可能的。
4.2 第二步:用smem和ps_mem做进程内存的精确统计
ps aux里的RSS列其实并不准确,它会把共享库的内存重复计算到每个进程头上。比如有20个进程都链接了同一个2MB的共享库,ps会把这2MB算20次,导致总和远超实际使用量。
用smem可以看到更准确的PSS(Proportional Set Size):
$ sudo smem -t -k -s pss PID User Command Swap USS PSS RSS 1234 mysql /usr/sbin/mysqld 12.0M 456.0M 478.0M 512.0M 5678 java java -Xmx2g ... 8.0M 890.0M 912.0M 980.0M ... ------------------------------------------------------------------------------- 25 3 users 45.0M 2.1G 2.3G 2.8GUSS是进程独占的内存,PSS是平摊了共享内存之后的内存,RSS是传统的常驻内存。看PSS的总和才是比较接近真实的值。
如果没有smem,可以用ps_mem这个Python脚本:
$ sudo ps_mem -s Private + Shared = RAM used Program 456.0 MiB + 56.0 MiB = 512.0 MiB mysqld 890.0 MiB + 90.0 MiB = 980.0 MiB java ... --------------------------------- 2.8 GiB4.3 第三步:检查Slab和内核内存的占用
$ sudo slabtop -o -s c | head -20 Active / Total Objects (% used) : 1234567 / 1345678 (91.7%) Active / Total Slabs (% used) : 45678 / 45678 (100.0%) Active / Total Caches (% used) : 123 / 145 (84.8%) Active / Total Size (% used) : 789.45M / 892.34M (88.5%) Minimum / Average / Maximum Object : 0.02K / 0.34K / 8.00K OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME 456789 456789 100% 0.19K 21752 21 174016K dentry 345678 345678 100% 0.58K 34567 10 276536K inode_cache ...如果dentry和inode_cache占用很大,说明系统中有大量文件操作。这本身不是问题,因为这些缓存可以回收。但如果SUnreclaim很大,就要看是哪个缓存不可回收:
$ cat /proc/meminfo | grep SUnreclaim SUnreclaim: 136136 kBSUnreclaim通常不会太大,如果超过1GB,可能需要检查是否有内核模块泄漏。
4.4 第四步:排查tmpfs和共享内存
$ df -h -t tmpfs $ ipcs -m $ sudo lsof /dev/shm 2>/dev/null | head -20 $ ls -lh /dev/shm/如果/dev/shm里有大文件,确认是哪个应用创建的。有些应用(比如某些数据库、缓存系统)会默认使用/dev/shm作为临时存储,如果配置不当可能会把内存吃光。
4.5 第五步:检查cgroup限制和容器内存
$ cat /proc/self/cgroup $ cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null $ cat /sys/fs/cgroup/memory/memory.usage_in_bytes 2>/dev/null $ cat /sys/fs/cgroup/memory/memory.stat 2>/dev/null | head -20如果memory.limit_in_bytes是一个有限值(不是9223372036854771712这种超大数),说明在cgroup限制下运行。memory.stat里的rss、cache、shmem等字段可以告诉你内存具体花在哪里了。
4.6 第六步:用/proc/meminfo做最终对账
把所有字段过一遍,做一个简单的加法验证:
$ awk '/MemTotal|MemFree|Buffers|Cached|Slab|Shmem|KernelStack|PageTables|VmallocUsed/ {print}' /proc/meminfo一个粗略的对账公式是:
MemTotal ≈ MemFree + Buffers + Cached + Slab + KernelStack + PageTables + Shmem + 匿名页 + 预留内存
如果左边和右边差距很大,说明有某个字段没有被考虑到。这时候可以对比/proc/meminfo和free的输出,看看是不是free的格式化过程丢失了信息。
5. 常见问题速查与避坑指南
5.1 为什么重启服务后内存没降下来
这是最常见的问题之一。重启了一个占内存的服务,free显示的可用内存却没有明显变化。原因通常是:服务占用的内存中有一部分是共享内存或页面缓存,重启服务只释放了匿名页部分,页面缓存还被内核留着。另外,如果服务使用了System V共享内存段,重启服务不会自动删除这些段,需要用ipcrm手动清理。
排查方法:
$ ipcs -m | grep -v "^key" | awk '$5 == 0 {print $2}' | xargs -I{} ipcrm -m {}这条命令会删除所有nattch为0的共享内存段。执行前确认没有正在运行的服务依赖这些段。
5.2 available还有但系统已经开始OOM
有时候available显示还有几百兆,但系统日志里已经出现了OOM Killer的记录。这是因为available是一个估算值,它假设所有可回收的内存都能被成功回收。但在实际回收过程中,可能会遇到不可移动的页面、内存碎片等问题,导致回收失败。另外,如果OOM Killer触发时系统正在申请一个大的连续内存块(比如大页),即使总可用内存足够,也可能因为碎片问题而分配失败。
查看OOM记录:
$ dmesg | grep -i "out of memory" $ dmesg | grep -i "oom-killer" $ journalctl -k | grep -i oom5.3 内存泄漏的排查思路
如果available在持续下降,且排除了缓存增长的因素,那很可能是内存泄漏。排查步骤:
- 用
smem或ps_mem定期记录各进程的PSS,观察哪个进程的内存在持续增长。 - 对于疑似泄漏的进程,用
pmap -x <pid>查看内存映射详情。 - 如果是Java应用,用
jmap -heap <pid>和jstat -gc <pid>查看堆内存和GC情况。 - 如果是C/C++应用,可以用
valgrind的massif工具做堆分析,或者用gperftools的heap profiler。
一个实用的监控脚本:
#!/bin/bash while true; do date >> /tmp/mem_monitor.log ps -eo pid,comm,rss --sort=-rss | head -10 >> /tmp/mem_monitor.log echo "---" >> /tmp/mem_monitor.log sleep 60 done5.4 速查表:症状与对应排查方向
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| free低但available高 | 页面缓存占用,正常现象 | free -wh看available |
| available低但进程RSS总和不大 | Slab、tmpfs、共享内存 | slabtop、df -t tmpfs、ipcs -m |
| 容器内free显示宿主机内存 | 容器无独立meminfo | 看cgroup的memory.limit_in_bytes |
| 重启服务内存不释放 | 共享内存段残留 | ipcs -m、ipcrm |
| available有但OOM | 内存碎片或大页分配失败 | `dmesg |
| 内存持续下降 | 内存泄漏 | smem定期记录、pmap -x |
5.5 几个容易踩的坑
第一个坑是过度依赖free的free列。很多人看到free列数字小就以为内存不够,实际上应该看available。这个习惯需要刻意纠正。
第二个坑是忽略shared列。shared表示tmpfs和共享内存占用的内存,这部分内存不会出现在任何进程的RSS里,但确实被占用了。如果shared很大,一定要查/dev/shm和ipcs -m。
第三个坑是在容器里用宿主机的排查方法。容器里的free、ps、top看到的都可能是宿主机的数据,必须通过cgroup文件来判断容器自身的资源使用情况。
第四个坑是看到buff/cache大就想手动清理。echo 3 > /proc/sys/vm/drop_caches确实可以清理缓存,但在生产环境上这么做会导致后续的磁盘IO性能下降,因为缓存被清空后所有读操作都要走磁盘。除非是在做性能测试需要可重复的冷启动环境,否则不建议手动清理缓存。
6. 个人实操体会与几个实用技巧
我在实际排查这类问题的过程中,慢慢形成了一套自己的习惯。首先,我从来不会只看free就下结论,一定会同时看/proc/meminfo和slabtop。其次,我会在服务器上常备smem和ps_mem这两个工具,它们比ps aux靠谱得多。再次,对于容器环境,我会把cgroup的内存相关文件做成一个快捷脚本,一键查看容器的真实内存使用。
还有一个很实用的小技巧:用watch命令实时观察内存变化,比反复手动执行命令高效得多。
$ watch -n 2 -d 'free -wh; echo "---"; cat /proc/meminfo | grep -E "Slab|SReclaimable|SUnreclaim|Shmem"'-d参数会高亮显示变化的部分,这样一眼就能看出哪个字段在涨。
另外,如果你怀疑是某个进程在慢慢吃内存,可以用这个一行命令持续记录:
$ while true; do ps -eo pid,comm,rss --sort=-rss | head -5 >> /tmp/top_mem.log; echo "===$(date)===" >> /tmp/top_mem.log; sleep 30; done跑几个小时之后翻日志,哪个进程的RSS在持续增长一目了然。
最后说一个关于available的细节。available的计算考虑了页面缓存中可回收的部分,但它假设系统当前的内存压力不变。如果你突然启动一个需要大量内存的应用,内核回收缓存的速度可能跟不上应用申请内存的速度,这时候即使available看起来还有不少,应用也可能因为申请内存失败而崩溃。所以对于内存敏感的线上服务,available的安全阈值不要设得太低,我个人建议至少保留总内存的15%到20%作为缓冲。
这套排查思路基本上覆盖了我在实际工作中遇到的所有“内存去哪了”的场景。从free的输出开始,沿着Slab、tmpfs、共享内存、cgroup这几条线逐一排查,最后用/proc/meminfo做对账,绝大多数情况下都能找到那个“隐身”的内存消耗者。