ARTICLE DETAIL

资讯详情

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

Linux大页与小页:从TLB原理到配置实战

Linux大页与小页:从TLB原理到配置实战 聊到Linux内存管理大页HugePages和小页4KB普通页这对概念几乎是每个做内核、数据库或后端性能优化的人绕不开的门槛。大页的核心思路并不复杂把操作系统默认的4KB内存页放大到2MB甚至1GB从而减少页表条目数量、降低CPU翻译地址时的TLB miss率让高频内存访问延迟降下来。但这背后牵扯到的分页机制、预留方式、透明大页THP的取舍以及实际配置时踩过的坑绝对值得展开好好聊一遍。这篇文章会用从业者的视角把大页与小页的来龙去脉、选型思路、实操步骤和排查心得完整串起来适合正在学Linux内核的开发者、做应用性能优化的人以及被“明明很多内存却分配不出大页”这类问题困扰的运维朋友。1. 大页与小页分页机制的底层逻辑1.1 分页是什么为什么必须有“页”这个东西要理解大页得先回到分页机制本身。现代操作系统不会让进程直接操作物理内存而是给每个进程一张虚拟地址空间再由CPU的内存管理单元MMU完成虚拟地址到物理地址的翻译。翻译过程不是按字节一条条映射而是把内存切成固定大小的块这些块就是“页”。Linux默认页大小是4KBx86-64架构下通常使用四级页表结构PGD、PUD、PMD、PTE。每一页的映射关系最终都要在PTE这一层占一个条目而为了管理这些条目内核又需要分配页表本身的内存。页表这个东西可以类比成一本城市地图。4KB小页相当于地图上把每一条街道都独立标为一个条目2MB大页则相当于把一整片街区合并成一个条目。找路的时候需要翻看的条目越少自然越快。CPU内部有一个叫TLBTranslation Lookaside Buffer的“高速地图缓存”专门缓存最近用到的页表翻译结果。TLB的容量非常有限通常只有几十到几百个条目。当你频繁访问的内存区域很大而每页只能覆盖4KB时TLB很快就装不下了之后每次访问都可能miss然后CPU就得回到内存里去查多级页表这个开销比一次普通缓存miss高出几个数量级。1.2 默认4KB小页在大内存场景下的瓶颈我们来算一笔账。假设一台服务器有64GB内存全部使用4KB页那么页总数是64GB / 4KB 16,777,216页如果每条PTE按8字节计算仅仅PTE这一层就需要约128MB内存再算上高层页表总页表大小超过200MB是很常见的事。更糟糕的是TLB命中率假设一个数据库进程使用了20GB的共享内存且按顺序扫描这些数据TLB需要覆盖的页有20GB / 4KB 5,242,880页这远远超出常见CPU的TLB容量。实际表现就是应用明明在做单纯的内存读取CPU使用率却居高不下服务也没做什么复杂计算但top里的sy时间占比很高。这种情况我就遇到过不止一次排除了业务逻辑问题后用perf一看大量时间消耗在dTLB-load-misses上罪魁祸首就是小页的TLB不够用。1.3 大页的两个分支静态HugePages与透明大页THPLinux下的大页不是单一机制准确说有两类第一类是传统HugePages也叫静态大页。它通过系统启动时或者运行时向内核预留一部分连续物理内存这些内存只能以页池形式存在应用必须显式请求才能使用。它的优点是物理连续性有保证、内存访问路径稳定性能波动很小适合关键业务缺点也很明显预留后其他普通内存分配用不了这部分空间如果预留太大反而可能导致普通内存不足。第二类是透明大页Transparent Huge Pages, THP。它不需要应用感知内核的khugepaged后台线程会自动扫描进程的内存把符合条件的相邻小页合并成大页。它的好处是“零成本”启用坏处则是合并过程会引起轻微的延迟抖动而且管理策略并不总是适配所有负载。数据库领域普遍建议关闭THP原因后面会详细讲。2. 大页方案选型三条路径怎么选2.1 静态预留大页稳定优先的场景如果你追求的是稳定可控的内存访问性能最推荐的就是静态预留HugePages。Linux提供了两个核心参数vm.nr_hugepages控制全局大页数量vm.nr_overcommit_hugepages允许临时超用一部分大页。默认情况下HugePages在启动时通过内核参数hugepagesN预留运行时也可以动态调整但动态调整受内存碎片影响不一定总能成功。我一般这样确认当前状态cat /proc/meminfo | grep Huge输出里最重要的是这几项HugePages_Total系统总的大页数HugePages_Free尚未分配的大页数HugePages_Rsvd已保留但尚未使用的大页数HugePages_Surp超用的大页数只看这些字段还不够还需要看大页的大小cat /proc/meminfo | grep Hugepagesize绝大多数x86-64系统默认大页大小是2MB。为什么不直接用1GB大页因为1GB大页虽然对TLB命中率提升最明显但物理内存必须非常充裕而且大多数场景用不着2MB是一个性价比很高的平衡点。2.2 透明大页THP省心但要留意副作用THP的开关在/sys/kernel/mm/transparent_hugepage/enabled有三个可选值always、madvise、never。简单介绍三者的区别always内核尽量给所有进程自动使用大页madvise只有进程通过madvise(MADV_HUGEPAGE)主动标记的内存区域才使用大页never完全关闭自动大页对数据库和Java应用场景我强烈建议至少使用madvise更稳妥的是直接never。原因在于THP的khugepaged线程会定期扫描进程内存并尝试合并页这个过程会占用CPU还可能在合并或分裂页时导致瞬时停顿。很多线上数据库出现的“时不时卡顿几十毫秒”问题排查到最后都指向THP。MySQL官方文档里也明确建议禁用THP就是因为它对性能延迟特别敏感。临时关闭方法echo never /sys/kernel/mm/transparent_hugepage/enabled当然这只是临时生效重启后还会恢复。要永久关闭最好在/etc/default/grub的GRUB_CMDLINE_LINUX里加上transparent_hugepagenever然后执行grub2-mkconfig -o /boot/grub2/grub.cfg并重启。这里要注意不同发行版的grub配置文件路径可能不同Ubuntu通常是/etc/default/grub加update-grubCentOS/RHEL则需要按上面方式处理。2.3 hugetlbfs文件系统应用如何真正用上大页静态预留的大页最终要通过两种方式交给应用使用。一种是通过mmap系统调用加上MAP_HUGETLB标志另一种是挂载hugetlbfs文件系统应用在文件系统上创建文件并进行映射。后者更直观适合多个进程共享同一块大页内存的场景。挂载方式很简单mkdir -p /mnt/huge mount -t hugetlbfs -o pagesize2M none /mnt/huge如果希望系统重启后自动挂载可以写入/etc/fstabhugetlbfs /mnt/huge hugetlbfs pagesize2M 0 0这里有一个容易被忽略的细节/mnt/huge挂载后目录里没有文件但你不能直接在这个目录里创建普通文件。普通用户需要确认自己的UID有权限访问该目录否则mmap时会报Permission denied。生产环境里我经常看到有人挂载完却忘了chmod 777导致应用启动失败。如果走C语言mmap这条路核心代码大致是#include sys/mman.h #include stdio.h #include stdlib.h #ifndef MAP_HUGETLB #define MAP_HUGETLB 0x40000 #endif int main(void) { size_t length 2 * 1024 * 1024; /* 2MB */ char *addr mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0); if (addr MAP_FAILED) { perror(mmap); exit(1); } /* 访问一部分内存确保物理页被分配 */ addr[0] 1; printf(hugetlb mmap success at %p\n, addr); return 0; }编译时不需要额外链接库直接gcc -o hugetlb_test hugetlb_test.c即可。如果程序崩溃或者mmap返回失败大概率是大页池不够先检查HugePages_Free是否足够。这个例子看起来很基础但它是验证大页生效最快的方式。3. 实操详解在Linux中配置大页的完整流程3.1 第一步看清硬件与环境动手配置前先确认几个信息CPU架构、总内存大小、NUMA节点分布。为什么关心NUMA因为大页内存如果跨了NUMA节点访问远端内存的延迟远高于本地节点性能优化效果会打折扣。查看NUMA拓扑numactl --hardware输出会列出各节点可用的内存范围。假设服务器有2个CPU节点每个节点64GB内存。那么你配置大页时最好是每个节点都分配一部分避免全部集中在一个节点上导致另一个节点的应用访问远端大页。具体做法是用/sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages这样的接口分别对node0和node1设置。如果没有NUMA或者你的应用没有绑定NUMA节点那直接改全局参数就行。3.2 第二步预留大页并验证以2MB大页为例假设要预留1024个大页也就是2GB内存echo 1024 /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages但这种方式在系统运行中做会受到物理内存碎片的影响。如果你看到写入成功但实际大页总数量并没有增加说明内核无法分配到足够连续的物理页。这时候有两个办法一是重启系统在grub内核参数里加上hugepages1024二是在/etc/sysctl.conf里设置vm.nr_hugepages1024重启后大页会在内存初始化阶段就预留好基本不会受碎片影响。设置完可以用这条命令验证cat /proc/meminfo | grep -i huge建议把预期结果和实际结果记录下来。比如参数预期值检查命令HugePages_Total1024cat /proc/meminfoHugepagesize2048 kBcat /proc/meminfo大页池总大小2GBTotal * Hugepagesize这里特别提醒vm.nr_hugepages一旦写入内核会立即尝试分配连续内存。如果分配失败它会悄悄地把HugePages_Total保持为0不会报错。所有“看起来配置了但实际没生效”的问题十有八九都出在这里。3.3 第三步应用侧如何申请大页应用要拿到大页有几种途径。如果是数据库或Java中间件通常配置文件中直接指定如果是自己写代码可以用前面的mmap方式也可以用libhugetlbfs库来透明化加速普通应用程序。还有一个更常见的场景是共享内存比如Oracle数据库它使用System V共享内存时可以设置SHMMAX和SHMALL再让共享内存段落在hugepage池里。比较典型的是Java JVM。JVM参数可以这样加-XX:UseLargePages -XX:LargePageSizeInBytes2m但要注意UseLargePages只代表JVM尝试使用大页是否成功取决于操作系统配置。如果JVM分配失败有时会直接报Unable to allocate large pages这时需要检查大页池是否足够大JVM进程是否有权限访问hugetlbfs或MAP_HUGETLB是否因为overcommit设置导致分配被拒绝对于MySQL这类数据库如果使用InnoDB引擎实际场景里更推荐的是“关闭THP 使用传统HugePages”组合方案因为InnoDB的缓冲池和redo log都有大量连续内存访问静态大页能显著减少TLB miss。不过MySQL对大页的支持不是默认开启需要在配置里设置large_pagesON。3.4 第四步把大页和NUMA绑定组合使用真正的大型生产环境我一般会建议把应用进程用numactl绑定到具体CPU节点然后在该节点上使用对应的大页池。这样能最大化局部性收益。比如numactl --cpunodebind0 --membind0 ./my_app同时只给node0配置大页echo 512 /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages如果应用绑定node0而大页却全部在node1上那么每次访问内存都要跨节点性能可能比不用大页还差。这个坑很隐蔽很多人只看到大页配置成功了没有去核对NUMA节点结果性能提升不明显甚至倒退。4. 指标观测与性能验证方法4.1 如何确认大页真正生效配置大页后不能只看/proc/meminfo里的Total字段还要看应用是否真正用上了大页。一个常见方法是查看/proc/pid/smaps它会详细列出进程每一段内存的映射信息。当某一段内存使用大页时smaps里会出现的项是KernelPageSize: 2048 kB或MMUPageSize: 2048 kB而不是2048kB。具体做法grep -E KernelPageSize|MMUPageSize /proc/$(pidof my_app)/smaps | grep -v 4 kB | head如果全部都是4 kB说明进程没有使用大页即使系统大页池是满的也不代表应用受益。4.2 性能对比的实测方法我在实际调优时喜欢用一个小工具做对比分配一个200MB的缓冲区然后按顺序读写统计耗时。先跑普通页版本再跑大页版本。C语言示例的大致逻辑是这样#include sys/mman.h #include stdio.h #include string.h #include time.h #include stdlib.h #define SIZE (200 * 1024 * 1024) double test_memory(char *buf, size_t size) { struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, start); for (size_t i 0; i size; i 4096) { buf[i] 0x01; } for (size_t i 0; i size; i 4096) { volatile char tmp buf[i]; (void)tmp; } clock_gettime(CLOCK_MONOTONIC, end); return (end.tv_sec - start.tv_sec) * 1000.0 (end.tv_nsec - start.tv_nsec) / 1e6; } int main(void) { char *normal mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); char *huge mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB, -1, 0); if (normal MAP_FAILED || huge MAP_FAILED) { perror(mmap); return 1; } memset(normal, 0, SIZE); memset(huge, 0, SIZE); printf(normal: %.2f ms\n, test_memory(normal, SIZE)); printf(huge: %.2f ms\n, test_memory(huge, SIZE)); return 0; }同样200MB的内存在开启大页后顺序访问的耗时常能降低10%-30%具体幅度取决于机器CPU的TLB容量和主频。如果是随机访问效果就没那么明显了这也是大页不是“万能加速器”的原因。4.3 常见的性能误区误区一“开启THP就等于用上大页了”。THP确实会自动合并内存页但合并是后台异步进行的而且合并后的页也可能被分裂回来。你无法精确控制哪些区域被合并对性能敏感的数据库来说这种不确定性是大忌。误区二“大页越大越好”。1GB大页虽然能最大化TLB覆盖但每页占用1GB连续物理内存不仅分配困难还容易造成资源浪费。如果应用本身只用了几百MB硬上一个1GB大页反而白白占用了一块大空闲区影响其他内存分配灵活性。误区三“只要大页池满了性能就一定好”。大页池里的内存只能通过特定接口分配出去如果应用没有显式请求大页池再满也只是“摆设”。务必通过smaps或/proc/pid/numa_maps确认进程确实在使用大页。5. 常见问题与排查技巧实录5.1 预留大页失败Total始终为0这是出现频率最高的问题。现象是往nr_hugepages里写数字但/proc/meminfo里HugePages_Total纹丝不动。原因基本是内存碎片化或者剩余连续页不足。排查思路# 查看内存碎片情况 cat /proc/buddyinfo这个文件里的数字代表不同页阶的可用块数量。如果高阶比如order9以上对应的数字很小说明连续大内存很难找。解决办法就是改到启动阶段预留用grub内核参数hugepagesN重启。另一个隐藏原因是vm.nr_hugepages写入了但被cgroup的限制拦住了。检查/sys/fs/cgroup下相关控制组的memory.hugepages限制如果有设置会导致大页分配上限被卡住。5.2 THP导致的延迟抖动这个问题在数据库场景极其典型。现象是数据库平均响应时间正常但P99或P99.9偶尔飙高持续时间只有几十毫秒到几百毫秒。如果确认业务没有大流量脉冲就要怀疑THP的khugepaged在后台做了大页合并。排查方法# 查看khugepaged是否在频繁占用CPU ps -ef | grep khugepaged或者在perf top里看到khugepaged占比较高。解决思路就是按前面说的关闭THP或者改用madvise模式把普通进程排除在外只有明确使用MADV_HUGEPAGE的进程才启用大页。我实操过的一个真实案例某MySQL实例在高峰时段每几分钟出现一次约80ms的延迟尖刺排查IO、锁、慢查询都没有问题最后检查/sys/kernel/mm/transparent_hugepage/enabled发现是always。关闭THP后尖刺基本消失P99从40ms降到了约15ms。这个案例后来被我写进了团队的性能调优手册。5.3 大页使用但应用崩溃或OOM如果应用用mmap申请大页时报“Cannot allocate memory”检查大页池是否够大页池的可用数量看HugePages_Free如果Free不足说明没有预留够。还有一个容易被忽略的参数vm.overcommit_memory如果被设置为2禁止超额分配即使大页池还有空余某些大内存申请也会被拒绝因为内核需要确保整个系统的内存不会超额。这种情况下要么把vm.overcommit_memory改回0或1要么单独计算大页预留量确保系统内存足够。Java应用如果启动时指定-XX:UseLargePages且预留不足JVM会直接退出日志里会留下类似错误。解决办法是在系统启动参数或sysctl里把大页预留到满足所有实例叠加的值。5.4 排查指令速查表诉求命令备注查看系统大页总量cat /proc/meminfo关注HugePages_Total查看透明大页状态cat /sys/kernel/mm/transparent_hugepage/enabledalways/madvise/never查看NUMA节点大页分配cat /sys/devices/system/node/node*/meminfo关注HugePages_Total查看进程内存页大小grep -E KernelPageSizeMMUPageSize /proc/pid/smaps动态调整大页数量echo N /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages可能因碎片失败统计TLB missperf stat -e dTLB-load-misses ./program对比开启大页前后数据这个速查表是我日常工作里最常用的部分比翻文档快得多。结尾一些实战体会我个人在实际操作中的体会是大页优化这件事最重要的是“确认收益”而不是“堆参数”。每次配置完我都会先跑一次perf对比开启前后的dTLB-load-misses和耗时数据确认提升幅度值得付出的配置成本。再分享一个小细节如果服务器是多实例部署最好逐个实例确认它们能否共享同一个大页池不要为了省事把所有实例都塞进一个池里否则一个实例内存暴涨会把整个池吃穿。大页用得好是性能利器用不好就是运维噩梦。以上这些经验是我在排查过数次“内存优化后反而更卡”的问题后一点点积累出来的希望你能少走这些弯路。
返回列表