服务器内存使用率很高正常吗?Linux缓存与真实内存占用分析

几乎所有刚接触 Linux 运维的人都会被同一个问题困扰:监控图上内存使用率长期 90% 以上,free 一看 used 占了 15G、free 只剩几百 MB,是不是要加内存了?

大多数情况下答案是:不用加,这是正常的。 Linux 会主动把空闲内存拿去做文件缓存,而且这部分缓存在应用需要内存时会被自动回收。但"大多数"不等于"全部"——真正内存不足时表现完全不同,需要认得出来,否则 OOM Kill 掉你的数据库进程时就来不及了。

这篇文章讲清楚 Linux 的内存账怎么算:free 每个字段的确切含义、/proc/meminfo 里该看哪几项、Page Cache 与 Buffer 的区别、怎样判断是真的不够还是只是缓存、swapiness 该不该调、OOM Killer 怎么触发以及日志怎么读、以及怎么找到真正的内存大户。

一、free -m 的每个字段到底什么意思

$ free -m
               total        used        free      shared  buff/cache   available
Mem:           15948        9284         412         156        6252        6180
Swap:           2047         128        1919

先说最容易踩的坑:不要简单地把 used 当"已占用内存",也不要把 free 当"可用内存"。 这两个都是老口径的字段,会给出误导性结论。

逐列解释:

字段 含义 备注
total 物理内存总量 略小于标称值,部分被内核和设备占用
used total - free - buff/cache 真正被程序占用且难以回收的部分
free 完全没被使用的内存 这个值很小是正常的,不是问题
shared 主要是 tmpfs(含 /dev/shm)占用的部分 会被计入 buff/cache 相关计算
buff/cache Buffer + Page Cache 之和 可被回收,内存紧张时内核会释放
available 估算的、不经过 swap 就能分配给新进程的内存量 判断内存是否够用看这里

上面的例子该怎么读:总共 16G,buff/cache 有 6.2G,说明系统用空闲内存缓存了 6.2G 的文件数据,这部分随时可以被回收。available 有 6.1G,意味着现在还能再分配 6GB 给新进程而不触发 swap。这台机器内存非常充足,尽管 used 看着有 9.2G。

反过来也是同一套逻辑:如果某台机器 used 只有 2G,但 buff/cache 几乎没有、free 很大,说明这台机器其实没有被充分利用——文件读请求都打到磁盘上了。看到内存占用低先别高兴,可能只是缓存没建立起来(比如刚启动、或者刚 drop_caches 过)。

需要注意的是,free 各版本的输出有过变化。老版本(procps-ng 3.3.9 之前)的 used 会把 buff/cache 也算进去,那一列的含义完全不同。确认版本:

free --version

另外 -m 是 MB、-g 是 GB、-h 人类可读,加 -w 可以把 buff 和 cache 拆开显示,加 -s 1 可以每秒刷新。

free -wh 1        # 拆开 buff/cache,每秒刷新一次
free -wt          # 多显示一行 Total

二、/proc/meminfo:真正的权威数据源

free 只是读取 /proc/meminfo 后做了加工,直接看原始数据能得到更多细节:

cat /proc/meminfo

关键字段整理:

字段 含义 用途
MemTotal 总物理内存 分母
MemFree 完全空闲 参考价值低
MemAvailable 估算可用内存 最重要的单一指标
Buffers 块设备元数据缓存 一般不大
Cached 文件页缓存(Page Cache) 可被回收
SwapCached 已换出又被换回但仍留在 swap 的页 —
Active / Inactive 活跃/非活跃页(含文件页与匿名页) Inactive 更容易被回收
Active(anon) / Inactive(anon) 匿名页(进程数据)的活跃度 匿名页回收需要 swap
Active(file) / Inactive(file) 文件页的活跃度 文件页可直接丢弃或回写
Slab 内核对象缓存 包含 dentries 与 inodes
SReclaimable 可回收的 Slab 主要是 dentry/inode 缓存
SUnreclaim 不可回收的 Slab 持续增长可能是内核泄漏
Dirty 待回写到磁盘的脏页 太大说明回写压力大
Writeback 正在回写的页 —
AnonPages 匿名页总量 进程实际占用的重要参考
Mapped mmap 映射的文件页 —
Shmem shared memory + tmpfs 会被算进 used
HugePages_* 大页相关 数据库场景关注

一个实用的判断口径:真实内存占用 ≈ MemTotal - MemFree - Cached - SReclaimable - Buffers。 这个近似比 free 的 used 更接近事实,因为它把可回收的内核对象也扣掉了。

MemAvailable 的估算算法值得一提:它不是简单的相加,内核会根据当前的内存压力,估算"在最坏情况下能从 Page Cache 和 Slab 里回收多少",同时还考虑了"低于某个水位就会开始回收"的约束。所以它比 MemFree + Cached 这种粗略相加更准,这也是为什么推荐直接看它。

查看 Slab 明细(排查内核对象增长时用):

sudo slabtop -o                      # 交互式按占用排序
sudo cat /proc/slabinfo | sort -k3 -n -r | head -20

三、Buffer 与 Page Cache 的区别

这两个经常被人混为一谈,其实管的层次不同:

对比项 Buffer Page Cache
缓存对象 块设备本身的元数据与裸块(superblock、inode 表、目录块) 文件的内容(文件数据页)
层次 块层 / 文件系统元数据 文件层
典型大小 通常几十 MB 到几百 MB 可以占几十 GB
现代 Linux 占比很小 是 buff/cache 的主体
回收方式 可丢弃(需先回写元数据) 干净页直接丢弃,脏页回写后丢弃

历史上(Linux 2.4 之前)两者是分离的缓存结构,现在内核把它们统一到页缓存框架下,Buffers 主要就是文件系统元数据那部分。日常说"文件缓存"指的基本都是 Page Cache。

为什么要用内存做文件缓存? 因为磁盘(尤其是机械盘)比内存慢几个数量级。同一个文件被反复读 1000 次(比如网站上的静态资源、数据库的索引文件),第一次从磁盘读,之后全部命中内存。这部分内存如果被"空着",才是真正的浪费。所以 Linux 的设计哲学是:空闲内存就是浪费的内存。

代价是这部分内存必须在有别的进程需要时被立刻让出来。内核通过 LRU(更准确说是双 LRU 链表:Active/Inactive)来判断哪些页是可以优先回收的,并且会设定一个低水位(low watermark)——当可用内存逼近这个水位时,后台回收线程 kswapd 就开始工作,这一切都是自动的,不需要人工介入。

需要强调:不要通过 /proc/sys/vm/drop_caches 去"释放"缓存来解决所谓的内存高占用。

# 非必要不要执行;仅在测试 IO 性能、验证缓存效果时才用
sync
echo 3 > /proc/sys/vm/drop_caches

这样做的唯一结果是让系统性能变差——所有后续的文件读都要重新走磁盘。它唯一的合法用途是在做磁盘基准测试前排除缓存干扰,或者在极少数诊断场景验证某块内存是不是缓存。日常运维中执行它没有任何正向收益。

四、什么时候"内存占用高"是真的异常

下面是六个可靠的异常信号,出现任意一条才需要处理:

信号一:available 长期很低。 比如低于总内存的 5%,或者低于几百 MB。这说明即使回收了所有缓存,也没多少余量了。

信号二:swap 使用量持续增长。 free -m 的 swap 行 used 不为 0 本身不一定是灾难(内核会把很少使用的匿名页换出去腾空间),但持续增长且不回落就是危险信号,说明系统必须在磁盘上"赊账"才能维持运行。

信号三:vmstat 的 si/so 长期不为 0。 这是最关键的判断依据。

$ vmstat 1
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  262144 184320  10234 1240332  512    0  1024   320 4120 3210 12  4 80  4  0

si(swap in,从磁盘换入)和 so(swap out,换出到磁盘)单位是 KB/s。只要这两个值持续不等于 0,系统就在换页,性能已经在受损——每次换页意味着一次磁盘访问,延迟比内存访问高几个数量级。偶发的一次管理是正常的,长期不为 0 必须处理。

配套看 sar -B 能看到缺页率的统计:

sar -B 1 5
# pgpgin/pgpgout 换入换出 KB/s,fault/majflt 缺页与**主要缺页**(需访问磁盘)
# majflt 高说明进程在频繁访问已经被换出的页,是典型的内存不足

信号四:dmesg 里出现 OOM Killer。 详见下一节。

信号五:Slab(特别是 SUnreclaim)持续增长不回落。 这通常是内核态的对象泄漏,常见于某些驱动、或者文件系统 bug(比如大量创建删除小文件导致 dentry 暴涨)。用 slabtop 找出具体是哪类对象。

信号六:单个进程的 RSS 单调增长且不回落。 这是应用层内存泄漏的判断依据,需要连续采样确认。

# 每分钟记录一次目标进程的 RSS
while true; do
  date '+%F %T'
  grep VmRSS /proc/PID/status
  sleep 60
done

# 或者用一行,连续采样 60 次
for i in $(seq 60); do date '+%T'; grep VmRSS /proc/PID/status; sleep 60; done

内存泄漏的判断要谨慎:RSS 涨到某个值之后稳定在高位是正常的(比如 JVM 堆扩容到 Xmx 就不再增长,缓存预热到满也属正常)。真正的泄漏是"锯齿上升"或者"阶梯式上升且不回落"——每次 Full GC 后应该能降下来一部分,如果 Full GC 后的基线也在持续抬高,那就是泄漏。

五、swapiness 要不要调

vm.swappiness 的取值范围是 0~100(部分新内核支持到 200),控制内核在回收内存时"更倾向于换出匿名页"还是"更倾向于丢弃文件页"。

  • 值越高,越倾向把匿名页(进程数据)换到磁盘。
  • 值越低,越倾向先丢弃文件页缓存,实在不行才 swap。
  • 设为 0 并不等于关闭 swap,它只表示"除非为了避免 OOM,否则不 swap"(内核 3.5 之后的语义;更老的内核对此处理不同)。

默认 60 对通用场景是合理的。建议调整的场景:

场景 建议值 理由
数据库(MySQL/PostgreSQL/Redis) 1~10 数据库的数据页多数是匿名页,被换出去后延迟灾难性上升
Java 应用 1~10 JVM 堆是匿名页,被 swap 会导致 GC 时间暴涨
文件服务器、大量顺序读的应用 60~100 文件页缓存比匿名页更该被优先丢弃...反之应保留默认让内核自行权衡
桌面/通用服务器 60(默认) 保持默认

修改方式(持久化到 /etc/sysctl.d/,避免重启失效):

# 查看当前值
cat /proc/sys/vm/swappiness

# 临时生效
sudo sysctl -w vm.swappiness=10

# 持久化(推荐独立文件,便于回滚)
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-app-tuning.conf
sudo sysctl --system                 # 重新加载全部 sysctl 配置

需要把争议讲清楚: 调低 swappiness 不会让系统变快,它只是改变了回收的优先级。把 swappiness 调到 1 之后,内存真的耗尽时内核无路可退,会更早地触发 OOM Killer 直接杀进程——这对数据库来说可能比慢一点更糟(进程被杀 = 服务中断 + 崩溃恢复)。所以配套要做的是:给数据库留出足够的内存余量、配置合理的 OOM 优先级调整,而不是只调这一个参数就以为万事大吉。

同时建议配置的最小空闲水位 vm.min_free_kbytes,它决定内核保留多少内存以备紧急分配。默认是自动计算的值,不建议手动调大到离谱的数值(比如总内存的 10%),那会限制文件系统缓存可用的空间。只有在收到 "page allocation failure" 日志时才考虑适度调高。

真正不需要 swap 的场景(比如明确不希望 IO 抖动的大型数据库集群),可以直接关闭 swap:

sudo swapoff -a                      # 临时关闭(会先做页迁移,慢且需要足够的空闲内存)
# 永久关闭:注释 /etc/fstab 里的 swap 行
sudo sed -i '/swap/s/^/#/' /etc/fstab

风险提示:关闭 swap 之后,内存耗尽时内核没有任何缓冲,会直接触发 OOM 杀进程。只有在监控完善、内存余量充足的前提下才这么做,并且要先把服务的 OOM 优先级调整好。

六、OOM Killer:为什么会杀进程,日志怎么读

当系统无法满足一次内存分配请求时,Linux 内核会调用 OOM Killer,选择一个进程杀掉来释放内存。这是最后的手段,也是很多人第一次遇到数据库突然消失的原因。

触发条件是"分配失败",而不是简单的"内存用完了"。常见触发场景:

  • 真的耗尽了:所有可回收的内存都回收完了,匿名页也换不出去(比如没有 swap)。
  • 内存碎片:物理上还有内存,但连续大页分配不出来。
  • cgroup 限制:容器或进程组的 memory limit 到了,在这个 group 内部触发 OOM。
  • 内核预留内存耗尽:vm.min_free_kbytes 那一块被突破。

这是最关键的一点:OOM 可能发生在一个 "看起来内存还很多" 的系统上,特别是 memory cgroup 的场景。容器里 free 看到宿主机有 30G 空闲,但 Pod 的 limit 是 2G,超限就会被杀。

查看是否被 OOM 杀过:

# RHEL、Rocky、CentOS(systemd 系统)
sudo dmesg -T | grep -i -A 20 'out of memory'
sudo journalctl -k | grep -i 'oom\|killed process'

# Debian、Ubuntu
sudo dmesg -T | grep -i -B 5 -A 20 'killed process'
sudo grep -i 'killed process' /var/log/kern.log

# systemd 管理的服务被杀,也可以看单元日志
sudo journalctl -u 服务名 | tail -50

一段典型的内核日志怎么读:

[timestamp] Memory cgroup out of memory: Killed process 31245 (mysqld) total-vm:8388608kB, anon-rss:3906MB, file-rss:0kB, shmem-rss:0kB, oom_score_adj:0
片段 含义
Memory cgroup out of memory 在 cgroup 里触发,不是整机 OOM
Killed process 31245 (mysqld) 被杀的 PID 和进程名
total-vm:8388608kB 虚拟地址空间 8GB(参考价值低)
anon-rss:3906MB 实际占用的物理内存(匿名页)约 3.9GB,这是核心数字
file-rss:0kB 映射的文件页
oom_score_adj OOM 倾向性调整值

日志里还会有每个进程的 oom_score 排名表,内核选择的是 score 最高的那个。score 由进程占用内存的百分比加上 oom_score_adj 决定。

保护重要进程:调整 oom_score_adj,取值范围 -1000(完全不被选中)到 1000(优先被杀),默认值 0。

# systemd 方式(推荐,会持久化)
sudo systemctl edit mysqld
# 加入:
# [Service]
# OOMScoreAdjust=-900

# 或者直接写到已有 service 文件目录
sudo mkdir -p /etc/systemd/system/mysqld.service.d/
printf '[Service]\nOOMScoreAdjust=-900\n' | sudo tee /etc/systemd/system/mysqld.service.d/oom.conf
sudo systemctl daemon-reload
sudo systemctl restart mysqld        # 注意:重启会中断服务,请在低峰期操作

# 非 systemd 管理的进程
echo -900 > /proc/PID/oom_score_adj

给数据库、消息队列这类核心服务设置负值,同时给可以被牺牲的批处理任务设置正值(比如 500),这样系统内存耗尽时牺牲的会是次要进程。

七、找出真正的内存大户

确认确实是某几个进程在吃内存之后,用这几种方式定位:

# 按 RSS 排序(最常用)
ps -eo pid,ppid,user,%mem,rss,comm,args --sort=-rss | head -20
# rss 单位是 KB,除以 1024 得 MB

# 更可读的格式
ps -eo pid,user,rss,args --sort=-rss | awk 'NR==1{print "PID USER RSS(MB) CMD"; next} {printf "%s %s %.0f %s\n",$1,$2,$3/1024,$4}'

# 查看单个进程的详细内存构成
cat /proc/PID/status | grep -E 'VmSize|VmRSS|VmData|VmStk|VmExe|VmLib|VmSwap|VmPin|Threads'

/proc/PID/status 里几个字段的含义很不一样,别只看 VmSize:

字段 含义 参考价值
VmSize 虚拟地址空间总量 参考价值最低,Java 进程常虚高几十 GB
VmRSS 常驻物理内存 核心指标,真实占用
VmData 数据段 堆分配相关
VmLib 加载的动态库 —
VmSwap 该进程被换出的大小 非 0 说明已被 swap
VmPTE 页表占用 内存很大时页表本身也可观

其他有用工具:

# pmap:看进程的内存映射构成,找大块是谁
sudo pmap -x PID | tail -5                        # 末尾是汇总
sudo pmap -x PID | sort -k3 -n -r | head -20      # 按大小排序找出最大映射
# 大量 64MB 的 anon 块通常是 glibc arena 或 Java DirectBuffer

# smem:能算 PSS(按比例分摊共享内存),比 RSS 更公平地反映多进程共享库的情况
sudo smem -s pss -r | head -20                    # -s 排序字段 -r 倒序
sudo smem -u -s pss -r | head                     # 按用户聚合
sudo smem -P nginx -s pss -r                      # 只看匹配进程

# 安装
sudo dnf install -y smem                # RHEL、Rocky、CentOS(需要 EPEL)
sudo apt install -y smem                # Debian、Ubuntu

PSS 与 RSS 的区别值得记住:RSS 对共享内存重复计算(10 个进程共享一个 10MB 的库,每个都记 10MB,总共 100MB),PSS 按进程数平摊(每个记 1MB,总共 10MB)。所以在有大量相同进程(比如 Nginx worker、PHP-FPM)的场景下,PSS 更能反映真实开销,RSS 加总会严重高估。

最后别忘了两个非进程的内存去处:

# tmpfs / /dev/shm 占用(会被计入 shared 与 used)
df -h -t tmpfs

# Page Cache 之外的内核占用
sudo slabtop -o

常见问题

free 显示 used 15G、free 只剩 400M,需要加内存吗?

大概率不需要。先看 available 那一列。Linux 会把空闲内存用作文件缓存,buff/cache 高、available 也高,说明内存充裕且被有效利用了。真正需要关注的是 available 很小、swap 持续增长、vmstat 里 si/so 长期不为 0,此时才考虑加内存。

为什么有的机器内存监控显示 99%,有的显示 70%,明明跑的是一样的服务?

很可能是两款监控计算的口径不同。有的用 (total - available) / total,有的用旧口径 used / total(把缓存算进去了),还有的扣除了 Slab 可回收部分。排查问题时不要跨工具比较百分比,直接看绝对值:MemAvailable 和 AnonPages。

echo 3 > /proc/sys/vm/drop_caches 能释放内存、提升性能吗?

不能。它只会清掉文件缓存,导致后续所有文件读取重新走磁盘,性能反而下降。它唯一的合理用途是在做磁盘基准测试前排除缓存影响。把它写进定时任务定期执行是常见的错误做法,会持续损害系统性能。

为什么没有 swap 的机器反而更容易被 OOM Kill?

因为 swap 提供了缓冲。内核可以先把很少使用的匿名页换出去,为突发分配腾出空间,进程不会被立刻杀掉。完全没有 swap 时,内存一旦耗尽内核只能直接杀进程。这也是为什么很多建议是"保留少量 swap(比如 1~2G)作为缓冲",并把 swappiness 调低,而不是完全关闭。

容器里内存限制设多少合适?

容器内的 RSS 上限应该在压测基础上留余量。经验做法是:观察一周的 RSS 峰值,limit 设在峰值的 1.3~1.5 倍;request 设在平均值附近。同时要注意:Java 容器的堆(Xmx)之外还有 Metaspace、线程栈、DirectBuffer、JIT 代码缓存,这些加起来通常是堆的 30%~50%,不要把 Xmx 设成等于 limit。

内存泄漏怎么确认?

连续采样 RSS 至少几个小时(最好跨越一个完整的业务高峰低谷)。正常的缓存预热曲线是会趋于平缓的;泄漏的表现是在每次 GC 或缓存释放之后基线仍然持续抬高,呈阶梯上升。确认后抓堆转储(jmap -dump 会触发 STW,要在低峰或先摘流量)分析是哪类对象占比最高、引用链从哪来。

总结

  • free -m 里判断内存是否够用,看 available 而不是 used 或 free。used 高、available 也高、buff/cache 大,说明内存被用作了文件缓存,这是 Linux 的正常且期望的行为。"空闲内存就是浪费的内存"是设计哲学,不是缺陷。
  • /proc/meminfo 是权威数据源:看 MemAvailable(可用量)、Active(file)/Inactive(file)(文件页活跃度)、SReclaimable(可回收 Slab)、Dirty(待回写脏页)。真实占用可近似估算为 MemTotal - MemFree - Cached - SReclaimable - Buffers。
  • Buffer 缓存块设备元数据,Page Cache 缓存文件内容,后者才是 buff/cache 的主体(现代内核已统一到页缓存框架)。不要执行 drop_caches 释放缓存,它只会让后续读操作重新打磁盘,没有任何正向收益。
  • 六个真实异常信号:available 长期很低;swap 使用量持续增长;vmstat 的 si/so 长期不为 0(这是最关键的判据);dmesg 出现 OOM Killer;SUnreclaim 持续增长;单进程 RSS 阶梯式上升不回落。
  • swappiness 默认 60 对通用场景合理,数据库和 Java 主机可降到 1~10。但调低它不提升性能,只是让内核优先丢弃文件页,代价是内存真耗尽时会更早触发 OOM 杀进程。关闭 swap 前必须确保监控完善、余量充足,并配合 OOMScoreAdjust 设置。
  • OOM 可以在"看起来内存充足"的情况下发生,尤其是 cgroup limit 触发的场景。日志用 dmesg -T | grep -i 'killed process' 或 journalctl -k 查看,核心数字是 anon-rss。用 systemctl set-property OOMScoreAdjust=-900 保护核心服务,牺牲次要进程。
  • 找内存大户用 ps -eo pid,user,rss,args --sort=-rss,细节看 /proc/PID/status 的 VmRSS(不要用 VmSize,Java 进程虚高)。多进程的场景用 smem 看 PSS,它对共享内存做了平摊,比 RSS 加总更接近真实开销;别忘了检查 tmpfs 和 slabtop 两处非进程占用。