简介:这份资料面向银河麒麟V10系统运维人员,解决服务器长期运行中内存不释放、可用内存逐渐减少导致的性能下降问题。包内提供一套轻量定时处理方案:RAR压缩包共3个文件,含2个shell脚本和1个txt配置说明文档,脚本可用于自动化监控与释放内存,说明文档则交代部署和调整要点,整体仅2KB,易于下载后直接按需修改。已有780人学习下载,适合需要快速缓解内存泄漏或临时回收内存的初中级运维者。通过阅读脚本与配置说明,读者可结合free、top等命令掌握内存使用观察方法,并据此设置定时任务、调整执行频率或阈值,形成可持续的内存管理习惯。对于暂无法定位代码级泄漏的场景,该方案能提供实用兜底手段,降低系统卡顿与宕机风险。
1. 银河麒麟V10内存不释放:一个让运维凌晨三点爬起来的玄学问题
在国产化服务器上跑业务,我接手过一台银河麒麟V10的机器,现象非常典型:free -h 看 used 一天比一天高,top 里看不到哪个进程特别离谱,服务也还在,可系统就是慢慢被拖到卡死,最后某天凌晨业务进程直接消失,查 dmesg 才发现是 OOM 把进程杀了。更折腾的是,手动 drop_caches 或者重启应用,内存能好一阵子,过几天又涨回来,像是系统自己把内存“吃”了不吐。这类问题在国产化替代的服务器上尤其多,因为很多迁移过来的老业务没有完整的监控体系,等看到告警时往往已经晚了。本文就用我实际处理过的一套定时清理方案,把这台机器“内存不释放”的根因、排查顺序、脚本和坑全部梳理一遍:新手可以直接抄脚本跑起来,熟手能从我踩过的坑里拿走排查思路。
2. 内存不释放的真相:先分清 buffer/cache 和进程 RSS,再谈清理
2.1 看内存只看 used 是最大的错觉
很多人一登录服务器就敲 free -m,看到 used 那一列飙到 90% 就慌了,马上想清理。问题在于,银河麒麟V10 的 free 输出里,used 是包含 page cache 的。内核为了提高 IO 性能,会把读过的磁盘块、文件内容缓存到内存里,这部分叫 Cached 和 Buffers,本质上是“可以随时回收”的。真正需要关心的指标是 available,也就是“在不触发 swap 的前提下,还能分配给新进程的内存”。我处理那台机器时,第一眼看到 used 93%,但 available 还有 20%,说明系统并没有那么紧张,B 同学直接执行 echo 3 > /proc/sys/vm/drop_caches,把整台机器的 cache 全部清掉,结果可用内存是降下来了,可业务 IO 直接冲高,数据库查询慢了一倍多。从那以后我要求团队先看 available,再决定动不动手。判断内存是否真的“不释放”,正确姿势是读 /proc/meminfo 的几个关键字段:MemTotal、MemFree、MemAvailable、Buffers、Cached、SReclaimable、Writeback。其中 MemAvailable 才是内核根据内存碎片、回收压力动态计算出来的“真实可分配量”。如果 available 长期低于总内存的 10%,才叫内存吃紧;如果只是 Cached 高,那是正常的系统行为。
2.2 三种常见的病根:cache 未回收、内存碎片、进程泄漏
银河麒麟V10 上“内存不释放”本质上有三种原因,三种的排查方向和清理手段完全不同。第一种是 page cache 回收不及时,典型特征是 MemFree 低、MemAvailable 也低、Cached 高、Writeback 不为 0。这通常是因为磁盘写回慢,脏页堆积占用了大量内存。第二种是内存碎片,内核的伙伴系统分配不出大块连续内存,但小块内存还有很多剩余,这种在 /proc/buddyinfo 里一眼能看出来——order 为 0、1 的空闲块很多,order 3 以上的大块几乎没有。碎片问题用 drop_caches 也没用,因为它不整理伙伴系统。第三种是进程内存泄漏,特征是某几个进程的 RSS 随时间持续增长,重启进程后内存明显下降,过一阵又涨回去。定位泄漏要观察单进程趋势,不能只看全局。我遇到过一台跑 Java 服务的机器,堆外内存一直在涨,全局看 used 高,但 Cached 不高、available 极低,ps aux --sort=-%mem 一看,某个 Java 进程 RSS 已经占了一半内存,重启进程后半小时内存又拉满,最后定位到是 JNI 层没释放内存。遇到这种情况,定时清理只能作为止血手段,真正的解法是让开发改代码。
3. 定时清理落地:crontab + 水位监控脚本,给内存装上自动阀门
3.1 第一步:写一个只看 RSS 和 available 的监控脚本
我习惯把清理脚本放在 /usr/local/sbin/ 下,用 root 身份运行,逻辑分两层:先检测系统整体水位,再检测具体进程的 RSS。如果整体 available 低于阈值,就记录 top 内存进程;如果某个白名单外的进程 RSS 异常增长,就重启它。不要把“全局 used 高”当成杀进程的依据,否则会误伤。下面这个脚本是我实际在银河麒麟V10上跑过一段时间的版本,删掉了具体的业务进程名,保留通用部分。
#!/bin/bash # 内存水位监控与自动清理脚本,适用于银河麒麟V10 # 放在 /usr/local/sbin/mem_watch.sh,确认有执行权限后由 crond 调用 MEM_FILE=/proc/meminfo LOG_FILE=/var/log/mem_watch.log WARN_PCT=85 # MemAvailable 低于 15% 时触发记录 RESTART_PCT=92 # MemAvailable 低于 8% 时触发重启动作 # 读 MemTotal 和 MemAvailable,单位都是 kB total=$(awk '/MemTotal/ {print $2}' "$MEM_FILE") avail=$(awk '/MemAvailable/ {print $2}' "$MEM_FILE") # 整数运算,避免浮点比较在不同发行版上的兼容性问题 used_pct=$(( (total - avail) * 100 / total )) echo "$(date '+%F %T') used_pct=${used_pct}% total_kb=${total} avail_kb=${avail}" >> "$LOG_FILE" if [ "$used_pct" -ge "$RESTART_PCT" ]; then echo "--- $(date '+%F %T') 内存紧张,记录 top 进程 ---" >> "$LOG_FILE" ps aux --sort=-%mem | head -n 20 >> "$LOG_FILE" # 白名单:只处理确定可以安全重启的业务服务 # 启动命令按实际环境修改,tt --restart 是模拟重启动作 for svc in biz-web biz-api; do if pgrep -f "$svc" > /dev/null; then /usr/bin/systemctl restart "$svc" echo "已重启 $svc" >> "$LOG_FILE" fi done elif [ "$used_pct" -ge "$WARN_PCT" ]; then echo "--- $(date '+%F %T') 达到警戒线,只记录不重启 ---" >> "$LOG_FILE" ps aux --sort=-%mem | head -n 10 >> "$LOG_FILE" fi脚本先用两个 awk 分别从 /proc/meminfo 里提取 MemTotal 和 MemAvailable,这一步不能用 free -m 解析,因为 free 的输出在不同版本里列位置会变,生产脚本里解析 free 是自找麻烦。这里所有判断都用整数 kB 计算 used_pct,好处是 shell 的算术扩展不依赖 bc 或 awk 的浮点能力,银河麒麟V10 默认安装的 mawk 在某些版本上浮点比较会静默出错。WARN_PCT 和 RESTART_PCT 两个阈值根据机器实际内存调整,我一般建议 4GB 内存的机器警戒线放低一点,内存大于 32GB 的机器可以适当调高。脚本最关键的设计是“只重启白名单服务”,宁愿漏杀也不要错杀,后面避坑章节我会专门讲误杀事故。
3.2 第二步:crontab 配置要避开环境变量这个大坑
脚本写好之后,测试执行一遍确认没有报错,然后交给 crond。crontab 里有两处容易翻车:一是 crond 默认 PATH 很精简,脚本里如果用了 /usr/bin 之外的工具,或者脚本本身没有写绝对路径,就会静默失败;二是 crond 不是登录 shell,不会加载 /etc/profile 里的环境变量,如果脚本依赖了 JAVA_HOME 之类的变量,直接跑没问题,放进 cron 就找不到命令。我的做法是脚本内部全用绝对路径,systemctl、pgrep、awk 都写全路径,crond 配置里显式指定 SHELL 和 PATH。
# crontab -e 追加,每 5 分钟执行一次内存水位检查 SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin */5 * * * * /bin/bash /usr/local/sbin/mem_watch.sh >> /var/log/mem_watch_cron.log 2>&1第 1、2 行是给 crond 用的环境变量,保证脚本运行时能用到正确的 bash 和基础命令路径。第 3 行是定时规则,*/5 表示每 5 分钟跑一次。这里我刻意选了 5 分钟而不是 1 分钟,因为内存增长是慢过程,1 分钟跑一次除了刷日志没意义,还会在内存峰值时叠加脚本自身的开销。日志重定向到独立文件,避免和脚本内部日志混在一起,排查 crond 问题时可以分开看。注意:脚本本身必须用 chmod +x 加上执行权限,否则 crontab 会报 “No such file or directory”,并不是脚本不存在,而是没权限执行。
3.3 第三步:drop_caches 到底该不该用,什么时候用
很多网上的教程会让你执行 echo 3 > /proc/sys/vm/drop_caches,说能“清空缓存释放内存”。这个命令在银河麒麟V10 下确实有效,但我强烈建议只在两种场景使用:一是确认 available 已经极低,且进程 RSS 都不高,说明内存被 cache 占满,这时可以清;二是在业务低峰期例行维护时,为了给即将上线的批量任务腾内存。drop_caches 的写法有讲究,参数 1 只释放 page cache,参数 2 释放 dentries 和 inodes,参数 3 是两者都清。生产环境我会先用 1,不要一上来就 3,因为清 dentries 会让文件系统元数据重新加载,对大量小文件的业务影响很明显。实际执行前强制刷盘是必须的:
sync echo 1 > /proc/sys/vm/drop_caches cat /proc/sys/vm/drop_cachessync 先确保脏页写回磁盘,避免丢数据;echo 1 只清 page cache,保留 dentries;最后的 cat 只是确认值写入成功。我遇到过一次执行 echo 3 之后,某个 Java 应用访问大量小配置文件,每次 stat 都重新读磁盘,接口延迟从 8 毫秒涨到 200 多毫秒,从那以后我再也没有默认用 3。如果你把它放进定时脚本,还要加一个条件判断:只有 available 低于阈值时才执行,不能每次 crontab 触发都清。更好的替代方案是调低 /proc/sys/vm/vfs_cache_pressure,让内核更积极回收缓存,而不是手动粗暴清理。
4. 进程被 OOM 杀掉之后:先捞现场,再决定重启还是扩容
4.1 从 dmesg 和 journald 里找回被杀进程的现场
OOM 是比内存不释放更严重的结果,说明定时清理也没能及时止住。银河麒麟V10 的内核在触发 OOM 时会把“谁被杀了、当时内存状况如何”写进内核日志。我最常用的捞现场命令是 dmesg,因为 OOM 信息会保留到下次重启,journald 在某些裁剪版系统上可能没有完整落盘,dmesg 更稳。
dmesg -T | grep -i -B 2 -A 8 "out of memory" journalctl -k -T --since "1 hour ago" | grep -i -A 8 "oom-kill" cat /var/log/messages | grep -i -B 2 -A 8 "oom"第一条直接搜内核环形缓冲区里的 OOM 记录,-B 2 带出触发前两行上下文,-A 8 显示被杀的进程名、PID、RSS 和当时的全局内存水位。第二条是查看 systemd 的 kernel 日志,适合 dmesg 输出被别的内容刷掉时用。第三条是保守方案,/var/log/messages 在很多系统上还在写。我实际遇到的情况是 dmesg 里明确写了 “Killed process 13245 (biz-java), total-vm:8543200kB, anon-rss:4100000kB”,说明是 Java 进程的匿名页占满了内存。注意不要只看进程名就完事,还要看是哪个 cgroup 触发的 OOM,如果 OOM 发生在 systemd slice 里,说明是服务本身的 MemoryLimit 设置太保守,而不是整机内存不够。
4.2 Overcommit 和 min_free_kbytes:改内核参数防止频繁误杀
捞到现场之后,如果不做任何调优,过几天同样的 OOM 还会再来。我一般会顺手检查 /proc/sys/vm/overcommit_memory 和 /proc/sys/vm/min_free_kbytes。overcommit_memory 默认是 0,也就是启发式过度分配,部分应用申请内存时会预支;如果被改成了 1,内核几乎不做任何检查就直接分配,等到物理内存耗尽时再来 OOM,这会让定时清理脚本根本来不及反应。min_free_kbytes 则决定内核为紧急分配保留多少内存,默认值太小的话,系统在内存耗尽前不会主动回收。这两个参数不建议在生产环境乱改,但有一个场景我几乎必改:数据库或缓存类服务对延迟敏感,需要把 OOM 的触发点尽量延后,给运维留出人工介入时间。落地写法是在 /etc/sysctl.conf 里追加配置再 sysctl -p 生效:
# 保留更多紧急内存,防止系统在内存耗尽前毫无还手之力 vm.min_free_kbytes = 1048576 # 保持默认的启发式 overcommit,不强制关闭 vm.overcommit_memory = 0 vm.swappiness = 10vm.min_free_kbytes 设成 1GB,适合 16GB 以上的机器,内存小的机器设置这么大会白白浪费可用内存。swappiness 降到 10 是让内核尽量少用 swap,优先回收内存。这些参数不是万能药,但配合定时清理脚本一起用,能把 OOM 概率大幅压低。如果业务确实需要超大虚拟内存,那就别靠内核参数硬撑,老老实实加物理内存或者拆分服务。
5. 避坑与常见问题:定时清理方案里的五个坑,我全踩过
5.1 现象:crontab 里写了脚本,但它一次都没跑
原因排查:crond 服务没启动、脚本没有执行权限、脚本内部用了相对路径。我踩的坑是脚本里写了 top -b -n 1,而 top 在精简版系统上不存在,cron 日志里看到 “command not found”,但 crond 本身是好的。解决:systemctl status crond 确认服务在跑,然后直接用 /bin/bash 调用脚本,脚本内部所有外部命令都用绝对路径,运行时把 stdout 和 stderr 都重定向到日志。验证方式很简单,在 crontab 里临时加一条每分钟执行 date 的命令,看 /var/log/cron 有没有记录,就能确认 crond 链路是否正常。
5.2 现象:脚本里的 awk 浮点比较在银河麒麟V10 上报错或者结果不对
原因:系统默认的是 mawk,对浮点精度支持不好,用 awk 'BEGIN{print 1.1 > 1.0}' 这类判断在某些版本上会静默输出错误结果。解决:不搞浮点,全部换算成整数再比较。我在脚本里用 used_pct=$(( (total - avail) * 100 / total )),纯 shell 整数运算,避免 awk 参与大小判断。如果你确实需要算百分比,用 bc 也可以,但 bc 不一定默认安装,为了一个百分比去额外安装软件包不值得。
5.3 现象:脚本把 sshd 或者其他关键进程给杀了,远程直接断掉
原因:auto-kill 逻辑里用了 pgrep 模糊匹配,pgrep -f ssh 会把 sshd、sshd-agent 全部匹配进来,误杀后远程连接全部断开。解决:白名单机制,只对明确写出的服务名执行重启;再加一个保护判断,禁止对 PID 为 1 的进程以及 sshd、crond 执行任何 kill 动作。我的脚本里不会写“谁内存高就杀谁”,只写“白名单内的服务内存超限就重启”,宁可漏掉泄漏进程,也不能误杀无辜。还有一个细节:重启服务之前先检查它是不是 systemd 管理的,如果服务不是 systemd 启动的,systemctl restart 会报错,要改用 kill + 原启动命令拉起。
5.4 现象:/proc/meminfo 的数单位搞错,脚本算出来的百分比完全错误
原因:/proc/meminfo 里的 MemTotal、MemAvailable 单位是 kB,有人把它当成字节来算,然后跟 free -m 对比发现对不上,就怀疑脚本有问题。解决:全链路统一用 kB 计算,只有输出给人类看时才转成 MB 或 GB。我脚本里的 total_kb 和 avail_kb 就是这种思路,日志里记录单位,不会出现“30561MB”这种混用。特别注意,/proc/meminfo 的数值都是按 1024 进制的,不是 1000,写死倍数时不要用字节除以 1000。
5.5 现象:定时执行 echo 1 > drop_caches 之后,业务查询反而变慢
原因:cache 冷启动。page cache 被清掉后,所有原来命中缓存的文件读取全部穿透到磁盘,尤其对数据库类业务,缓冲池的 page 重新加载需要时间。解决:不要把 drop_caches 当成常规定时任务,只在 available 低于阈值时应急触发;优先调高 vfs_cache_pressure 让内核主动回收;如果业务是数据库,可以用数据库自己的内存管理参数,例如 MySQL 的 innodb_buffer_pool_size,没必要靠操作系统清理缓存来腾内存。这个坑本质上是“把 cache 高当成内存不释放”的错误认知导致的,回到第 2 章的判断方法:先确认 available 真的低,再考虑清理。
6. 快速定位内存不释放:一套黑匣子排查顺序,三十秒分清类别
6.1 三步定位法
接到告警之后,不要先看监控大盘,直接登录服务器按下面顺序执行。第一步,free -h 看 available;第二步,ps aux --sort=-%mem | head -n 10 看是谁占了进程内存;第三步,cat /proc/meminfo 看 Cached 和 Writeback。这三步能筛掉九成误判,顺序不要反,先看全局再看局部才能避免把 cache 当泄漏。定位结果对照:available 低但 Cached 高,是缓存回收问题;available 低且单个进程 RSS 高,是泄漏;available 低且 buddyinfo 大块空闲少,是碎片。碎片问题再执行一条 grep -A 5 "Node" /proc/buddyinfo,如果 order 3 以上基本为 0,那就是碎片,这种用 None 解决不了,要么重启服务,要么让内核做内存规整(echo 1 > /proc/sys/vm/compact_memory),不过对正在跑的生产环境我不建议手动 compact。
6.2 验证定时清理是否生效
脚本上线之后,至少观察三天,重点看 /var/log/mem_watch.log 里每天的内存趋势,以及 /var/log/cron 里有没有报错。我的习惯是把日志按周切割,如果内存使用率曲线没有持续抬升,说明方案生效;如果每周水位都在缓慢上涨,说明泄漏源头没堵住,定时清理只是拖延了 OOM 的发生时间,必须回去查具体进程。从那以后我每次接内存告警,都是先 free -h,再 ps 排 RSS,把 cache 和泄漏分开想清楚再动刀,踩过的这些坑基本没再翻过车。希望帮到你。
本文还有配套的精品资源,点击获取