在广告系统中,在投广告的信息作为核心数据,具有改动频繁、实时性要求高的特点。为了设计并实现一个满足业务需求的广告系统,目前业界比较常见的解决方案是在广告检索集群每一台服务器的内存中存放索引数据,并通过mmap技术进行持久化(也可以直接使用tmpfs,不进行磁盘级别的持久化)。
众所周知,为了方便内存管理,提高内存使用效率,操作系统对内存进行了分段分页处理,其中分段管理的粒度较粗,分页管理的粒度较细。特别的,Linux默认的内存分页大小为4KB/page。内存具有对密集访问友好的特性,即连续访问内存性能上要优于跳跃访问。主要原因有两方面因素:
1、现代计算机配有高速cache,连续访问内存能更好的利用cache进行快速访问
2、虚拟内存->物理内存的映射表(页表)也具有缓存的特性,最近被访问的页表会存放在cahce的快表(TLB)中
当内存不连续访问时,会出现频繁的cache换页,影响性能。但是在内存索引的应用中,难免会遇到跳跃访问内存的场景,此时除了想办法“变跳跃为连续”以外,还有没有其他的思路能对性能进行优化?Linux中的hugepage为我们提供了另外一种思路。既然跳跃无法避免,我们可不可以减小跳跃带来的开销,也就是减小换页的概率?一个很直接的方式就是加大每个内存页的大小。hugepage就是这样一种内存管理策略。
使用大内存页有哪些好处:
1、减少页表(Page Table)大小。每一个Huge Page,对应的是连续的2MB物理内存(默认),这样12GB的物理内存只需要48KB的Page Table,与原来的24MB相比减少很多。
2、Huge Page内存只能锁定在物理内存中,不能被交换到交换区。这样避免了交换引起的性能影响。
3、由于页表数量的减少,使得CPU中的TLB(可理解为CPU对页表的CACHE)的命中率大大提高。
4、针对Huge Page的页表,在各进程之间可以共享,也降低了Page Table的大小。实际上这里可以反映出Linux在分页处理机制上的缺陷。而其他操作系统,比如AIX,对于共享内存段这样的内存,进程共享相同的页表,避免了Linux的这种问题。
1、首先用
cat /proc/meminfo | grep Huge
命令来查看操作系统是否支持hugepage,若支持则返回如下
AnonHugePages: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
其中HugePages_Total指系统中分配的hugepage的大内存页数,HugePages_Free指的是未被使用的大内存页数,HugePages Rsvd表示已经分配但是还未使用的页面数,Hugepagesize表示大内存页面大小,这里为2MB
2、修改/etc/sysctl.conf文件,增加如下行:
vm.nr_hugepages=1000
然后执行sysctl –p命令,使配置生效。
这里vm.nr_hugepages是大内存页数量。然后检查/proc/meminfo,如果HugePages_Total小于设置的数量,那么表明没有足够的连续物理内存用于这些大内存页,需要重启服务器。
重启后运行
cat /proc/meminfo | grep Huge
正常返回如下
AnonHugePages: 0 kB
HugePages_Total: 1000
HugePages_Free: 1000
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
目前总结出的hugepage有两种使用方法。
例程如下:
#include
#include
#include
int main(int argc, char *argv[]) {
char *m;
size_t s = (8UL * 1024 * 1024);
m = mmap(NULL, s, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | 0x40000 /*MAP_HUGETLB*/, -1, 0);
if (m == MAP_FAILED) {
perror("map mem");
m = NULL;
return 1;
}
memset(m, 0, s);
printf("map_hugetlb ok, press ENTER to quit!\n");
getchar();
munmap(m, s);
return 0;
}
在这种使用方法中,进程使用mmap打开了8MB大小的一块hugepage内存,并进行了清零。在程序暂定的时候使用
cat /proc/meminfo | grep Huge
可以看到
AnonHugePages: 0 kB
HugePages_Total: 1000
HugePages_Free: 996
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
可以看出,大内存页被占用8MB/2MB=4页,还剩余1000-4=996页。
十分需要注意的是,在这种使用方法中,mmap的0x40000(MAP_HUGETLB)参数必须同时配合MAP_PRIVATE和MAP_ANONYMOUS使用,缺一不可,否则mmap调用不会成功。因此,这种方法限制了hugepage只能有一个进程共访问,无法实现进程中共享。
这种使用方法的思路是先在系统中挂载一个hugetlbfs类型的文件系统,该文件系统下的文件只能mmap访问,无法用编辑器编辑,也无法用read/write内核调用操作。挂载后在目录下touch一个文件,应用程序可以使用mmap内核调用的内存共享参数将文件映射到虚拟内存,然后进行访问。这种方法的好处是多个进程可以共享hugepage。
1、挂载hugetlbfs
mkdir -p /mnt/hugepages
mount -t hugetlbfs hugetlbfs /mnt/hugepages/
2、建立文件
touch /mnt/hugepages/a
3、使用mmap内核调用的内存共享参数进行映射
size_t s = (8UL * 1024 * 1024);
uint32_t fd = open("/mnt/hugepages/a", O_RDWR);
char* m = (char*)mmap(NULL, s, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd, 0);
实践中发现,在使用mmap对hugepage进行映射时,映射的内存大小s一定要满足
ceil(s/4KB)*4KB == N * hugepagesize
其中N是整数,否则,mmap调用会返回失败。
例程代码见附录1。
程序首先验证了2.3.1中提到的映射大小的问题,然后对比hugepage和普通内存在顺序和乱序访问的性能,对比时用了1G内存进行实验。注意mmap映射后,需要遍历访问一次才能保证数据被真正加载进物理内存。
使用-O0优化参数编译后输出如下:
hugepage mmap range : 2093056
hugepage mmap failed: Invalid argument
hugepage mmap range : 2093057
mmap succeed
mem access perf
huge-unorder time : 18875138 us
4K-unorder time : 22091062 us
huge-order time : 2723796 us
4K-order time : 2717091 us
可以看出,在顺序访问时,hugepage和普通分页内存的性能是几乎相同的,但是在跳跃访问时,hugepage的性能较普通分页内存提升了15%左右。
使用-O3优化参数编译后输出如下:
hugepage mmap range : 2093056
hugepage mmap failed: Invalid argument
hugepage mmap range : 2093057
mmap succeed
mem access perf
huge-unorder time : 17522742 us
4K-unorder time : 19695761 us
huge-order time : 1251910 us
4K-order time : 1256035 us
可以看出顺序访问性能较-O0提升了1倍以上,乱序访问性能提升不大。在跳跃访问时,hugepage的性能较普通分页内存提升了11%左右。
1、乱序访问内存性能提升
2、多个线程实例可以共用一份虚拟内存地址,节省了多个线程多次mmap带来的虚拟内存浪费,避免了虚拟内存不足的问题;当cpu核心数量较多时,可以规避由于虚拟内存空间占满而带来的瓶颈
1、广告系统中的内存索引大部分是由专门的集群构建,然后用文件的形式分发到检索集群。但是hugetlbfs文件系统不能直接拷贝文件,需要专门的服务将索引文件的整个目录结构转存成每台机器上的hugepage内存映射
2、由于每一次进行hugepage内存映射的最小单位为2MB(默认),因此对索引中的小文件进行映射时会造成很大的内存浪费,这就要求索引文件体系要尽量避免小文件。
3、huagepage的内存利用效率较低,会出现难以避免的内存浪费, 给检索服务的稳定性带来风险。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
char *m;
char *n;
size_t threshold = ( 2044 * 1024 );
size_t s = (1024UL * 1024 * 1024);
uint32_t fd = open("/mnt/hugepages/a", O_RDWR);
uint32_t fd2 = open("./aaa", O_RDWR);
lseek(fd2, s, SEEK_SET);
write(fd2, "", 1);
printf("hugepage mmap range : %d\n", threshold);
m = (char*)mmap(NULL, threshold, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd, 0);
n = (char*)mmap(NULL, threshold, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd2, 0);
if (m == MAP_FAILED) {
perror("hugepage mmap failed");
}
if (n == MAP_FAILED) {
perror("normal mmap failed");
}
threshold++;
printf("hugepage mmap range : %d\n", threshold);
m = (char*)mmap(NULL, threshold, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd, 0);
n = (char*)mmap(NULL, threshold, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd2, 0);
if (m == MAP_FAILED) {
perror("hugepage mmap failed");
}
if (n == MAP_FAILED) {
perror("normal mmap failed");
}
printf("mmap succeed\n\n");
printf("mem access perf\n");
m = (char*)mmap(NULL, s, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd, 0);
n = (char*)mmap(NULL, s, PROT_READ | PROT_WRITE,
MAP_SHARED /*MAP_HUGETLB*/, fd2, 0);
if (m == MAP_FAILED) {
perror("hugepage mmap failed");
return -1;
}
if (n == MAP_FAILED) {
perror("normal mmap failed");
return -1;
}
struct timeval start,end;
uint64_t consume_time_us = 0;
for (int i = 0 ; i < s; i++) {
m[i]++;
}
for (int i = 0 ; i < s; i++) {
n[i]++;
}
gettimeofday(&start, NULL);
for (int j = 0 ; j < 4096; j++) {
for (int i = 0 ; i < s; i += 4096) {
m[i + j]++;
}
}
gettimeofday(&end, NULL);
consume_time_us =
(end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec;
printf("huge-unorder time : %lu us\n", consume_time_us);
gettimeofday(&start, NULL);
for (int j = 0 ; j < 4096; j++) {
for (int i = 0 ; i < s; i += 4096) {
n[i + j]++;
}
}
gettimeofday(&end, NULL);
consume_time_us =
(end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec;
printf("4K-unorder time : %lu us\n", consume_time_us);
gettimeofday(&start, NULL);
for (int i = 0 ; i < s; i++) {
m[i]++;
}
gettimeofday(&end, NULL);
consume_time_us =
(end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec;
printf("huge-order time : %lu us\n", consume_time_us);
gettimeofday(&start, NULL);
for (int i = 0 ; i < s; i++) {
n[i]++;
}
gettimeofday(&end, NULL);
consume_time_us =
(end.tv_sec - start.tv_sec) * 1000000 + end.tv_usec - start.tv_usec;
printf("4K-order time : %lu us\n", consume_time_us);
munmap(m, s);
munmap(n, s);
close(fd);
close(fd2);
return 0;
}
如果感兴趣,欢迎关注微信技术公众号