DPDK内存碎片优化,性能最高提升30+倍!

原文链接点击 DPDK内存碎片优化,性能最高提升30+倍!

背景

DPDK(Data Plane Development Kit)是一个用于高性能数据包处理的开源项目,常用于构建各类高性能网络应用。基于 DPDK 的设计思想及其提供的线程/内存模型,SPDK(Storage Performance Development Kit)被开发出来,广泛用于构建各类高性能存储应用。

为了提升应用程序的性能,DPDK 基于大页内存实现了一套自己的内存管理接口,这些内存接口被广泛应用于SPDK/DPDK 的各类应用中,而且不少应用会在 IO 路径中进行内存的分配与释放。DPDK 常用的内存接口有:

void *rte_malloc(const char *type, size_t size, unsigned align);
void *rte_realloc(void *ptr, size_t size, unsigned int align);
void rte_free(void *ptr);




在实际使用过程中,我们发现 DPDK 原生的内存分配接口在一些场景下有着较大的性能问题。本文将结合实际遇到的问题,为大家介绍如何优化 DPDK 内存碎片问题。

问题现象

当接到 DPDK 内存碎片化的问题反馈后,经过多方排查,最终将怀疑点锁定到大页内存的分配上,并将实际使用时内存分配的逻辑与参数进行抽象后,编写了如下的测试 case:

uint64_t entry_time, time;
size_t size = 4096;
unsigned align = 4096;
for (int j = 0; j < 10; j++) {
        entry_time = rte_get_timer_cycles();
        for (int i = 0; i < 2000; i++) {
                rte_malloc(NULL, size, align);
        }
        time = (rte_get_timer_cycles()-entry_time) * 1000000 /
                rte_get_timer_hz();
        printf("total open time %lu us, avg time %lu us\n", time, time/2000);
}




单线程执行内存分配,总共分配 1 万个 4k 大小的对象, 每 2000 次分配后计算平均耗时,中间不释放内存,各阶段单次平均耗时如下表:

malloc次数 平均耗时
0-2000 15us
2000-4000 44us
4000-6000 77us
6000-8000 168us
8000-10000 253us

从上表可以看到,随着分配次数的增加,后续每分配 4K内存的耗时在线性增加,按照我们之前的理解,单线程下多次分配内存耗时不会过高,每次分配4K内存的耗时应该差不多,但是当前结果与预期不符,针对该场景我们进行了深入分析。

分析

首先结合代码分析了 DPDK 分配的逻辑,然后结合火焰图对怀疑点加了些调试手段以进一步 debug,最终得到了结论,并对结论进行了的验证。

代码分析

DPDK 通过 heap 管理每个 NUMA 节点上的大页内存,对于一块连续内存,首先转换成一个 struct malloc_elem 对象 elem,再将 elem 插入链表中,并保证链表中每项的地址按大小排序。每个 heap 都维护了 13 个链表,每个链表中的 elem 大小在相同范围内,比如大小 0-256 的 elem 在同一个链表,256-1024 的 elem 在同一个链表,1024-4096 的 elem 在同一个链表...。

DPDK内存分配的主要逻辑如下图所示:

image.png

  1. 根据参数和当前线程所在 CPU 决定从哪个 heap 上分配内存。
  2. 根据要分配的大小,找到对应的链表(对应代码中的 free_head[N]),遍历该链表中所有的 elem,找到合适(满足大小、对齐等要求)的 elem。
  3. 将 elelm 从链表上摘除,根据大小进行切分并返回。

抓火焰图

可以看到 find_subitable_element 函数耗时非常高,这个函数的作用就是遍历链表上的 elem,找到满足要求的 elelm。以分配 4K 内存为例,首先遍历第三个链表即 free_head[2] 上的所有 elem,逐个检查是否满足要求,如果找到合适的 elem,就从链表上摘除,根据大小进行切分并返回,如果 free_head[2] 上找不到,会依次在free_head[3]、free_head[4] 上查找,直到找到为止。

从代码分析单次判断 elem 是否满足要求耗时应该很短,所以怀疑是链表上元素过多,导致整个遍历耗时过久。

那么我们可以尝试添加些 log,看看各个链表上 elem 的个数是否有变化

添加调试手段

经分析 DPDK 有些内存 dump 的 rpc,其中用到的 malloc_heap_get_stats 函数会遍历 heap 上所有链表上的 elem,我们可以简单修改,查看每个链表上的 elem 个数。

系统刚启动完成时,可以看到 elem 个数不多,大小也比较集中。

分配 2000 个 4K 内存后,看到 free_head[2] 上 elem 数量明显增加:

  • 分配 4000 个 4K 内存后:

可以看到 free_head[2] 上 elem 个数随着分配次数的增加线性增加,这会导致每次分配 4K 所需遍历的 elem 个数也会增加,那么为什么会这样呢?

原因分析

添加 log 发现,free_head[2] 上所有 elem 的大小都小于 4096 字节:

为什么会出现这么多小的 elem 呢?经分析我们发现是在分配时切分 elem 而生成,切分 elem 示意图如下:

image.png

以上图为例:

  1. 当调用 find_suitable_element 函数后,找到了一个满足要求的 elem,该 elem 大小一般远大于 4096 字节,所以在 malloc_elem_alloc 中会切分。
  2. malloc_elem_alloc 中会从 elem 末尾开始,根据大小和对齐算出 new_elem 的地址,由于分配内存要求 4K 对齐,所以 end-start 很可能大于 4096 ,而我们只要 4096,那么 new_elem 后面的 tailer 这段空间为避免浪费,是可以被重新利用的,由于其大小是 2432,也会被插入 free_head[2] 中。这也就导致了 free_head[2] 上 elem 个数在线性增加。

验证

根据上述分析,如果 align 为 64 字节,elem 的尾部空间应该小于 64,不会再插入链表,就不会有这个问题,将测试代码中的 align 参数改为 64 后,测试结果如下:

平均耗时 0.7us ~ 0.8us,不再有线性增加的现象。

那么是不是所有的 align 都有这个问题?我们又测了分配 64K 大小,align 为 64K 的情况,也出现了相同的情况。

结论

DPDK在管理内存时会将相邻大小的 elem 放在一个链表,分配时根据大小 4K 找到对应的链表 free_head[2],遍历该链表上的 elem 来查找满足要求的 elem,当分配时指定 align 过大,会使找到的 elem 尾部空间较大,为避免空间浪费会将尾部空间重新插入 free_head[2] 链表中,导致 free_head[2] 链表中 elem 数量增多,下次分配时又需要先遍历 free_head[2],使每次分配耗时呈线性增加。

解决方案

方案一:

struct malloc_heap {
        rte_spinlock_t lock;
        LIST_HEAD(, malloc_elem) free_head[RTE_HEAP_NUM_FREELISTS];
        size_t free_head_max_size[RTE_HEAP_NUM_FREELISTS];
        .....
}




在 heap 中新增 free_head_max_size 数组,记录每个 freelist 中 elem 的最大 size,遍历时如果要求的 size 大于该 freelist 上最大的size,可以直接跳过,但缺点是每次分配/释放时都要维护该数组。

可以看到单次分配耗时没有再呈线性增加,平均耗时稳定在0.5us左右,对比之前性能提升30 倍+:

方案二:

在原有的逻辑中,大小为 4K 的 elem 在 free_head[2] 上,4K 刚好处于边界,如果修改链表选择的规则,改到在 free_head[3] 进行查找,也可以解决问题。以分配 4K 为例,在 4K-16K 的链表上更容易找到对应的 elem,16K 也是在 16K-64K 的链表上能查找的概率更高。

也即每个链表上包含的元素大小范围如下:

heap->free_head[ 0 ] - ( 0 , 2 ^ 8 ) 0 < x < 256  heap->free_head[ 1 ] - [ 2 ^ 8 , 2 ^ 10 ) 256 <= x < 1024  heap->free_head[ 2 ] - [ 2 ^ 10 , 2 ^ 12 ) 1024 <= x < 4096  heap->free_head[ 3 ] - [ 2 ^ 12 , 2 ^ 14 ) 4 k <= x < 16 k
heap->free_head[ 4 ] - [ 2 ^ 14 , 2 ^ 16 ) 16 k <= x < 64 k ...




对比测试效果与方案一无明显差异,优点是对 16K/64K 等处于边界点的分配效率也有提升,缺点是可能会对极端情况有影响,需要更多测试。

测试验证

单线程

bulk:连续分配 1000 个对象,memset 后,1000 个对象一起释放,统计单次平均分配/释放耗时

no-bulk:分配 1 个对象,memset, delay 5us, 释放,重复 1 万次,统计平均分配和释放的总耗时

realloc:分配 1 个对象,memset, realloc 成原本 2 倍大小,memset, realloc 成原本 4 倍大小,memset,释放,重复 1000 次,统计平均总耗时

单次分配大小 测试项 修改前align=64 修改后align=64 修改前align=4096 修改后align=4096
64 bulk 0.309/0.083 0.287/0.079 / /
no-bluk 5.275 5.273 / /
realloc 0.538 0.512 / /
128 bulk 0.410/0.074 0.399/0.085 / /
no-bluk 5.270 5.264 / /
realloc 0.584 0.616 / /
512 bulk 0.380/0.115 0.382/0.086 / /
no-bluk 5.291 5.292 / /
realloc 1.033 0.963 / /
1024 bulk 0.636/0.117 0.36/0.146 / /
no-bluk 5.291 5.289 / /
realloc 1.56 1.31us / /
3072 bulk 0.801/0.386 0.812/0.343 0.875/0.217 0.883/0.225
no-bluk 5.32 5.26 5.77 5.81
realloc 2.15 2.09 2.774 2.769
4096 bulk 1.099/0.486 0.771/0.474 0.90/0.324 0.762/0.362
no-bluk 5.62 5.329 5.65 5.394
realloc 2.89 2.59 3.36 2.81
8192 bulk 0.967/0.563 1.011/0.589 0.886/0.516 0.883/0.476
no-bluk 5.37 5.37 5.43 5.43
realloc 4.68 4.65 4.87 4.75
16384 bulk 1.33/0.87 1.21/0.881 1.379/0.938 1.33/0.95
no-bluk 5.49 5.465 5.57 5.525
realloc 9.59 9.528 9.76 9.705
32768 bulk 2.22/2.01 2.276/2.06 2.41/2.06 2.44/2.068
no-bluk 5.65 5.646 5.72 5.71us
realloc 20.5 20.49 20.8 20.74
131072 bulk 10.432/10.526 10.397/10.531 10.68/10.79 10.676/10.818
no-bluk 9.87 9.866 9.9 9.9
realloc 82.0 82.15 82.8 82.56
524288 bulk 42.8/42.59 43.058/42.689 43.6/43.2 43.609/43.357
no-bluk 23.72 23.74 23.8 23.8us
realloc 406 406 407 408.75
1048576 bulk 125.2/125.7 124.9/125 88.2/86.8 87.3/86.9
no-bluk 42.38 42.25 42.46 42.46
realloc 869 873 890 888

多线程

bulk:连续分配 1000 个对象,memset 后,1000 个对象一起释放,统计单次平均分配/释放耗时

no-bulk:分配 1 个对象,memset, delay 5us, 释放,重复 1 万次,统计平均分配和释放的总耗时

realloc:分配 1 个对象,memset, realloc 成原本 2 倍大小,memset, realloc 成原本 4 倍大小,memset,释放,重复 1000 次,统计平均总耗时

单次分配大小 测试项 修改前align=64 修改后align=64 修改前align=4096 修改后align=4096
64 bulk 15.0/6.5 15.1/6.5 / /
no-bluk 22.6 22.6 / /
realloc 73.5 73.8 / /
128 bulk 17.4/6.7 17.2/6.7 / /
no-bluk 23.5 23.5 / /
realloc 73.9 73.3 / /
512 bulk 16.8/6.2 16.4/6.2 / /
no-bluk 22.3 22.4 / /
realloc 82.1 79.5 / /
1024 bulk 18.5/6.8 15.9/6.7 / /
no-bluk 25.1 24.0 / /
realloc 92.1 84.7 / /
3072 bulk 18.2/7.8 18.2/7.9 29.1/6.7 29.3/6.5
no-bluk 23.9 25 36.8 36.3
realloc 91.8 91.3 117.7 116.2
4096 bulk 21.3/8.5 16.7/8.4 30.3/14.2 23.0/7.5
no-bluk 28.9 24.6 39.2 30.0
realloc 101.8 95.1 125.6 108
8192 bulk 18.4/12.0 18.4/12 25.0/9.8 24.1/11.5
no-bluk 26.0 25.9 31.2 30.9
realloc 110.7 108.4 122.8 121
16384 bulk 22.9/19.4 19.4/19.0 28.5/16.5 27.2/15.6
no-bluk 27.8 26.5 33.5 31.5
realloc 125.6 122.5 140.9 136.2
32768 bulk 27.4/33.2 27.4/33.2 32.8/30.3 34.1/27.5
no-bluk 28.9 28.8 34 34
realloc 171 168.2 184.5 180.8
131072 bulk 44.8/170 44.6/171 44.7/169.4 44.6/167
no-bluk 58.5 58.1 63.2 62.9
realloc 463.6 458.3 472.7 470
524288 bulk 160/655 160/660 160.8/653.7 160.1/652.5
no-bluk 162.8 163 168.8 168.7
realloc 1812 1826 1824 1820

16 个线程,每个线程单独绑核后做如下测试:

连续分配 N 个 4K 大小对象,4K 对齐,每次分配之间 delay 5us, 统计耗时,然后连续释放 N 个 4K 对象,不delay,统计单次耗时

单线程分配次数 优化前 优化后
64 109us 16us
128 214us 17.8us
256 408us 17.5us
512 962us 18.2us
1024 3.1ms 15.3us
2048 8.6ms 17.6us
4096 20.5ms 17.4us

DPDK malloc perf test

malloc 测试,修改前:

malloc 测试,修改后,可以看到没有性能衰退,4K/64K/1M 等大小分配有 15%-50% 的性能提升:

memzone 测试,修改前:

memzone测试,修改后没有性能衰退,64K/1M 大小的分配分别有 7% 和 52% 的性能提升:

总结

通过以上分析,确定了 DPDK 内存碎片化的诱因是在分配时要求对齐的大小比较大,导致 elem 切分时产生了很多碎片,并影响了后续内存分配的效率。经过完整的测试,最终选用了方案二,改动简单,效果也更好,不仅解决碎片场景下的问题,使得性能提升 30+ 倍,也在非碎片情况下,使得 4K/64K/1M 等大小的内存分配性能提升 15-50% ,同时也能缓解多进程并发分配时竞争锁的压力。目前字节跳动 STE 团队已将该补丁提交至 DPDK 社区,并于 2023 年 2 月 合入 DPDK 主线,链接如下:https://inbox.dpdk.org/dev/CAJFAV8x5k55jH0A_BHN9jxA1m-3o08tKr7RbCesvVL7o4MKgGQ@mail.gmail.com/

DPDK/SPDK 作为开源项目,凭借其出色的性能表现赢得了众多开发者的青睐,我们也希望在使用开源项目获得收益的同时,能为项目与社区带来更多想法,做出更多贡献,与开发者们一起推进社区的建设。

优秀的个人博客,低调大师

微信关注我们

原文链接:https://my.oschina.net/u/6150560/blog/8881899

转载内容版权归作者及来源网站所有!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

相关文章

发表评论

资源下载

更多资源
Mario,低调大师唯一一个Java游戏作品

Mario,低调大师唯一一个Java游戏作品

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Apache Tomcat7、8、9(Java Web服务器)

Apache Tomcat7、8、9(Java Web服务器)

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Java Development Kit(Java开发工具)

Java Development Kit(Java开发工具)

JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。