Linux 的 overcommit 政策控制某些虚拟内存承诺是否可以获准,和分配常驻物理页、选择 OOM 牺牲进程、执行容器内存限制并不是同一件事。一次成功申请可能早于实际物理需求,但这不表示所有分配都没有成本,也不保证后续访问在任何资源约束下都能成功。分析时应把承诺、驻留和故障处理分别记录,而不是都称为“剩余内存”。

虚拟承诺不同于实际驻留

malloc 等分配器可能通过 brk 或 mmap 取得内存,也可能直接复用已有内存区,不发起新的系统调用。匿名页通常按需填入,但地址空间元数据、页表以及实际缺页过程仍然消耗资源。具体成本取决于应用申请什么、访问什么,不能把延迟分配简化成近乎免费地运行整个进程。

Committed_AS 是记账数量,不是所有进程常驻集之和,也不是当前内存压力读数。映射类型会影响成本:私有可写映射可能在页面尚未全部驻留时就形成承诺,普通共享或只读文件映射则采用不同规则。因此,虚拟大小很大的进程未必就是物理内存短缺的责任方,应同时查看实际使用和对应记账范围。

三种准入政策

模式零是默认启发式政策,拒绝明显超额的申请,同时允许超过简单常驻内存预算的分配。它的判断不等于为每个获准字节准备专属 RAM。模式一跳过 overcommit 准入检查,有助于某些稀疏工作负载,但地址空间上限、映射数量以及其他分配失败仍然存在,不能描述成任何荒谬请求都会被无条件接受。

模式二执行更严格的承诺记账。名义 CommitLimit 来自交换空间加上配置的内存额度:比例计算会扣除预留给 hugetlb 的内存,非零 overcommit_kbytes 则用绝对额度代替比例。内核保留量与调用上下文还会影响实际准入。因此,CommitLimit 与 Committed_AS 的差值是有用背景,不是保证任意调用者都能一次申请成功的最大值,也不是当前可用物理内存。

需要记账的分配如果无法纳入预算,可能立即返回 ENOMEM,使部分容量问题更早暴露,包括 fork 和后续改变映射权限所需的承诺。fork 记账与具体映射及写时复制义务有关,并不是简单把父进程常驻集乘二。有些应用有意保留稀疏地址空间,因此政策变化可能影响启动,即使它们预计不会触碰所有页面。

严格记账不是 OOM 免疫

模式二降低一类超额承诺风险,却不会覆盖 cgroup 内存限制、NUMA 约束、其他内核分配以及所有后续需求。任务仍可能遇到内存分配失败或 OOM 事件。不能承诺选了模式二,某个业务工作进程就绝不会被杀,更不能把这个模式当成服务可靠性的单一开关。

OOM 牺牲者选择属于另一套政策,要结合事件范围与候选资格理解,并非随机惩罚,也不是 CPU 调度器决定谁来买单。被杀进程不一定是最后申请内存的任务。判断原因之前,应查看事件属于宿主还是某个 cgroup,并结合相关压力与分配证据,不能只凭日志里的进程名称就完成责任归因。

容器共享宿主内核,不应假定每个容器都有自己独立的 vm.overcommit_memory 政策;容器内存控制又增加另一条边界。虚拟机有自己的客户机内核记账,但宿主后备内存和气球等机制还可能增加约束。比较预算前,应先标明每个读数属于哪一层,避免拿宿主 CommitLimit 去解释某个容器的完整可用额度。

坏掉的样子

工作负载可以成功保留内存,随后随着真实需求增长进入回收、交换或 OOM。模式零和模式一都不保证故障一定慢慢发生,不保证有交换空间,也不保证由某个固定进程被杀。应把业务需求和延迟与真实回收、内存压力证据对齐,而不是由模式编号直接猜测故障形态。

相反,模式二可能在物理内存看似仍有空闲时拒绝申请,因为承诺预算已经占满。这是 ENOMEM 的一个可能原因,不是仅凭错误就得出的诊断;进程资源限制、映射过多和应用配置也需要检查。一次启动失败与一次运行中被杀代表不同观察,不应混成同一条调参建议。

名义余量很大,不证明比例太保守;余量很小,也不自动意味着应增大额度。预算变大会放行更多未来可能竞争真实资源的需求。应根据业务要求与恢复余量做规划,保留配置上下文,并先在非生产环境验证变化。只读报告可以帮助提出问题,但不应擅自替操作者决定扩大或缩小系统承诺。

快速验证

保存为 overcommit_probe.py,运行 python3 overcommit_probe.py。程序只读取列出的 proc 文件,打印实际模式和记账字段,同时执行合成解析检查。它不保留巨大内存、不触发 OOM,也不自动建议修改参数。读不到必要文件时明确跳过,而不是把权限错误文本里的数字当作有效内存数据。

from pathlib import Path
import re


def read_value(path):
    try:
        return Path(path).read_text(encoding="utf-8").strip()
    except OSError as error:
        print("SKIP", path, type(error).__name__)
        return None


def meminfo_kb(text):
    return {name: int(value) for name, value in re.findall(
        r"^(MemTotal|SwapTotal|CommitLimit|Committed_AS):\s+(\d+)\s+kB\s*$",
        text, re.M)}


def report(mode, info):
    names = {"0": "heuristic", "1": "always overcommit", "2": "strict accounting"}
    lines = ["mode: " + names.get(mode, "unknown or unreadable")]
    limit, committed = info.get("CommitLimit"), info.get("Committed_AS")
    if limit is not None and committed is not None:
        lines.append("nominal commit headroom: %s kB" % (limit - committed))
    else:
        lines.append("commit headroom unavailable")
    if mode == "2":
        lines.append("Commit accounting can reject chargeable requests; other limits still apply")
    else:
        lines.append("CommitLimit is not the enforced mode-2 ceiling in this mode")
    lines.append("This snapshot is not available RAM, a cgroup budget, or OOM immunity")
    return lines


fixture = meminfo_kb("CommitLimit: 100 kB\nCommitted_AS: 0 kB\n")
assert "nominal commit headroom: 100 kB" in report("2", fixture)
assert meminfo_kb("CommitLimit: bad kB\nCommitted_AS: 7 MB\n") == {}
assert "commit headroom unavailable" in report("2", {})
assert report("9", {})[0] == "mode: unknown or unreadable"
for mode in ("0", "1", "2"):
    assert "OOM immunity" in report(mode, fixture)[-1]

mode = read_value("/proc/sys/vm/overcommit_memory")
ratio = read_value("/proc/sys/vm/overcommit_ratio")
kbytes = read_value("/proc/sys/vm/overcommit_kbytes")
text = read_value("/proc/meminfo")
if mode is None or text is None:
    print("SKIP live overcommit report; synthetic checks passed")
else:
    info = meminfo_kb(text)
    print("overcommit_memory=%s overcommit_ratio=%s overcommit_kbytes=%s" % (mode, ratio, kbytes))
    print("Observed kB fields:", info)
    for line in report(mode, info):
        print(line)

预期得到实时快照,或明确的 SKIP 信息;已提交量为零仍是有效值。修订后的示例已在 Linux、Python 3.12.14 下通过,合成断言覆盖错误格式、缺失字段和三种模式的解释。读取计数没有验证真实分配拒绝或恢复行为;这些仍需要另外设计受控实验,不能为了检查政策而在共享机器上制造内存耗尽。

延伸阅读

来源