ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux内存“一半”竟是假象?OOM Killer杀进程真相与VPS排查指南

Linux内存“一半”竟是假象?OOM Killer杀进程真相与VPS排查指南 凌晨三点被用户吵醒的经历我这辈子应该都忘不了。对方在群里刷屏“VPS内存明明还剩一半为什么我的Java进程又挂了”坦白说“还剩一半”这个描述是排查“进程忽然被杀”类问题时最误导人的信号。很多人把free输出里的free列当成剩余可用内存然后用这套认知判断一台服务器还能扛多少负载。但在Linux的内存管理里背后运行的是一套水位线、回收机制、过度分配策略和OOM Killer裁决逻辑。你看到的“一半”在内核眼里可能已经是“即将断粮”。这篇文章我会先把free命令的语义陷阱讲透再带你完整走一遍OOM Killer从判定到动手杀进程的链路然后复盘一次真实的VPS进程被杀排查过程最后给出我多年来常用的内存配置清单和规避方案。无论你是刚上手VPS的新手还是已经被OOM折磨过的老运维看完应该都能少走不少弯路。1. 让我先泼盆冷水free命令显示的“一半”可能只是假象1.1 只看free这一列等于只看钱包里不动的那几百块先做个小试验。你登录一台1G内存的VPS运行下面的命令free -h输出大概是这样的total used free shared buff/cache available Mem: 976Mi 380Mi 412Mi 12Mi 184Mi 434Mi Swap: 0B 0B 0B很多人的目光会锁定在free那一列412MB这不还有将近一半吗程序怎么就被杀了问题就出在这个“free”的定义上。Linux的free列指的是完全没有被任何用途占用的物理内存页。它既不包括可回收的页面缓存page cache也不在意这些内存页在内核眼中是否“容易拿到”。而真正能被进程安全申请到的内存更接近右侧的available列——这一列才是内核根据当前可回收性、内存水位、swap情况综合估算出的“还能给多少”的值。举个例子。你的VPS跑着Nginx、一个Python后端、再加一个Redis这些进程会不断产生文件读写Linux内核会把读过的文件内容缓存起来当作 page cache以便下次加速读取。这些缓存内存确实写在buff/cache里但当你启动一个需要500MB内存的Java进程时内核会优先回收这些缓存来满足新申请。可问题是回收缓存需要时间而且如果页面是脏页还涉及写盘操作这个过程会明显拖慢分配速度。如果在极短时间内有多个进程抢内存或者被回收的页面里脏页比例太高内核就等不了那么久了它可能直接进入紧凑回收甚至OOM路径。所以在判断“这根VPS还剩多少内存能扛事”时不要被free那列骗了。你真正应该关注的是available它低于总内存的20%时就该当成“内存告急”来看待了。1.2available才是内核自己都认的“余量”估算值available的估算逻辑大致是当前空闲页面 可以快速回收的缓存页面 不在回收列表里的未激活页减去预留水位线之后得到的值。内核在估算时会考虑“如果我现在把这些页交给进程会不会立刻导致系统陷入内存不足”所以available通常比free保守但也更接近真实。我见过不少朋友看到free还有400MB就大胆给JVM设置了-Xmx512m结果一到流量高峰JVM堆扩容到接近上限时瞬间需要向操作系统申请一批连续的大页块。此时内存虽然显示“还有剩余”但可用内存已经见底内核回收不过来OOM Killer立刻动手。跟我之前一个客户的案例一模一样他的VPS看起来 free 450MBavailable 只有约180MB却给MySQL配置了双倍缓冲池高峰期不崩才怪。经验第一条看内存够不够用只看free和available两个值且以available为准。如果你能看到free和available差距很大说明系统里堆了海量可回收缓存属于“虚胖”不是真健康。提示如果available长期低于总内存的 10%~15%就要立刻排查常驻进程别再往里加任务了。2. OOM Killer的真实判定逻辑它不是“快没内存了”才动手2.1 overcommit机制内核先答应最后才发现给不起理解了可用内存之后你还需要知道Linux的“过度分配”策略。默认情况下vm.overcommit_memory0内核在决定是否批准一次内存申请时并不会精确检查当前到底还有多少物理内存而是基于一种启发式规则如果申请的单个块大小不超过一个阈值就大概率批准。这就像银行发信用卡额度之内先授权通过。但你刷的时候银行只检查你的账面额度不会去查ATM里到底有没有那么多现金。等到所有人都来取现时银行才发现现金不够了——这时候就是OOM Killer登场的时候。很多程序被杀不是因为它启动那一瞬间内存不够而是因为它之前“透支”了或者说系统里同时有几张卡都在透支。JVM启动时会一次性声明大块虚拟内存、Node.js的V8堆会预留大量地址空间、线程栈也要做映射……这些在vmstat或ps里可能表现为虚拟内存VSZ巨大但物理内存RSS不高。一切看起来都还好直到所有进程同时进入活跃状态物理内存真的见底系统才会“爆雷”。内核里有一个开关可以彻底关闭这种透支模式vm.overcommit_memory2它表示所有申请都不能超过一个明确比例默认是vm.overcommit_ratio50。但这个模式太严格会直接杀掉很多依赖虚拟内存预留的软件比如JVM、部分数据库。所以我一般不建议你在VPS上无脑改这个参数后面我会再说怎么处理。2.2 watermark、kswapd和压力瞬间为什么内存“还有一半”时可能撞线Linux内核给内存设计了三个水位线min、low、high。当空闲内存低于low水位时内核的后台回收线程kswapd开始异步回收可回收页如果回收速度赶不上分配速度空闲内存继续跌破min水位那当前申请内存的进程就会进入直接回收路径阻塞等待回收。如果连直接回收都救不了就触发OOM。这就是为什么你在free里看还有“一半”程序却突然被杀的原因之一free列显示的空闲页并不等于水位线附近的可分配页。可回收页可能有大量是 dirty 页必须先写回磁盘才能释放写盘一慢分配路径等不了系统直接判定“我顶不住了”。所以一次内存峰值可能不是持续几个小时而是短短几百毫秒OOM Killer就已经完成“审判”了。我用一个实际数字帮你建立体感一台2G内存的VPS默认min_free_kbytes大约是几MB到几十MBlow水位线通常是min的几倍。假设low水位线是 80MB那么当可用内存掉到 80MB 以下时内核就焦虑了。如果此时一个进程申请 300MB 内存而周围全是不可快速回写的脏页内核就可能直接选择杀掉那个最占内存且优先级最低的进程而不是优雅地等待写盘完成。2.3 杀谁OOM score并不等于RSS最高很多人以为OOM Killer一定杀内存占用最大的进程。其实并不完全对它杀的是oom_score最高的进程。这个分数由几个维度综合决定进程的物理内存RSS占大头但也包括交换空间使用量、页表大小、运行时长、优先级、以及你手动设置的oom_score_adj。举个例子。我的VPS上曾跑着一个常驻Python爬虫内存占用才60MB但它在一个被杀事件里先于一个1.2GB的MySQL进程倒下。原因就是MySQL设置过oom_score_adj-500被主观降低了被杀优先级而爬虫进程没有任何保护RSS不高但评分里“比较能杀”于是被系统挑中了。内核选“牺牲品”的思路不是找最肥的那块肉而是找“所有人都觉得可以牺牲的那块肉”。因此如果你有重要进程请主动调整它的oom_score_adj让它更难被杀同时也给不重要的进程调高一点别让系统在关键时刻选错目标。这一步很多教程都不提但在低配VPS上非常实用。3. 现场复盘一次真实的VPS进程被杀排查过程3.1 第一步永远先看dmesgKernel留下的“遗言”有一次一个朋友半夜找我说他的VPS上跑着一个Ghost博客 MariaDB连续两天MySQL进程在凌晨4点被杀。他说free显示一直有五六百MB完全不知道为什么。我让他做的第一件事非常简单dmesg -T | grep -i -E oom|killed process或者老一点没有-T的系统journalctl -k | grep -i -E oom|killed process输出里一般会有一条类似这样的记录[Wed Jan 4 04:03:21 2023] mysqld invoked oom-killer: gfp_mask0x14200ca(GFP_HIGHUSER_MOVABLE), order0, oom_score_adj0 [Wed Jan 4 04:03:21 2023] [pid] 1024 mysqld 2162936 210418 2162936 147466 0 12345 0 /usr/sbin/mysqld [Wed Jan 4 04:03:22 2023] Out of memory: Killed process 1024 (mysqld) total-vm:2162936kB, anon-rss:210418kB, file-rss:0kB, shmem-rss:147466kB看到Out of memory: Killed process才算确认真是OOM Killer干的不是什么神秘力量。这条日志还会告诉你当时的内存情况total-vm是虚拟内存anon-rss是匿名内存进程私有堆file-rss是映射文件占用的页shmem-rss是共享内存。很多时候MySQL光shmem-rss就占了几百MB这是你ps默认视图里不显示的。在这条日志之后内核通常还会打出一整份内存使用排行榜——所有进程的RSS、swap、页表情况。别小看这份列表它就是当时的“事故现场照片”。3.2 第二步用ps和/proc拆开内存账本确认是OOM之后别急着加内存。要弄清楚到底是谁在凌晨4点把内存吃满了当时我们是这么做的# 按物理内存排序看看当前常驻大头是谁 ps aux --sort-rss | head -20结果发现除了mysqld还有一个单独的php-fpm: pool www进程占了很大RSS而且不止一个是好几个PHP-FPM子进程一起把内存吃满了。再看具体进程状态cat /proc/pid/status | grep -E VmRSS|VmSwap|RssAnon|RssFile|RssShmemRssShmem这里很容易被忽略但它和RssAnon一样会稀释掉整个系统的可用内存。一套流程走下来原因基本浮出水面某个PHP脚本在凌晨跑定时任务时对一个巨大数组做了循环拼接单个FPM进程的匿名内存飙到500MB多个并发FPM进程一起跑直接把available打到0最终内核把评分最高的mysqld给杀了。虽然free里看还有“一半”但那“一半”接近全是缓存和共享页真正能快速变现的可用内存已经归零。3.3 第三步查cgroup别忽略面板给你的“隐形天花板”排查还没结束。朋友这台VPS是某云厂商的轻量服务器这类产品通常在外层有cgroup限制或者面板自带的“内存限额”设置。即便你在系统内看着内存没爆但cgroup的memory.max在规定值进程组里的内存占用一旦触顶也会触发组内OOM。你可以用下面的命令查看自己是否身属某个存在内存上限的cgroupcat /sys/fs/cgroup/memory.max 2/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes如果输出不是max或一个很大的数字说明系统确实有个“隐形天花板”。很多时候VPS面板上写着“1GB内存”实际上给你的memory.max就是1GB而宿主机还有几百MB的页缓存和内核slab算在别的cgroup里。也就是说你看到的free是宿主机视角cgroup 限制才是你应用真正撞上的那堵墙。我当时让他把 cgroup 上限记录和dmesg时间对上发现凌晨4点恰好有一个 cgroup 层级的 OOM 事件被触发。也就是说即使宿主机内存没耗尽他自己的资源组也已经在触线了。这个隐藏因素不查后面加再多内存都可能白加。3.4 复盘结论为什么“还有一半”还是被杀最后综合所有证据结论很清楚那一刻的可用内存并不是显示的一半而是被并发PHP-FPM的瞬时峰值、mysqld的共享内存、cgroup天花板三层因素共同压到了0。从系统调用发起内存申请到内核斩杀进程可能不到1秒你后来看free已经恢复正常于是感觉“明明没满啊”。复盘的价值就在这里如果你在事后只看free会觉得莫名其妙但如果把dmesg日志、ps快照、cgroup上限三点对齐就能还原出完整的死亡过程。下次遇到类似情况先做这一步问题通常能定位到具体某类进程或某个定时任务。4. 排查中发现的内存陷阱清单除了OOM Killer还有谁在背后下黑手4.1 内存碎片导致的分配失败free不少但连续难求OOM日志里有一类关键信息gfp_mask0x14200ca(GFP_HIGHUSER_MOVABLE), order0。这个order是分配阶数order0表示单页分配order4代表想申请16个连续页等等。如果某个驱动或程序需要的是高阶分配比如大块连续DMA内存而物理内存已经碎片化可能出现“空余总量足够但找不到连续的一段”的情况。可以用下面的命令查看碎片cat /proc/buddyinfo如果低阶前面几列空闲页很多但高阶靠后几列非常少说明碎片化严重。VPS的虚拟化环境里这种问题相对少见但某些数据库或Java大对象分配还是可能踩中。处理碎片的方法通常是触发内存压缩、调整vm.compact_memory、或者对特定业务降低单次大块申请的需求。这里要提一句如果你看到dmesg里出现“page allocation failure”而不是“out of memory”不要当成普通OOM它是一个独立问题。我见过有人因为某驱动反复请求高阶内存失败最终整个虚拟化平台上的其他租户都被连累。尽管这种情况不多但值得你记住这个排查方向。4.2 内核内存与slab那部分开销不会出现在寻常的“used”里free输出的表格里buff/cache中包含了一部分Slab。Slab是内核用来管理对象比如inode、dentry、文件系统元数据的内存池。当一个VPS上存在大量小文件、大量连接时Slab内存会悄悄涨起来。它既不像普通进程RSS那么显眼又不能像page cache那样随意回收。具体排查命令是cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaimSReclaimable表示可回收的slab比如文件系统缓存SUnreclaim不可回收比如内核自身的一些数据结构。如果SUnreclaim持续高位说明有问题例如大量TCP连接、路由表、或者某个模块泄漏。我在一台低配VPS上见过/proc/meminfo里的SUnreclaim高达300MB一查发现是内核里某种连接跟踪表爆炸导致的。这部分内存不会以“某个进程”的形式出现在top里但它的总量实实在在地挤压了可用内存。4.3 运行时堆外内存Java、Nginx、数据库的隐藏开销很多人给JVM设置-Xmx512m就以为Java进程最多吃512MB内存其实完全错了。JVM除了堆还有元空间Metaspace、线程栈、JIT编译产物、DirectByteBuffer堆外内存、GC用的额外结构等。尤其像Netty、Kafka这类大量用堆外缓冲的组件DirectMemory可以轻松超过堆大小。我见过一台VPSJava进程-Xmx256m结果RSS稳定在700MB就是因为用了大量堆外内存。排查时可以用# JVM顺手看一下堆外 jcmd pid VM.native_memory summary # 如果没有开启NMT就先看进程的RSS和堆对比 ps -o pid,rss,vsz,cmd -p pid同理Nginx的directio、OpenSSL的异步引擎、MySQL的innodb_buffer_pool_size、Redis的持久化子进程都是“隐藏内存大户”。如果只盯着进程列表看很容易漏掉它们对宿主机的实际消耗。4.4 cgroup/容器内存限制的确定性杀进程第三节提到过cgroup的“隐形天花板”但这里我要再强调一次因为它太容易踩了。现在主流VPS面板、Docker、Kubernetes底层都依赖cgroup做资源隔离。Docker里看宿主机free永远有“剩余”但容器内进程到了memory.max就被杀这不叫“宿主机内存不足”而是容器内存超限。如果你用的是systemd管理的服务也可以用systemd-cgtop看看当前服务的cgroup内存占用。K8s里则看kubectl describe pod nameEvents里经常会出现OOMKilled的记录这通常是容器limit设定了而不是节点压力。遇到这种情况第一反应不是调内核参数而是调整容器的resources.limits.memory和应用自身的堆内存配置让两者匹配。4.5 小VPS上swap策略的“阴谋”还有一类被隐藏的问题是swap策略。很多云厂商默认给你配置一个特别小的swap或者完全没有swap。当物理内存紧张时内核会希望把一部分冷数据换到swap里去腾出物理内存。如果swap太小或关闭内核就只能直接回收或走OOM。所以当你发现free里 Swap 那一行全是0B时说明你完全没有缓冲余量。即便你觉得“程序不换页更好”在内存紧张的VPS上一个略大于物理内存的swapfile能为你赢得宝贵的反应时间。我一般建议在内存低于2G的VPS上创建2G的swap但注意别无脑依赖它——如果swap使用量持续上升那说明真实内存需求已经超出物理容量加内存才是正解swap只是保命手段。提示别把vm.swappiness一调到底默认60在低内存VPS上可以但如果你的SSD I/O很弱过高的swappiness会导致卡顿和延迟飙升。5. 按这套组合拳配置VPS内存程序才活得久5.1 监控先行至少要让自己在被杀前看到趋势没有被杀之前你不会意识到监控多重要。我给自己的VPS起手式是装一套轻量监控数据落到Prometheus node_exporter告警走企业微信或Telegram Bot。如果不想搞这么重那就用netdata装完打开网页就能看实时趋势。至少要做到每天能看到MemAvailable、Swap、dmesg中OOM事件、进程RSS Top10 的变化曲线。为什么一定要看趋势因为很多OOM发生在凌晨流量高峰或定时任务期间你睡着了没有趋势图就只能事后猜。我当时给朋友的VPS接上监控后发现连续一周每天4点都有一次MemAvailable暴跌到几十MB的尖峰对应的是他自己的备份脚本。趋势比单点快照可靠得多。5.2 内核参数的具体调整方法和边界很多人听说调内核参数能防OOM就一股脑往上怼。我的建议是克制只碰几个真正有效的。第一个是vm.min_free_kbytes它决定内核为关键路径保留多少不可分配的内存。在低配VPS上如果你不想频繁进入内存紧张状态可以适当调高一点sysctl -w vm.min_free_kbytes65536设置成64MB对大多数VPS算合理。这会牺牲一部分可用内存但能减少内核因为低水位导致的高压力回收和OOM概率。我见过个别机器因为默认 min 太小比如只有20MB内核几乎每次内存申请都走直接回收系统卡成PPT调大之后明显好转。第二个是vm.vfs_cache_pressure。默认100表示内核倾向于尽快回收dentries/inodes缓存。如果你的VPS上文件很多、inode压力大可以降低sysctl -w vm.vfs_cache_pressure50这样能保留更多文件系统缓存减少反复加载元数据的I/O。但别调太狠太低会导致内存里堆很多无效的dentry造成内存浪费。第三个是vm.swappiness。如果SSD的VPS配了swap我建议设成10~30让内核只在必要的时候才换页sysctl -w vm.swappiness20如果完全没有swap这个参数其实没有意义请忽略。最后vm.overcommit_memory要保持默认除非你有很强的理由。乱改overcommit_memory0或2的行为往往会让一些依赖虚拟内存预留的软件比如JVM直接启动失败或者触发更离奇的故障。我踩过一次坑为了“防止过度分配”把系统改成overcommit_memory2结果应用全部申请不到足够虚拟内存全线重启。后来我意识到VPS场景下真正要注意的是监控和进程侧约束而不是靠内核参数硬撑。5.3 程序侧的真实配置建议Java、Node、数据库程序侧配置不当才是大多数VPS OOM的根源。给Java系应用一个通用建议容器/系统内存多少-Xmx只给它的60%~75%同时显式设置MaxMetaspaceSize并限制DirectMemory。java -Xms128m -Xmx384m \ -XX:MaxMetaspaceSize128m \ -XX:MaxDirectMemorySize128m \ -jar app.jarNode.js应用则通过环境变量限制老生代堆大小export NODE_OPTIONS--max-old-space-size512数据库方面MySQL/MariaDB的innodb_buffer_pool_size别超过可用内存的50%——不是理论值50%是参考了其他业务模型之后的保守估计。Redis如果不做持久化尽量不用save如果用注意fork时子进程可能瞬间翻倍消耗内存。另外把所有服务都改成systemd托管并给每个服务设置MemoryMax限制和OOMScoreAdjust。这不会阻止杀进程但可以让内核优先杀不重要的服务还能让systemd自动重启[Service] MemoryMax600M OOMScoreAdjust200 Restarton-failure RestartSec55.4 被OOM杀掉的兜底方案systemd自动重启资源预留无论怎么优化低配VPS总有“防不胜防”的时候。所以我会给所有关键服务做两层兜底。第一层设置Restartalways或Restarton-failure保证OOM杀掉进程后systemd能拉起它。很多面板服务不会自动重启所以你会经历“凌晨被杀白天发现网站打不开”的惨剧。有了systemd自动重启至少服务能在几秒内恢复。第二层用cgroup或systemd的MemoryHigh和MemoryMax做软硬限制。MemoryHigh是用来提示“超过这个值就开始回收/压缩”的软线MemoryMax是硬线超过就触发OOM。让一个不重要的服务先被限制总好过它把重要服务挤死。配合OOMScoreAdjust就能精确控制杀进程时的优先级。注意MemoryMax设置后如果服务本身所需内存确实超过限制它会频繁被杀导致不可用。所以先监控再限制别凭感觉拍脑袋设值。最后分享一个我自己的小习惯。凡是跑关键服务的VPS我都会写一个简单的巡检脚本每小时检查一次MemAvailable如果低于阈值就通过Webhook通知我同时保留dmesg最近一小时OOM记录。再次遇到“内存一半却被杀”的诡异现象你可以注销掉“怀疑人生”的阶段直接从日志和趋势图里找到真凶。加内存前先让系统自己说话。
返回列表