透明代码大页:让数据库也能用上2MB大页!
背景
大页技术是操作系统中优化内存访问延迟的一种技术,其优化原理与CPU TLB硬件有直接关系,而其优化效果不仅受CPU TLB硬件影响,还需要看应用访存特点。只考虑arm和x86两种平台,已知的大页技术包括透明大页、hugetlbfs、16k和64k全局大页。在合适的场景,大页技术可以提升应用性能达10%以上,尤其是针对当前云上应用逐年增长的内存使用趋势,使用大页技术是其中重要的提升“性能-成本”比例的优化手段。透明大页(Transparent Huge Pages,THP)从2011年开始在Linux内核中已经支持起来,其通过一次性分配2M页填充进程页表,避免多次缺页开销,更深层次从硬件角度优化了TLB缺失开销,在最好情况下,对应用的优化效果达到10%左右。除以上优点外,透明大页(主要供堆栈使用)使用过度也会导致严重的内存碎片化、内存膨胀和内存利用率低等问题,这就是当前透明大页没有在数据库中使用的核心原因,只能感叹“卿本巧技,奈何有坑”。
代码大页在透明大页的基础上,将支持扩展到可执行二进制文件,包括进程二进制文件本身、共享库等可执行数据。与透明大页相比,由于代码大页仅将占比较低且有限的可执行文件页部分转换为大页,从根本上避开了内存碎片以及内存不足的问题。与此同时,由于代码类数据和普通堆栈数据访问热度对整体性能影响不同(主要指代码数据或堆栈数据访问缺页一次的性能影响),导致代码类数据使用大页所提升的性能远大于同样分量的透明大页。所以推广和完善代码大页相比透明大页更加简单和容易。
本文主要介绍我们的代码大页方案以及一些实验阶段性能测试。为了方便阅读,在这里简单归纳了一下Linux系统中大页的支持现状和和必要的数据库相关背景。
大页现状
当前Linux内核支持的大页包括THP和hugetlb,其页大小分别是:

不考虑其他架构,在x86和arm架构中,提到THP,我们可以一股脑的认为其大小就是2MB,当前内核暂时还不支持1GB THP,技术上实现没有什么问题,社区隐隐约约也有人曾经发过相关补丁...。回到这里,arm64相比x86,hugtlb多两种大页支持,主要是contiguous bit,该特性主要是针对TLB entry的优化(连续的16个PTE/PMB,若其上的PFN也是连续的,cont bit会将其使用的16个TLB entry优化仅占一个TLB entry)。这里的64KB和32MB的由来就是16*4KB和16*2MB。
另外,在全局页粒度的支持上,arm64也比x86更会玩,提供的16KB(CONFIG_16KB表示)和64KB(CONFIG_64KB表示)两种选择,这两种页相比4KB页,在TLB和cache上都有明显的优化相关,从而优化宏观指标,当然也并非所有benchmark表现出性能提升,例如在SPECjvm2008和stream中,我们就发现有多项指标在使用CONFIG_64KB的时候有较严重的性能下降。除以上问题,还想再啰嗦一下,CONFIG_64KB中,THP的大小着实有点“吓人”,有512MB。
因此,大伙对16KB、64KB还是又爱又恨。
Mysql、PostgreSQL和OceanBase
站在内存管理的角度,我们仅仅关心Mysql、PostgreSQL这些数据库用了多少内存、页缓存占多少、匿名页占多少以及代码段还有iTLB/dTLB miss到底高不高。当然还随便想知道THP有没有优化,下面几个是简单归纳的几点我们关系的数据库特点:
Mysql
- Mysql是一个多线程模式的数据库,其代码段大小一般18M左右;
- THP不敏感,打开THP,大约仅有不到3%的性能提升;
- 跨NUMA敏感,本地虚拟机32核验证跨NUMA抖动在5~7%左右;
PostgreSQL
- 多进程模型,代码段大小大约10M左右;
- 应用iTLB-load-misses较高,大约1.41%左右;
OceanBase
- 多线程模型,代码段大小打印200M~280M;
- 一般独占单机使用,性能验证过程中并发数要求高:128、1000、1500;
- THP本地验证不敏感;
这些数据库大约至少有两个共同点:代码段大、iTLB Miss高。本文也是基于这两个特征进行的优化,当然代码大页优化目标也不局限于这三种数据库。
代码大页
接着前面数据库背景介绍,这里直接开始代码大页方案。
代码大页大致实现分为:整理结构、大致实现、填充功能简介还有最后的代码大页性能评估(包括Mysql和PostgreSQL),最后是我们专门为解决x86平台设计的自适应功能。
整体结构与实现
基于透明大页异步整合大页(主要指khugepaged内核线程)的框架:
上图所展示的代码大页方案主要包括三个部分:
(1)映射首地址对齐(蓝色高亮):这个部分主要是在elf binary和DSO建立映射的过程中,优先考虑分配2M对齐的虚拟地址空间,便于映射到2M大页;
(2)异步khugepaged扫描整合以及加速(橙色高亮):与THP相似,单独设计用户态接口hugetext_enabled控制。复用khugepaged整合2M大页。此外,由于hugetext与THP共用khugepaged,在THP=always时,也能整合部分符合条件的代码大页;关于加速部分,我们解决了在THP使能的场景中,代码段整合慢的问题,这也是我们改进READ_ONLY_THP_FOR_FS带来的挑战之一。
(3)DSO写回退(紫色高亮):对于DSO所建立的映射,内核屏蔽了MAP_DENYWRITE,导致用户态可以写打开共享库文件(尽管一旦对该共享库写,进程多数情况会core dump)。针对这种情况,在检测到该共享库存在写者时,对其pagecache进行清空;DSO为什么有这么多顾虑可以跳转:https://developer.aliyun.com/article/863760
注意:首地址2M对齐的本意是 mmap addr = mmap pgoff (mod 2M),由于elf binary和DSO的可执行LOAD段的pgoff一般为0,这里为了叙述方便,我们简称地址2M对齐。
另外,在开发和测试过程中,我们的实现方案解决了file THP相关的三个bug的补丁,Linux社区已经合入,glibc社区也合入了一个p_align修复补丁。
除以上部分,我们目前已经完成的自适应代码大页已经完成,主要解决x86平台使用代码大页过多的问题。详细见“自适应处理”一节。
代码段填充功能
前面,我们已经描述代码大页方案支持动态链接库(DSO)和二进制文件本身,对于libhugetlbfs方案,其仅仅支持二进制文件本身的大页转换,虽转换比较完全,但是它无法同时让DSO使用大页。显而易见,与libhugetlbfs相比,我们提供的hugetext_enabled方式更加完整,性能优势更大。填充功能主要是我们为弥补在某些场景中,其大量的热点发生在代码段的尾部,libhugetlbfs天生可以做到,所以我们也不得不解决这个问题。
言归正传,回到代码段填充。首先用一张图来表示代码段填充具体是什么。
大多数应用并不能完全保证其代码段都会2M对齐,如上图所示,一个应用第一个r-xp有3.2M,其中2M我们的hugetext_enabled本身即支持,后面的1.2M我们不得不向后填充这个vma,保证其映射大小2M对齐,其填充的内容其实就来自于下一个r--p的数据(r-xp和r--p在ELF磁盘中是两个相邻的PT_LOAD段,具体可以通过readelf观察)。当然我们在填充过程中会判断填充后大小不会超过原文件大小和不会与下一个r--p属性的vma重叠。下面是我们新引入的代码大页填充使能开关,例如将0x1000写入hugetext_pad_threshold,表示需填充内容超过4k时填充功能才会使能。
/sys/kernel/mm/transparent_hugepage/hugetext_pad_threshold
最后再夹带一点私货,以上代码段填充主要是在不重新编译应用程序的情况的一种内核方式,还有一种方式是在应用的链接脚本中加入“. = ALIGN(0x200000)”,将代码段直接按照2M对齐,如此不需要填充或进行填充更加安全。例如以一个简单的测试程序align.out为例。设置链接脚本后Section出现对齐:
$ readelf -l align.out
Elf file type is EXEC (Executable file)
Entry point 0x400520
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001b 0x000000000000001b R 0x1
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000200194 0x0000000000200194 R E 0x200000
LOAD 0x0000000000400000 0x0000000000a00000 0x0000000000a00000
0x0000000000010040 0x0000000000200008 RW 0x200000
DYNAMIC 0x0000000000400010 0x0000000000a00010 0x0000000000a00010
0x00000000000001d0 0x00000000000001d0 RW 0x8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x000000000020004c 0x000000000060004c 0x000000000060004c
0x000000000000004c 0x000000000000004c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000400000 0x0000000000a00000 0x0000000000a00000
0x0000000000010000 0x0000000000010000 R 0x1
Section to Segment mapping:从上面readelf文件输出可以看出:两个PT_LOAD的offset已经按照2M对齐,并且两个PT_LOAD完全分布到不同的2M页中,如此,若进行填充,完全不需要用后一个PT_LOAD数据。测试方法:导出默认并修改默认的lds文件,在代码和数据段前加上". = ALIGN(0x200000);"即可。备注:“gcc -Wl,--verbose”可查看默认的链接脚本。另外,在编译过程中需要加上“-z max-page-size=0x200000”选项,否则对齐会被约束为4K或64K,如下:
$ gcc main.c test.c -o align.out -Wl,-T ld-align.lds -z max-page-size=0x200000链接脚本加hugetext_pad_threshold结合使用,可以让应用对代码大页使用更加友好。
快速试用
为了方便业务使用,我们扩展了两种打开方式:启动参数和sysfs接口:
- 启动参数
打开:hugetext=1 or 2 or 3
关闭:缺省即为关闭
- sysfs接口
仅打开二进制和动态库大页:echo 1 > /sys/kernel/mm/transparent_hugepage/hugetext_enabled
仅打开可执行匿名大页:echo 2 > /sys/kernel/mm/transparent_hugepage/hugetext_enabled
打开以上两类大页:echo 3 > /sys/kernel/mm/transparent_hugepage/hugetext_enabled
关闭:echo 0 > /sys/kernel/mm/transparent_hugepage/hugetext_enabled
查看/proc/<pid>/smaps中FilePmdMapped字段可确定是否使用了代码大页。
扫描进程代码大页使用数量(单位KB):
cat /proc/<pid>/smaps | grep FilePmdMapped | awk '{sum+=$2}END{print"Sum= ",sum}'- padding接口
/sys/kernel/mm/transparent_hugepage/hugetext_pad_threshold
echo [0~2097151] > /sys/kernel/mm/transparent_hugepage/hugetext_pad_threshold
当二进制文件末尾剩余text段由于不足2M而无法使用大页,当剩余text大小超过hugetext_pad_threshold值,可将其填充为2M text,保证可使用上大页。hugetext_pad_threshold=0,表示填充功能关闭,该接口依赖hugetext_enabled接口。
建议一般情况写4096即可:echo 4096 > /sys/kernel/mm/transparent_hugepage/hugetext_pad_threshold
当然,如果想完全回退代码大页对应用的影响,可以采用下面的回退方式:
在打开hugetext_enabled后,若关闭hugetext_enabled并且完全消除hugetext_enabled影响,可以下面几种方式:
- 清理整个系统的page cache:echo 3 > /proc/sys/vm/drop_caches
- 清理单个文件的page cache:vmtouch -e /<path>/target
清理遗留大页:echo 1 > /sys/kernel/debug/split_huge_pages
代码大页性能评估
Mysql性能评估
为了充分挖掘代码大页的收益,我们针对不同的平台,包括x86、arm,分别对mysql、python以及jvm等应用进行了测试,由于数据太多、混乱,我们挑选出mysql上的测试数据,测试数据包括TPS、QPS以及iTLB miss数据。
本文数据的测试环境:
- 5.10(alios 5.10-002)
- mysql版本:MariaDB-10.3.28
- 虚拟机配置:32核、128G内存
- 物理机配置:打开透明大页
由于在arm平台和x86平台测试的数据指标和测试方法相同,所以我们挑选出arm篇对这些数据进行了较详细的描述和分析,在x86篇中,仅仅简单描述数据统计图中代码大页与4k代码页的性能差异。
arm篇
下面展示在arm平台上,代码大页对mysql的性能提升。
测试过程中,mysql并发数分别是1、8(25%)、16(50%)、32(100%)。测试结果与普通的4K代码页数据对比如下:
图3:

上图TPS对比可以看出:代码大页的性能始终高于普通的代码页。关于这张图中其他的结论,还有:
- 并发数为1时,外在的影响因素最小,此时,代码大页相比普通代码页,性能提升大约在6.9%;
- 并发数8、16基本可以保证没有cpu的竞争,代码大页的性能提升大约也在6.5%以上;
- 并发数32时,由于总核数为32,存在于其他应用竞争cpu,所以TPS较低于前面的测试结果。但是代码大页的鲁棒性更好,此时相比普通代码页,性能提升大约在11%左右;
另外,还有RT的对比,如下图:
图4:

RT的数据主要是前95%的请求的最大响应时间(95th percentile)。代码大页在RT上的表现与图1一致,大约提升在5.7%~11.4%之间。
最后的展示的数据是微架构数据,我们在分析代码大页对性能的影响过程中,主要观察iTLB miss,如图:
图5(a):代码大页与4k代码页iTLB miss对比
图5(b):代码大页与4k代码页iTLB MPKI对比

图5(a)和图5(b)都展示的是iTLB的数据,但是考虑到不同读者的偏好,我们在测试的过程中iTLB miss和iTLB MPKI也一并记录。如这两图所示:
- mysql使用代码大页后,iTLB miss大约下降了10倍左右,数值大小从原来的1%下降到0.08%左右;
- 在iTLB MPKI上,大约下降了6倍左右;
经过上述数据的对比,以及我们在实验阶段进行的THP(这里主要指 anon THP)数据对比, 在mysql场景下,大致可以得出一个简单的公式:
收益:代码大页 > anon THP > 4k
x86篇
与上一小节相同,这里分别对代码大页和4k代码页进行TPS、RT对比、iTLB miss对比。
图6(a、b)为TPS、RT对比图,图7(a、b)为iTLB miss和iTLB MPKI对比图:
图6(a、b)为TPS、RT对比图:

由于TLB硬件的差异,代码大页在x86和arm上性能提升存在差异,根据图6(a、b)中的测试数据,代码大页在x86上,对TPS的提升大致在3%~5之间,在RT上大致有5%~7.5%的收益。
图7(a、b)为iTLB miss和iTLB MPKI对比图:

在图7中,可以看出:
- 从iTLB miss的数据看,代码大页相比4k代码页大约下降了1.5~2.3倍左右;
- 从iTLB MPKI的数据看,代码大页相比4k代码页大约下降了1.6~2.3倍之间,与iTLB miss效果相似;
到这里,我们结束对代码大页性能数据的展示。关于在物理机的测试,感兴趣的读者可以自行在 alios 5.10-002 上进行测试。
PostgreSQL性能评估
代码大页相比libhugetlbfs方案,不仅仅解决了使用libhugetlbfs后无法使用perf观察热点问题,这里在PostgreSQL上padding功能派上了作用,最终代码大页性能提升较libhugetlbfs多2%左右。

上述测试数据主要在arm环境中。
小结

注:以上主要为arm平台测试数据。符号x可以认为该应用没有采用该技术,或优化效果几乎是0
最后归纳一下,归根结底,代码大页对性能的提升也是采用的内存领域常用方法:热点+大页。
自适应代码大页
自适应代码大页在代码大页的基础上,考虑到x86平台上2MB iTLB entry不足的问题,进行的“热点采集+大页整合”的大页使用数量限制策略,该特性基本解决了代码大页在某些特大代码段上的负优化问题。
云上JVM类应用大约占比60%以上,这类应用在阿里内部大约可发现所使用的code cache大约有150M~400M情况,这类code cache是比较特殊的匿名页,属于JIT热点代码缓存在此处,属于特殊的代码数据。对于ARM平台,其TLB硬件社区并未区域2M或4k的页表项,而对于x86平台,其TLB命中有页大小要求,例如:4k页只能使用4k TLB entry、2M页使用2M TLB entry。这种设计要求应用本身使用的页表不会超过硬件支持,否则易出现严重的TLB未命中情况,导致性能骤降。当前我们对云上常用的x86平台情况进行了一个调研,主要包括以下三类服务器在役:
平台\TLB类型 | 2M L1 code TLB | 2M L2 code TLB/STLB |
Intel Skylake | 8 entries | 1536 entries |
海光(AMD) | 64 entries | 1024 entries |
cascade lake | 8 entries | 1536 entries |
从上面的数据可以看出,当前在x86平台,基本无法使用代码大页,2M code TLB资源使用率基本为0。下面我们以蚂蚁的JAVA的业务为例说明由于TLB资源匮乏导致的性能问题。在蚂蚁的JAVA业务总通过hugetext让code cache使用大页,出现性能回退:iTLB miss上升16%左右,cpu利用率上升10%左右。其原因可以确定在于code cache大约150M,需要覆盖70多个2M iTLB entry,而当前蚂蚁环境使用的机器基本都是intel机器,以skylake为例,仅仅8 2M iTLB entry,造成2M iTLB entry竞争激烈。下表为测试的代码大页负优化数据:
intel | hugetext=0 | hugetext=2 | 正负优化 |
cpu_util | 19.57 | 21.63 | 负10.5% |
itlb_misses.walk_active/cycles | 4.5% | 1.1% | |
icache_64b.iftag_stall/cycles | 10.8% | 27.1% | 负16.3% |
itlb_misses.stlb_hit/cycles | 0.1140% | 0.59%(主要) | 负0.48% |
dtlb_load_misses.stlb_hit/cycles | 0.716% | 0.607% |
上面的数据表示在打开hugetext后,icache_64b.iftag_stall反而上升了16%左右,另外在STLB命中的比例上升了0.48%,造成的结果就是cpu利用率上升10%左右。
将上面描述的问题可以简单总结为以下:
(1)intel机器普遍存在大页iTLB entry数量较少的问题,大多数服务器集中在8~16之间。当前arm平台由于不区分4k和2M iTLB,尚未发现类似问题;
(2)诸如此类,其他业务,如flink、容器场景,代码大页使用过多造成上述问题;
以intel skylake为例,iTLB entry信息:
TLB信息 | 4k | 2M / 4M | 1GB |
L1 code TLB | 64 entries | 8 entries (per-thread) | 0 |
L1 data TLB | 64 entries | 32 entries | 4 entries |
L2 STLB | 1536 entries | 16 entries | |
另外,也发现STLB命中后,替换L1 iTLB entry的惩罚较重,大约占22 cycles。因此需要避免代码在STLB命中。简而言之,在Intel系列普遍存在2M iTLB entry有限的问题,是代码大页在此类平台性能提升不明显或存在负优化的核心原因。
针对上面描述的问题,自适应代码大页采用限制大页使用数量的方式,将较热点的数据整合为大页。在使用时,仅需在用户态设置自适应大页的数量接口。自适应代码大页处理流程如下:
使用该方案,可以解决代码大页在x86平台上负优化的问题,可以应用到JVM类应用和oceanbase数据库上。
自适应代码大页使用接口:/sys/kernel/mm/transparent_hugepage/khugepaged/max_nr_hugetext,只要为该接口设置一个值,就可以约束单个应用使用的最多代码大页数量,且不会影响THP的正常逻辑。
除了大页,代码段优化还有什么
代码段的优化除了大页的方案,业内还包括PGO/autoFDO、LTO等
按代码段优化时间,可以将当前在编译器和链接器所做优化,简单分为:post-link optimization、link-time optimizations以及比较混合的BOLT optimization,如下。这些优化都是对应用代码段布局的优化。
- feedback-driven optimizations (FDO) or called profile-guided optimizations (PGO)(一般用PGO统称)
- sample-based profiling
- instrumentation-based profiling (基本都是线下训练)
- hardware-event sampling e.g. LBR (e.g. SampleFDO, AutoFDO)
- profile-guided function reordering algorithm
- Pettis-Hansen (PH) heuristic (weight dynamic call-graph, instrumentation-based profiling)
- Call-Chain Clustering (C3) heuristic (weight and directed call-graph, hardware-event sampling or stack traces sampling)
- link-time optimizations (LTO, [27, 28])
- only depend on function reordering algorithm (PH heuristic, C3 heuristic)
- post-link optimization (e.g. Ispike Spike [5], Etch [6], FDPR [7])
- BOLT optimizations (大杂烩)
(不需要看懂,只要知道有autoFDO和BOLT就行)
所以这个东西干嘛用的?举一个简单例子,实验课上老师给大家一段排序的代码,让大家进行优化,将排序的时间开学降低10倍,这个时候就可以用上面的工具,再加上简单的perf使用,而不需要看具体的代码实现。
代码大页支持产品:龙蜥操作系统和阿里云ECS
当前,代码大页已经在龙蜥操作系统和阿里云ECS支持,参考链接如下:
龙蜥操作系统4.19和5.10最新版本默认打开CONFIG_HUGETEXT(代码大页的主要配置)。“代码大页性能评估”一节的数据可以在龙蜥操作系统5.10内核复现,使用镜像可选择:
名称 | 描述 |
AnolisOS-8.8-x86_64-ANCK.qcow2 | x86_64 架构 QEMU 虚拟机镜像(qcow2 格式, 5.10 内核) |
AnolisOS-8.8-x86_64-RHCK.qcow2 | x86_64 架构 QEMU 虚拟机镜像(qcow2 格式, 4.18 内核) |
AnolisOS-8.8-aarch64-ANCK.qcow2 | aarch64 架构 QEMU 虚拟机镜像(qcow2 格式, 5.10 内核) |
AnolisOS-8.8-aarch64-RHCK.qcow2 | aarch64 架构 QEMU 虚拟机镜像(qcow2 格式, 4.18 内核) |
镜像缺省sudo用户为 anuser,对应登录密码是 anolisos
链接: https://docs.openanolis.cn/products/anolis/rnotes/anolis-8.8.html
- 阿里云ECS
若使用环境为阿里云ECS,可参考:https://help.aliyun.com/document_detail/462660.html 使用代码大页。
后续展望
最后头脑风暴一下,代码大页也许还可以:
- 代码只读文件系统,专门用于放置lib库;
- 64kB、32MB代码大页,接近特殊场景2MB代码大页无法使用的问题;
- 多种代码大页支持;
- 靶向代码大页;
从iTLB miss角度看,这些也许只能算是功能完善,并没有太大的性能优化。
参考
- 阿里云ECS上使用代码大页说明:https://help.aliyun.com/document_detail/462660.html
- 将mysql可执行文件的代码段和数据段加载到大页上:https://jira.mariadb.org/browse/MDEV-24051
- 社区讨论:https://sourceware.org/pipermail/libc-alpha/2021-February/122334.html
- 如何将自己的代码段和数据段映射到大页:hugepage_text.cc
- MAP_DENYWRITE:被Linux内核屏蔽的flag:https://developer.aliyun.com/article/863760