首页 / 帮助文档 / Linux服务器内存使用率过高的缓存回收与进程调优

Linux服务器内存使用率过高的缓存回收与进程调优

很多运维人员看到Linux服务器内存使用率飙到95%以上,第一反应是服务器要崩了,急着去重启服务或者加物理内存。但往往忽略了Linux内存管理机制的核心——内存就是用来用的,空闲的内存才是浪费。问题的关键不在于内存被用光,而在于当系统真正需要内存时,能否快速、安全地回收可回收的缓存,以及进程本身是否存在内存泄漏或不合理的占用。

先定位:到底是谁吃掉了内存

动手调优之前,必须搞清楚内存都被谁占用了。free -h是最常用的命令,但很多人只看第一行的total和used,这容易误判。正确的方式是看available这一列,它直接反映了系统还能分配给新进程的内存大小。如果available还很充裕,那即便used接近total,也无需恐慌。

更精细的排查需要用/proc/meminfo文件,这个文件记录了内核层面详细的内存使用情况。重点关注几个字段:Cached表示页缓存和文件系统缓存的大小,这部分内存随时可以被回收;Buffers是块设备I/O缓冲;SReclaimable是内核中可回收的slab内存,包括dentry和inode缓存;SUnreclaim则是不可回收的slab内存。如果SUnreclaim异常偏高,通常意味着内核对象泄漏,需要进一步排查。

# 查看详细内存信息
cat /proc/meminfo | grep -E "^(MemTotal|MemFree|MemAvailable|Buffers|Cached|SReclaimable|SUnreclaim|Shmem):"

另一个容易被忽视的吃内存大户是Shmem,也就是共享内存。它既包括tmpfs文件系统占用的空间,也包括进程间通信使用的共享内存段。很多容器化环境会大量使用tmpfs挂载,这些内存无法被内核主动回收,只能通过删除文件来释放。用df -h查看tmpfs的使用量,用ipcs -m查看共享内存段,往往能发现意想不到的占用。

缓存回收:别盲目drop_caches

网上很多教程会教人执行echo 1 > /proc/sys/vm/drop_caches来手动释放缓存,这在某些紧急情况下确实立竿见影,但把它当成常规手段就是饮鸩止渴。内核的缓存机制是为了加速文件读写,强行清空会导致短期内I/O性能急剧下降,所有原本命中缓存的操作都要重新从磁盘读取。

真正需要关注的是内核自动回收缓存的参数调优。vm.vfs_cache_pressure控制内核回收dentry和inode缓存的倾向性,默认值是100。如果服务器上文件系统操作非常频繁,比如大量小文件的创建和删除,可以适当调高这个值,让内核更积极地回收这部分slab内存。但不要超过200,否则可能导致缓存命中率过低,元数据操作性能下降明显。

# 查看当前值
sysctl vm.vfs_cache_pressure
# 临时调整
sysctl -w vm.vfs_cache_pressure=150
# 永久生效
echo "vm.vfs_cache_pressure=150" >> /etc/sysctl.conf

另一个关键参数是vm.min_free_kbytes,它定义了系统必须保留的最小空闲内存。这个值设得太小,当内存突然紧张时,内核可能来不及回收缓存,导致OOM Killer被触发;设得太大,又会浪费内存资源。一般建议物理内存的0.5%到1%之间,对于128GB内存的服务器,可以设为512MB到1GB。但要注意,这个值不是越大越好,超过5%反而可能导致内存回收过于频繁,影响性能。

对于使用透明大页的场景,还需要关注大页内存的碎片化问题。如果/proc/buddyinfo中高阶内存块严重不足,说明内存碎片化严重,透明大页的分配成功率会降低。此时可以考虑关闭透明大页,或者调整vm.extfrag_threshold参数来减少碎片整理的开销。

进程级内存调优:找到真正的泄漏点

缓存回收只能解决文件缓存占用的问题,如果内存被进程本身大量占用,就必须从进程层面入手。top或者htop按内存排序可以快速定位占用最高的进程,但要注意区分进程的RSS和VSZ。RSS是进程实际占用的物理内存,VSZ是虚拟内存大小,后者包含了未实际分配的空间,参考价值有限。

更精确的分析工具是pmap,它可以查看某个进程的内存映射详情。通过pmap -x PID,可以看到进程的每个内存段是匿名内存还是文件映射,是私有还是共享。如果发现大量匿名内存段持续增长且不释放,基本可以判定为内存泄漏。

# 查看进程内存映射
pmap -x $(pgrep -f java | head -1) | sort -k3 -n -r | head -20

对于Java应用这类运行在虚拟机上的进程,堆内存的配置直接影响整体内存表现。很多人习惯把Xms和Xmx设成一样的值,防止堆扩容带来的性能抖动,但这样做会让JVM一启动就占用全部堆内存。在容器化环境中,如果堆内存设得过大,加上堆外内存和元空间,很容易触发容器的内存限制。建议为容器内的Java进程预留至少20%的内存给堆外使用,包括直接内存、线程栈和JVM自身开销。

Golang程序的内存表现则有所不同。Go的垃圾回收器采用并发标记清除算法,释放的内存并不会立即归还给操作系统,而是保留在runtime的内存池中,这会导致RSS看起来很高。可以通过设置GODEBUG=madvdontneed=1环境变量,让Go在释放内存时使用MADV_DONTNEED标记,主动通知内核回收这部分物理内存。但这样做会增加系统调用的开销,不适合所有场景。

OOM Killer的合理配置

当内存真的耗尽时,OOM Killer会选择一个进程杀掉来释放内存。默认的选择策略基于oom_score,内核根据进程的内存占用、运行时间等因素打分,分数越高越容易被杀。但默认策略有时候会杀掉关键进程,导致服务不可用。

可以通过/proc/PID/oom_score_adj来调整特定进程的OOM优先级。值范围是-1000到1000,-1000表示完全不会被OOM Killer选中。对于数据库、核心业务服务等关键进程,建议设为-500到-100,既保留了一定的保护,又不会完全阻止OOM机制发挥作用。完全不设防的风险在于,如果这些进程本身内存泄漏,系统将没有任何自救手段。

# 保护关键进程
echo -500 > /proc/$(pgrep -f mysqld)/oom_score_adj
# 主动标记可牺牲进程
echo 500 > /proc/$(pgrep -f cache-service)/oom_score_adj

对于运行容器化应用的服务器,cgroup的内存限制比OOM Killer更先触发。当容器内存使用量达到limit时,cgroup会直接杀掉容器内进程。合理设置容器的memory limit和memory reservation,可以给内核留出缓存回收的时间窗口。memory.limit_in_bytes是硬限制,memory.soft_limit_in_bytes是软限制,当系统内存紧张时,内核会优先回收超过软限制的容器的缓存。

监控与预警:比调优更重要的事

再好的调优也架不住业务量的增长和代码的变更。建立内存使用的监控体系,比事后的调优更有价值。除了常规的内存使用率,更应该监控内存分配速率和页回收速率。sar -B可以查看系统的页换入换出情况,如果pgscand/s持续大于零,说明系统在主动扫描和回收内存,这是内存压力的前兆。

# 查看内存页扫描和回收统计
sar -B 1 10
# 重点关注pgscank/s和pgscand/s

另一个容易被忽略的指标是kswapd进程的CPU占用率。kswapd是内核的内存回收守护进程,如果它的CPU占用率持续超过5%,说明内存回收已经成为了系统的性能瓶颈。这时候不是简单地加内存就能解决问题,需要结合vm.swappiness参数来调整回收策略。swappiness默认是60,对于纯SSD存储的服务器,可以降到10甚至0,让内核优先回收文件缓存而不是换出匿名页,减少不必要的磁盘I/O。

最后需要强调的是,内存调优没有银弹。一台跑数据库的服务器和一台跑静态文件服务的服务器,内存使用模式完全不同。前者需要尽可能大的页缓存来加速数据读取,后者则更需要关注网络缓冲区和连接状态的占用。理解业务的内存使用特征,结合内核提供的观测手段和调优接口,才能做出真正有效的优化。盲目照搬别人的参数配置,往往适得其反。