简介:面向银河麒麟V10服务器运维场景的轻量脚本包,聚焦系统长时间运行后内存不释放、可用内存逐步下降的问题,适合需要快速落地定时清理与日常巡检方案的中初级运维人员参考。压缩包仅2KB,共3个文件,由2个sh脚本和1个txt配置说明组成:脚本承担定时任务与内存释放逻辑,文本文件用于说明参数配置和使用注意,体量小巧便于在测试或生产环境前审阅。已有787人学习下载,说明该问题在国产化服务器运维中具有一定普遍性。内容围绕“定时”这一核心思路,给出可复用的脚本组织方式与配置说明,读者可据此理解如何将内存清理纳入计划任务、结合系统监控判断触发时机,并保留配置记录以便后续排查;同时可作为内存泄漏初步缓解与运维巡检的参考素材,帮助减少人工干预频率并提升问题定位效率。
1. 内存明明闲着,服务却被 OOM 干掉
一台跑麒麟 V10 的生产机,free -m里available还有十几个 G,半夜业务进程还是被 OOM killer 干掉了。运维第一反应通常是挂个定时任务清缓存——这条路能走通,但只在特定条件下有效,清错对象就是白忙。这篇记录我把麒麟 V10 上"内存不释放"这件事拆开:先用free和/proc/meminfo分清可回收缓存和真泄漏,再给一份带阈值判断的定时回收脚本,能直接挂 cron 或 systemd timer,最后说清楚哪些场景下drop_caches根本救不了你。适合管着几台到几十台国产 Linux 服务器、被"内存高"告警追着跑的运维。
2. 先分清可回收缓存和真泄漏:free 与 /proc/meminfo 的读法
在麒麟 V10 上判断内存状态,free只能当第一眼,/proc/meminfo才是定案依据。顺序反了,后面定时任务写得再漂亮也是在错误的目标上使劲。
2.1 free -m 里哪一列才是能用的内存
先看一份典型输出,这是我一般让同事贴过来的第一条命令:
free -m # total used free shared buff/cache available # Mem: 32011 18420 1203 412 22388 12960 # Swap: 8191 512 7679free只有 1.2G,很多人到这一步就开始紧张了,实际上真正的判据是最后一列available,这里还有 12.9G。buff/cache那 22G 里绝大部分是文件页和 dentry/inode 缓存,内核在进程要内存时会主动回收,属于"借出去还能要回来"的部分,不需要外部干预。
shared列容易被忽略,它统计的是 tmpfs 和 shmem 占用。这部分虽然也记在buff/cache里,但属于匿名页性质,drop_caches根本动不了它。见过有人把/dev/shm当临时目录写满,然后反复执行清缓存脚本,free一点都不涨,最后查df -h /dev/shm才发现问题——这类属于典型的清错对象。
available的估算口径大致是MemFree加上可回收的 page cache 和可回收 slab,再减去内核水位等不能挪用的部分。所以要看的不是它绝对值高低,而是它掉下去之后能不能自己回来。如果一小时内available从 12G 掉到 1G 且不再回升,那才是真出事了。
2.2 /proc/meminfo 里几个决定成败的字段
free的信息都来自这里,直接读原文件能拿到更细的拆分。我常备的一条命令:
awk '/^(MemTotal|MemFree|MemAvailable|Buffers|Cached|SwapCached|Slab|SReclaimable|SUnreclaim|AnonPages|Mapped|Dirty|Writeback|Shmem):/' /proc/meminfo各字段和"能不能被清掉"的关系,整理成一张表更直观:
| 字段 | 含义 | drop_caches 能否回收 |
|---|---|---|
Cached | 文件页缓存,含 shmem | 大部分可以,shmem/tmpfs 部分不行 |
Buffers | 块设备元数据缓存 | 可以 |
SReclaimable | 可回收 slab,如 dentry、inode_cache | 可以(对应drop_caches=2) |
SUnreclaim | 不可回收 slab | 不行 |
AnonPages | 匿名页,进程堆和栈 | 不行,只能 swap 或结束进程 |
Mapped | mmap 映射的文件页 | 部分可以 |
Dirty/Writeback | 脏页和正在回写的页 | 必须先sync,否则清不动 |
Shmem | tmpfs / 共享内存 | 不行 |
SReclaimable和SUnreclaim是分开的两项,很多人只看Slab总和,看到几十 G 就急着清。如果大头在SReclaimable,说明是文件系统遍历留下的 dentry/inode,清一次能下来;如果大头在SUnreclaim,那是内核模块或驱动自己申请的,定时任务对它是零效果,得从驱动版本上找原因。
Dirty值得单独提一句。有些机器上Cached很高但清不下来,是因为脏页还没回写,内核不会把脏页当成可立即回收的对象。脚本里sync那一步不是仪式感,是前置条件。
2.3 用 slabtop 和 ps_mem 把增长源夹出来
知道"哪一类内存在涨"之后,下一步是定位到具体的 cache 名或进程。slab 侧看排行:
# 按缓存占用排序,只看前 15 行 slabtop -o -s c | head -15如果dentry、inode_cache、ext4_inode_cache排在前三,基本可以确定是文件遍历或大量小文件操作留下的,drop_caches=2是有效手段。如果看到的是kmalloc-*或某个特定驱动的 cache 长期占大头,那就不是清理能解决的事。
进程侧看排行,ps_mem比ps aux准,因为它按 PSS 统计,共享库不会被重复计算:
# 仓库里有就用仓库的,没有可以 pip3 install ps_mem,本质是个单文件脚本 ps_mem | head -20再细一层,用smaps_rollup看单进程的内存构成。内核 4.14 以上有smaps_rollup,麒麟 V10 服务端自带的内核是满足的;老一点的版本退回累加smaps:
PID=$(pgrep -f your_service | head -1) if [ -r /proc/$PID/smaps_rollup ]; then grep -E '^(Rss|Pss|Private_Dirty|Anonymous):' /proc/$PID/smaps_rollup else # 兜底:从 smaps 累加,代价是遍历全部 VMA,慢但准 awk '/^Rss:/{r+=$2} /^Pss:/{p+=$2} /^Anonymous:/{a+=$2} \ END{printf "RSS=%dMB PSS=%dMB Anon=%dMB\n", r/1024, p/1024, a/1024}' /proc/$PID/smaps fi判断泄漏只需要一个动作:隔一段时间采样Anonymous,看它是否单调上涨。上涨就是应用自己的问题,清缓存只会让free数字好看,OOM 该来还是来。
PID=$(pgrep -f your_service | head -1) for i in $(seq 1 10); do awk -v t="$(date +%T)" '/^Anonymous:/{a+=$2} \ END{printf "%s anon=%.0fMB\n", t, a/1024}' /proc/$PID/smaps sleep 60 done十分钟的曲线足够定性了。平坦或者锯齿状反复是正常;一路向上不回头,就是泄漏,别再往定时清理上投入时间。
2.4 一条判断链:三种现象对应三种处置
把这套东西压缩成一个决策顺序,现场排查时按顺序走,不要跳步:
| 观测到的现象 | 最可能的原因 | 处置方向 |
|---|---|---|
buff/cache高,available充裕,无 OOM | 正常缓存 | 不动,最多调vfs_cache_pressure |
available掉到个位数,SReclaimable同步上涨 | dentry/inode 堆积 | 定时drop_caches=2,本文方案 |
AnonPages单调涨,某进程 RSS 同步涨 | 应用内存泄漏 | 定位进程,cgroup 限额或重启,清缓存无效 |
Slab涨但SReclaimable不涨 | 内核模块/驱动泄漏 | 升级驱动或内核,清缓存无效 |
Shmem高,df -h /dev/shm接近满 | tmpfs 被写满 | 清理临时文件或调整 tmpfs 容量 |
这张表的意义在于划边界:只有前两类里涉及SReclaimable的那部分,才是定时回收脚本的正当业务。把后面几类也交给定时任务,就成了定期做无用功,还容易把"清过缓存了"当成已经处理过的证据,耽误真正的定位。
3. 定时回收脚本:阈值判断比 drop_caches 本身更重要
3.1 手动验证一次,再谈自动化
写脚本之前先在测试机上手工跑一遍,确认清理确实有效、幅度多大,否则自动化只是把无效动作重复放大:
# 1. 记录清理前快照 awk '/^(MemAvailable|Cached|SReclaimable|Slab):/' /proc/meminfo # 2. 刷脏页,脏页没回写之前内核不会把它算作可回收 sync; sync # 3. 先回收 page cache echo 1 > /proc/sys/vm/drop_caches # 4. 再回收 slab 里的 dentry/inode echo 2 > /proc/sys/vm/drop_caches # 5. 再看一次快照对比 awk '/^(MemAvailable|Cached|SReclaimable|Slab):/' /proc/meminfodrop_caches的三个取值含义不同:1只清 page cache,2只清可回收 slab,3两者都清。之所以建议分两步写,是因为分开执行能看出到底哪一类占了大头,后续参数调整有依据。直接echo 3一次搞定也行,但就丢掉了这个观测点。
提示:
drop_caches是非破坏性操作,只回收干净页和可回收 slab,不会丢数据,但会让热数据缓存全部失效,下一波请求会集中打到磁盘上。生产机上不要在业务高峰期手工跑,做完一次先观察十分钟 IO。
3.2 脚本骨架:加锁、判阈值、留日志
最省事的写法是无条件定时清,但生产环境里我一般会加阈值和并发锁,避免它自己成为抖动源:
#!/bin/bash # /usr/local/sbin/mem_reclaim.sh set -u THRESHOLD=${THRESHOLD:-15} # 可用内存占比低于该值才动手,单位 % LOG=${LOG:-/var/log/mem_reclaim.log} LOCK=/var/run/mem_reclaim.lock # 并发锁:上一次还没跑完就跳过,避免短周期任务叠加 exec 9>"$LOCK" flock -n 9 || { echo "$(date '+%F %T') skip: previous run still running" >> "$LOG"; exit 0; } # 老内核可能没有 MemAvailable,用 MemFree+Buffers+Cached 兜底 read -r total avail <<< "$(awk ' /^MemTotal:/ {t=$2} /^MemAvailable:/ {a=$2; has=1} /^MemFree:/ {f=$2} /^Buffers:/ {b=$2} /^Cached:/ {c=$2} END{ if (!has) a = f + b + c; printf "%d %d", t, a }' /proc/meminfo)" [ "$total" -eq 0 ] && { echo "$(date '+%F %T') error: MemTotal unreadable" >> "$LOG"; exit 1; } pct=$(awk -v a="$avail" -v t="$total" 'BEGIN{printf "%d", a*100/t}') echo "$(date '+%F %T') total=${total}kB avail=${avail}kB pct=${pct}%" >> "$LOG" if [ "$pct" -lt "$THRESHOLD" ]; then sync sleep 1 # 给回写线程一点时间落盘 echo 1 > /proc/sys/vm/drop_caches echo 2 > /proc/sys/vm/drop_caches after=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo) echo "$(date '+%F %T') reclaimed pct=${pct}% avail_after=$((after/1024))MB" >> "$LOG" else echo "$(date '+%F %T') above threshold, no action" >> "$LOG" fi几个参数决定了它是"帮忙"还是"添乱":
| 参数 | 建议值 | 说明 |
|---|---|---|
THRESHOLD | 10~15(%) | 低于此才清。设太高会频繁触发,等于周期性冷启动 |
| 执行周期 | 30~60 分钟 | 10 分钟一次在生产上偏激进,除非 IO 余量很大 |
LOCK | /var/run/下 | 放 tmpfs,重启自动清掉,不会留下死锁文件 |
LOG | /var/log/下 | 必须配 logrotate,否则日志本身会变成另一个增长源 |
阈值这条线怎么定,我的习惯是先看历史曲线:把available的周内最低值找出来,取比它再低几个百分点的位置。这样正常运行期脚本基本不触发,只有真正下滑时才介入,drop_caches的执行次数一个月可能就几次,对 IO 的影响可以忽略。
3.3 挂 cron 还是 systemd timer
两种都能用。麒麟 V10 上 cron 更通用,systemd timer 的好处是能拿到执行状态、支持Persistent(关机错过的任务开机补跑)、日志直接进 journal。追求简单就选 cron:
# /etc/cron.d/mem_reclaim # 放在 cron.d 下必须声明 SHELL 和 PATH,cron 默认环境极简 SHELL=/bin/bash PATH=/sbin:/bin:/usr/sbin:/usr/bin */30 * * * * root /usr/local/sbin/mem_reclaim.sh >> /var/log/mem_reclaim.cron.log 2>&1如果想要更可控的状态管理,用 timer:
# /etc/systemd/system/mem-reclaim.service [Unit] Description=Reclaim reclaimable page and slab cache under memory pressure After=network.target [Service] Type=oneshot Environment=THRESHOLD=15 ExecStart=/usr/local/sbin/mem_reclaim.sh# /etc/systemd/system/mem-reclaim.timer [Unit] Description=Run mem-reclaim every 30 minutes [Timer] OnBootSec=10min OnUnitActiveSec=30min AccuracySec=1min Persistent=true [Install] WantedBy=timers.target启用和核对状态:
chmod 0755 /usr/local/sbin/mem_reclaim.sh systemctl daemon-reload systemctl enable --now mem-reclaim.timer systemctl list-timers --all | grep mem-reclaim journalctl -u mem-reclaim.service -n 20 --no-pager注意:脚本必须以 root 运行,
/proc/sys/vm/drop_caches默认只有 root 可写。放在容器里跑的思路行不通,容器内看到的/proc/sys通常是只读挂载,要清也只能清宿主机。
3.4 日志轮转和不定期回看
这个脚本一旦上线就是长期存在的东西,日志不轮转会慢慢把磁盘吃满,讽刺的是它本来是用来救内存的:
# /etc/logrotate.d/mem_reclaim /var/log/mem_reclaim.log /var/log/mem_reclaim.cron.log { weekly rotate 8 compress missingok notifempty create 0640 root root }比日志轮转更重要的是定期回看日志里pct的分布。如果连续两周每次执行都是above threshold, no action,说明这台机器根本不需要这个任务,撤掉减少一层复杂度;如果每次都在阈值线下、清理后avail_after只涨一点点,说明清的东西不对,回去看第 2 章的判断链。
4. 定时清理的五个坑:从清了没用到清了添乱
这一章是血泪经验集中区。前面脚本能跑起来不难,难的是跑起来之后别出岔子。
4.1 现象:手工执行有效,cron 里毫无反应
原因几乎都是环境差异。cron 的PATH只有/usr/bin:/bin,脚本里如果用到了相对路径或者/usr/sbin下的命令就会失败;另外cron.d下的文件权限和属主不对(必须是 root:root,且不能有组写权限),cron 会直接忽略不报错。
解决办法是把环境写死在任务文件里,命令全部用绝对路径,然后去看 cron 自己的日志确认到底有没有被调度:
# 麒麟 V10 上 cron 日志可能在 messages 或 journal 里,两个都查一下 grep CROND /var/log/messages | tail -20 journalctl -u crond --since "1 hour ago" --no-pager | tail -20如果日志里能看到执行记录但没有脚本自己的输出,问题就在脚本内部,直接bash -x手工模拟 cron 环境跑一遍最快。
4.2 现象:free 涨回来了,业务进程照样被 OOM 杀
这是最典型的翻车场景。drop_caches只动 page cache 和可回收 slab,对AnonPages一点办法没有。而 OOM killer 的评分主要看进程的匿名页占用和 cgroup 限额,跟free显示的剩余量关系不大。
确认方式很直接,看内核留下的现场:
dmesg -T | grep -i -E 'out of memory|killed process' | tail -10输出里会带上被杀进程的total-vm、rss和当时的Mem-Info快照。如果anon-rss很大而file-rss很小,说明压力全在匿名页上,定时清缓存这条路从一开始就不适用,得回到应用层查泄漏或者上 cgroup 限额。
4.3 现象:脚本报 Permission denied,或写 drop_caches 没效果
两个方向排查。一是权限,脚本是不是真的以 root 跑的,id打出来确认一下;二是/proc的挂载属性,容器或加固过的环境里/proc/sys可能是只读的:
mount | grep ' /proc ' # 输出里出现 ro 或者 /proc/sys 单独 ro 挂载,就别在容器里折腾了还有一种更隐蔽的情况:echo 3 > /proc/sys/vm/drop_caches执行成功但Cached几乎不降。这时候看Dirty和Writeback,如果脏页很多且磁盘 IO 已经饱和,回写线程跟不上,内核会拒绝回收这些页。先解决 IO 瓶颈,再谈回收。
4.4 现象:加了定时任务之后,业务接口的 P99 反而涨了
清缓存本身不慢,慢的是清完之后的那一波。所有热数据失效,紧接着的请求全部穿透到磁盘,如果底层是机械盘或者网络存储,这一下能顶出几秒的抖动。周期越短,这种"周期性冷启动"越频繁,等于人为制造内存和 IO 的锯齿。
处置办法是三条一起上:把周期从 10 分钟拉到 30~60 分钟;把阈值从 30% 降到 10~15%;在脚本里记录清理前后的时间戳,事后能和 APM 的毛刺对齐。如果业务对延迟极其敏感,宁可让这台机器多留 2G 余量,也不要靠高频清理硬撑。
4.5 现象:日志里 pct 恒为 0,或者脚本报 unary operator expected
前者是整数除法被截断,可用内存占比不足 1% 时a*100/t会算成 0,看起来像内存耗尽。后者多半是MemAvailable字段取不到——极老的内核没有这个字段,awk输出空串,后面比较就炸了。上面 3.2 的脚本里已经用has标志位做了兜底,用MemFree+Buffers+Cached近似,误差在可接受范围内。
另外注意单位。/proc/meminfo全是 kB,脚本里做展示时该除 1024 就除,别把 kB 当 MB 报出去,这种低级错误在值班交接时最容易误导人。
5. 参数级止血:把回收压力交给内核而不是定时任务
定时清理是兜底手段,不是首选。真正让内存"该放就放"的,是几个内核参数的方向调整。麒麟 V10 上改这些走/etc/sysctl.d/,别直接写/etc/sysctl.conf,便于按场景拆分和回滚:
# /etc/sysctl.d/90-memory-reclaim.conf # dentry/inode 缓存更容易被回收,适合小文件遍历多的场景 vm.vfs_cache_pressure = 200 # 给内核留出更大的安全水位,降低 direct reclaim 触发概率 vm.min_free_kbytes = 1048576 # 尽量少用 swap,但不要设 0,留一点能缓解突发压力 vm.swappiness = 10 # 脏页阈值收紧,避免一次性堆积太多脏页阻塞回收 vm.dirty_ratio = 15 vm.dirty_background_ratio = 5应用并确认:
sysctl --system sysctl vm.vfs_cache_pressure vm.min_free_kbytes vm.swappiness参数不是越大越好。vfs_cache_pressure调到 200 意味着 dentry 和 inode 更容易被丢弃,代价是频繁的文件元数据访问要反复重建,文件服务类业务会明显变慢,一般从 150 起试,观察一周再动。min_free_kbytes设到 1G 对于 32G 内存的机器是合理的,但 8G 小内存机上设这么高会直接吃掉可用内存,按总内存的 2%~3% 估比较稳。
验证有没有效果,别只看free。要看/proc/vmstat里的直接回收计数,它才是内存紧张程度的真实指标:
# 每 10 秒采一次,重点看 pgscan_direct 和 pgsteal_direct 的增量 vmstat -s | grep -E 'pages scanned|pages stolen' for i in $(seq 1 6); do awk -v t="$(date +%T)" ' /^pgscan_direct/ {sd=$2} /^pgsteal_direct/{td=$2} /^pgscan_kswapd/ {sk=$2} END{printf "%s direct_scan=%d direct_steal=%d kswapd_scan=%d\n", t, sd, td, sk}' /proc/vmstat sleep 10 done判断标准很干脆:pgscan_direct持续快速增长,说明进程申请内存时被逼着自己去回收,压力已经传导到业务线程上了,这时候调参数和加内存都比定时清理更对症;如果只有pgscan_kswapd在动,说明是后台线程在慢慢回收,属于健康状态,什么都不用做。
还有一个我踩过的坑:改完 sysctl 一定要做一次长时间对比,至少覆盖一个业务高峰。有次在测试机上把vfs_cache_pressure调到 300 感觉很爽,SReclaimable确实压下去了,结果上线后文件索引服务的延迟翻了一倍,回滚才恢复。从那以后我每次动内存相关参数,都会先在一台低峰期可切的机器上跑满 24 小时,把vmstat、sar -r和业务延迟三条曲线放在一起看,确认没有副作用再推到其他机器——内存这个黑匣子,宁可慢一点。希望帮到你。
本文还有配套的精品资源,点击获取