应用启动、创建子进程或加载大文件时突然报 Cannot allocate memory,但执行 free -h 还能看到几GB的 available。因为整台服务器看起来并没有用满,最常见的处理是清缓存、加Swap或重启服务。
这些操作可能让应用暂时恢复,却没有解释这次内存申请为什么被拒绝。Linux中的 ENOMEM 不只表示物理内存耗尽。进程所在的cgroup已经达到上限、虚拟地址空间受限、严格内存承诺不足,甚至内存映射数量达到上限,都可能返回同类错误。
这篇按照失败现场、系统证据、局部限制、最小修改和结果验证来排查。先确认哪一次申请失败,再决定应该扩容、改限制还是修应用。

先保留失败进程和准确时间
第一步不是执行 sync; echo 3 > /proc/sys/vm/drop_caches,而是记录报错时间、进程ID、服务名、容器名和触发动作。启动失败的systemd服务可以先查看本次日志:
sudo systemctl status example.service
sudo journalctl -u example.service --since '-20 minutes' --no-pager
仍在运行的进程记录PID后,保存它的状态和内存数据:
pid=1234
ps -o pid,ppid,user,stat,%cpu,%mem,rss,vsz,cmd -p "$pid"
cat /proc/$pid/status
RSS 大致反映当前驻留在物理内存中的页面,VSZ 表示进程使用的虚拟地址空间规模。应用申请了一大片虚拟地址空间但尚未真正使用时,RSS可以不高;此时只根据进程实际占用判断,也可能漏掉地址空间或内存承诺限制。
还要确认错误发生在主进程、PHP子进程、数据库、编译器,还是由它创建的新进程。Shell有时只把底层 fork 或 malloc 失败统一显示为Cannot allocate memory,真正失败的位置应结合应用日志判断。
先排除整机内存压力与OOM Kill
在故障仍然存在时连续保存几次系统数据:
free -h
vmstat 1 10
cat /proc/pressure/memory
grep -E 'MemAvailable|SwapFree|CommitLimit|Committed_AS' /proc/meminfo
free -h 中的 available 比 free 更有参考价值,因为Linux会利用空闲内存做缓存。vmstat 的 si、so 持续出现,说明系统正在频繁换入换出;/proc/pressure/memory 的 some 或 full 持续升高,则说明任务因内存回收发生等待。
随后检查内核是否真正触发过OOM:
sudo journalctl -k --since '-30 minutes' --no-pager | grep -Ei 'out of memory|oom-kill|killed process'
发现 Killed process 时,应继续记录被杀进程、当时内存占用、触发cgroup和调用栈。没有OOM日志也不能证明内存申请一定成功,因为内核可能在进入OOM Killer之前就因局部限制返回失败。
整机OOM、cgroup OOM和单次内存申请返回ENOMEM是三种不同现场,修复方式不能混用。
检查cgroup和进程自身的资源限制
Docker、Kubernetes和systemd服务都可能通过cgroup限制内存。宿主机还有大量可用内存,不代表某个容器或服务仍有额度。
先查看进程所属cgroup:
cat /proc/1234/cgroup
findmnt -t cgroup2
把PID替换成失败进程。使用cgroup v2时,结合返回路径找到对应目录,再读取:
cat /sys/fs/cgroup/path/to/group/memory.current
cat /sys/fs/cgroup/path/to/group/memory.max
cat /sys/fs/cgroup/path/to/group/memory.high
cat /sys/fs/cgroup/path/to/group/memory.events
memory.max 是硬上限,值为 max 表示未在这一层设置固定上限;memory.current 是当前使用量。memory.events 中的 max、oom 和 oom_kill 计数可以帮助确认该组是否曾触碰上限。还要检查父级cgroup,因为子组未限制,不代表上层没有限制。
systemd服务可以先用更直接的方式查看:
systemctl show example.service -p MemoryCurrent -p MemoryHigh -p MemoryMax -p ControlGroup
Docker环境则以容器实际配置和cgroup文件为准,不要只看Compose文件。配置已经修改但容器没有重建时,运行中的限制可能仍然是旧值。
确认硬上限过低后,最小改动是根据应用峰值和宿主机容量调整该服务或容器的限制,并保留其他服务需要的内存。直接取消全部上限,可能让一个异常进程拖垮整台服务器。
Linux资源限制也能让内存申请失败。先读取当前进程的实际限制:
prlimit --pid 1234
cat /proc/1234/limits
重点关注地址空间、数据段、锁定内存和进程数。Linux手册说明,RLIMIT_AS 限制进程的虚拟地址空间,超过时 brk、mmap 等调用可能以 ENOMEM 失败;RLIMIT_DATA 也会影响堆和部分内存映射。
通过SSH终端执行的 ulimit -a 只显示当前Shell及其子进程的限制,不一定等于systemd服务的限制。服务应结合以下结果判断:
systemctl show example.service -p LimitAS -p LimitDATA -p LimitMEMLOCK -p TasksMax
systemctl cat example.service
不要把所有限制统一改成infinity。先确认失败申请需要多大空间、应用正常峰值是多少、限制最初为何存在。修改systemd单元后需要执行配置重载并重启对应服务,旧进程不会自动获得新的资源上限。
查看严格内存承诺是否已经不足
Linux可以允许应用承诺比当前物理内存更多的虚拟内存,也可以执行严格核算。先保存当前策略:
sysctl vm.overcommit_memory
sysctl vm.overcommit_ratio
grep -E 'CommitLimit|Committed_AS' /proc/meminfo
当 vm.overcommit_memory=2 时,系统采用较严格的内存承诺策略。内核文档给出的 CommitLimit 会结合可用RAM、Swap和 overcommit_ratio 计算;Committed_AS 则反映系统已经承诺给进程的内存规模。后者接近或超过限制时,即使当前RSS没有占满,新申请也可能被拒绝。
这里最容易犯的错误,是直接把 vm.overcommit_memory 改成1,让内核始终乐观接受申请。这样可能绕过本次拒绝,却把风险推迟到真正访问页面时,最终由OOM Killer终止一个或多个进程。
正确处理要回到业务。数据库、Java程序或科学计算任务是否一次保留了过大的虚拟空间,Swap是否符合负载特点,严格策略是否为关键业务有意设置,都需要确认。策略调整属于系统级变化,应先在相同负载的测试环境验证,并准备回滚参数。
检查内存映射数量和程序位数
Linux的 malloc 手册还指出,进程创建的内存映射数量超过 vm.max_map_count 时,也可能返回 ENOMEM。先比较当前映射数与系统上限:
wc -l /proc/1234/maps
sysctl vm.max_map_count
映射数量逼近上限时,继续确认为什么应用产生大量映射。某些搜索、数据库、监控或高并发程序本来就需要较多映射;应用泄漏、频繁加载文件或线程模型异常也可能让数量不断增长。只把上限调大而不观察增长趋势,可能掩盖泄漏。
还要确认程序架构:
file /proc/1234/exe
32位进程可使用的虚拟地址空间远小于64位进程,整台服务器有大量内存也无法突破进程自身的地址空间边界。此时增加物理内存通常没有直接效果,应评估64位程序、单进程数据规模或工作进程拆分方案。
不要把清缓存和加Swap当成通用答案
drop_caches 主要释放可回收的文件缓存,不会修复cgroup硬上限、RLIMIT_AS、映射数量或应用泄漏。频繁清缓存还可能让随后读取重新访问磁盘,导致性能短时间变差。
Swap能为匿名内存提供缓冲,降低突发压力下立即OOM的概率,但它不是物理内存的等速替代。数据库或高延迟敏感服务大量使用Swap后,虽然进程没有退出,响应时间可能已经无法接受。cgroup还可能单独设置Swap上限,宿主机有Swap也不代表该容器可以使用。
确认应用真实工作集长期超过服务器容量时,扩容或迁移才是合理处理。可以在 萤光云 建立相近配置验证内存峰值,也可以通过按小时计费的 LightNode 做同版本并行压力测试。测试环境应使用脱敏数据,并保持相同cgroup、进程限制和内核参数,否则对比结果没有参考价值。
按证据选择最小改动
证据指向整机内存长期不足时,先修复异常增长,再评估增加内存或降低并发;证据指向cgroup硬上限时,只调整对应服务并给宿主机保留安全余量;证据指向RLIMIT时,修改服务实际继承的限制,而不是当前SSH窗口的 ulimit。
严格内存承诺不足时,先核对应用承诺规模和业务目的,再评估Swap、应用参数或系统策略;映射数量耗尽时,同时处理 vm.max_map_count 和映射持续增长的根因;32位地址空间不足时,则应从程序架构解决。
每次只改变一类限制,并记录修改前后的数值。重启整台服务器会同时重置进程、释放内存、重新建立cgroup和加载配置,虽然恢复得快,却很难知道到底是哪一个条件造成失败。
验收要覆盖同一触发动作
修复后重新执行最初失败的启动、导入、编译或请求,并在过程中同步采集 free、vmstat、内存压力、cgroup事件和进程限制。应用不再返回 ENOMEM 只是第一层结果,还要确认没有新的OOM Kill、Swap抖动和响应时间恶化。
经过一个业务高峰后,再次读取 memory.events、Committed_AS、映射数量和应用内存曲线。计数不再增长、资源峰值落在预留范围内,服务重启后配置仍然生效,才算完成闭环。
看到available还有内存,只能排除一部分整机耗尽场景。能指出失败发生在哪一层限制,并用修改前后的数据证明恢复,才是可靠结论。
常见问题
1.Cannot allocate memory和OOM Kill是一回事吗?
不是。前者通常表示某次内存或地址空间申请返回失败,进程可以自行处理或退出;OOM Kill是内核在内存压力下选择并终止进程。两者可能相关,但日志证据和处理路径不同。
2.重启服务后恢复,能说明只是偶发问题吗?
不能。重启会释放地址空间、映射和内存,也会重新加载cgroup及资源限制。应比较重启前后的进程数据,并观察相同负载下是否再次接近原来的边界。
3.内存不足时应该先加Swap还是升级内存?
先确认失败层级和工作集。短时匿名内存峰值可能受益于合理Swap,长期活跃内存超过容量则更适合优化应用或增加物理内存;cgroup、RLIMIT和映射上限问题即使增加Swap也不一定解决。


