数据库及高吞吐服务往往因此受益,但要背负需要连续内存、池子要预先算好大小的成本——这是一笔架构账,不是免费的。
为什么 4 KB 会吃亏 默认页是 4 KiB,TLB 能容纳的条目又很有限。管理百万级工作集的大型应用,很容易在访问一个页面时就 miss 掉一个 TLB 表项,于是不得不到页表里去翻。这里的成本不是页本身,而是反复的页表遍历;一旦工作集的错杀超过 TLB 回填的速度,大应用就会明显吃到痛。更大的页让每条表项覆盖更多内存,同样大小的工作集约只需要很少的表项,这正是吃内存沉重的负载最需要的缓解。
hugetlb 与 THP 的区别 想要大页有两条路。hugetlb 让你从一块预留的内存里自己分配,页的大小是确定的(常见 2 MiB 或更大),行为可预期、由你掌控;transparent(THP)则在后台自动把映射整合成大页,省心,却可能在某些负载上带来延迟尖峰、碎片与写时复制的问题。对延迟敏感的业务,hugetlb 让你自己抱住那份池和那份 trade-off,而不是把决定权丢给一个你不知道用不用得着的通用策略。选哪条路,其实是在决定「可预测性由谁掌握」——你,还是内核。
池子要提前量 大页的真正收益,建立在它被真用满、并且真的常驻内存的前提上。一页 2 MiB 若只用了很小一部分,却仍占着一个整页表项,被锁住的那块内存就等同于浪费,而池子到底定多大,才是真正的功夫。给池子只留那份最诚实、最稳定使用的内存,而不是凭感觉很轻率地划一大块,并要为「稀疏大页」附带的多占 RAM 留出余量。当这些都成立——页被用满、工作集收得小、居住集成一两个表项——大页的收益才完整兑现;要是把它喂给几乎填不满的负载,那就只是一笔平白消耗。
坏掉的样子 池子给小了,失败多在启动瞬间。数据库要的共享缓冲超过预留大页能覆盖的范围,进程会直接退出,常见报错是无法分配内存;此时系统空闲内存可能还很充足,容易误判。关键是显式大页不能自动回退到小页,缺几页就是缺几页。做法是按应用文档的需求加少量余量定池子,重启后核对总页数与已预留数,而不是只看启动参数。
启动正常、运行中分配失败,多半是碎片。大页要求物理连续的整块区间,机器跑久之后,空闲总量充足,却拼不出完整区间,新映射会被拒绝。典型信号是空闲大页数停在大于零不再下降,同时内核日志出现大页分配失败;此时重试映射没有意义,应回到启动阶段预留或用启动参数固定池子。
更隐蔽的是位置放错与自动整理。池子落在远端节点上,每次命中都要跨互联访问,省下的表项优势会被延迟吃掉,绑定前要先看分节点分布。透明大页则是另一种风险:后台整理与写时复制拆分会把平稳的尾延迟打成尖峰,从合并等待与整理线程占用能看到痕迹。延迟敏感服务建议设为按需启用,只在实测有效的地方打开。
快速验证 下面脚本只读内存信息与系统页大小,不做任何分配,也不需要特权,因此在无特权容器里同样可直接运行。若环境没有内存信息文件或不支持大页,它会打印跳过提示并继续给出页表项换算,而不是抛异常退出。
把下面代码保存为大页检查脚本并运行,即可看到本机覆盖率与固定工作集的表项对比。脚本先解析总页数、空闲数、已预留数、溢出数与单页大小,算出预留覆盖总量;再用系统调用得到的基准页大小,对比同样工作集在基准页与大页下各需多少表项,估算省下的数量。
#!/usr/bin/env python3 """Report hugepage coverage and estimated TLB entries saved (read-only).""" import os import re from pathlib import Path WORKING_SET_BYTES = 8 * 1024**3 # 8 GiB example working set def parse_meminfo(text): vals = {} for key in ("HugePages_Total", "HugePages_Free", "HugePages_Rsvd", "HugePages_Surp", "Hugepagesize"): m = re.search(r"^" + key + r":\s+(\d+)", text, re.M) if m: vals[key] = int(m.group(1)) return vals def main(): vals = {} try: text = Path("/proc/meminfo").read_text(encoding="utf-8") except OSError as exc: print(f"SKIP: /proc/meminfo unavailable ({exc}); showing page-table math only") text = "" if text: try: vals = parse_meminfo(text) if "HugePages_Total" not in vals or "Hugepagesize" not in vals: print("SKIP: no HugePages fields in /proc/meminfo (hugepages unsupported or disabled)") else: size_kb = vals["Hugepagesize"] total = vals["HugePages_Total"] free = vals.get("HugePages_Free", 0) rsvd = vals.get("HugePages_Rsvd", 0) surp = vals.get("HugePages_Surp", 0) cov_mb = total * size_kb / 1024 print(f"Hugepagesize: {size_kb} kB") print(f"HugePages total/free/rsvd/surp: {total}/{free}/{rsvd}/{surp}") print(f"Reserved coverage: {cov_mb:.1f} MiB ({cov_mb/1024:.2f} GiB)") except Exception as exc: print(f"SKIP: failed to parse /proc/meminfo ({exc})") try: base = os.sysconf("SC_PAGE_SIZE") except (AttributeError, ValueError, OSError): base = 4096 huge = vals.get("Hugepagesize", 2048) * 1024 base_entries = WORKING_SET_BYTES // base huge_entries = WORKING_SET_BYTES // huge print(f"Base page size (sysconf): {base} bytes") print(f"8 GiB needs ~{base_entries} base-page entries vs ~{huge_entries} huge-page entries") print(f"Estimated TLB entries saved: ~{base_entries - huge_entries}") if __name__ == "__main__": main() 在支持大页的系统上,预期能看到单页大小、各类计数与预留覆盖容量,以及两种粒度各自的表项数和节省量;在笔记本或极简容器里,则先看到一行跳过提示,再看到同样的表项换算,且本地基准页可能不是常见的四千字节。该脚本已在缺少内存信息文件的环境验证通过。它只衡量预留量与潜在节省,不能证明真实缺失下降、碎片或节点距离,调池子前还要用内核日志、分节点计数与延迟分位交叉确认。
...