• 如何快速定位 dpdk memzone 内存泄露问题?


    问题现象

    dpdk-19.11 secondary 进程无法启动,报错信息中有如下内容:

    memzone_reserve_aligned_thread_unsafe(): No more room in config
    
    • 1

    确认问题为创建 memzone 失败,却不清楚为什么失败,需要定位原因。

    阅读代码与调试

    使用关键字 No more room in config 在 dpdk 源码中搜索,找到 memzone_reserve_aligned_thread_unsafe 函数的如下代码:

    	struct rte_memzone *mz;
    	struct rte_mem_config *mcfg;
    	struct rte_fbarray *arr;
    
    	/* get pointer to global configuration */
    	mcfg = rte_eal_get_configuration()->mem_config;
    	arr = &mcfg->memzones;
    
    	/* no more room in config */
    	if (arr->count >= arr->len) {
    		RTE_LOG(ERR, EAL, "%s(): No more room in config\n", __func__);
    		rte_errno = ENOSPC;
    		return NULL;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    此代码首先获取到 rte_mem_config 的地址,然后访问其中的 memzones 字段,此字段是一个 rte_fbarray 结构。此后判断 rte_fbarray 结构的 count 值大于等于 len 时打印 No more room in config 的错误信息后返回。

    这个 memzones 的 rte_fbarray 的默认大小定义如下:

    common_base:98:CONFIG_RTE_MAX_MEMZONE=2560
    
    • 1

    gdb 调试发现,rte_mem_config memzones 字段中的 count 值与 len 值相等,均为 2560,表明此时 rte_fbarray 的所有表项都已经耗尽,大概率是存在 memzone 内存泄露。

    继续阅读代码确定 memzone_reserve_aligned_thread_unsafe 函数将会被 rte_memzone_reserve 函数调用,rte_memzone_reserve 函数会传递一个唯一的 name 字符串以标识每一个分配出来的 memzone。

    复现过程

    多次复现发现,当 primary 进程正常运行时,secondary 进程启动后退出达到一定的次数就会报 No more room in config 的问题,表明 secondary 进程每次启动后退出都会泄露一部分 memzone,当泄露完成后就无法创建新的 memzone

    如何找到泄露的位置?

    可能有如下方法:

    1. dump rte_mem_config 中的 memzones rte_fbarray 结构,查看到哪个 memzone 的名称前缀出现的次数最多,使用这个名称在代码中搜索即可。
    2. 修改 rte_memzone_reserve 函数代码,在其中添加打印 name 参数,重启 primary 进程,运行两次 secondary 进程找到名称最多的 name 前缀,搜索代码。
    3. 重启 primary 进程,运行一次 dpdk secondary 进程后退出,然后执行 od -c /var/run/dpdk/rte/fbarray_memzone 命令并将输出信息保存为 1.txt,再次运行一次 dpdk secondary 进程,然后继续执行 od -c /var/run/dpdk/rte/fbarray_memzone 命令并将输出信息保存为 2.txt,使用文本对比工具对比差异,找到新增的 memzone 的名称,搜索代码。
      od -c fbarray_memzone 文件内容示例如下:
      $ od -c ./fbarray_memzone
      0000000   r   t   e   _   e   t   h   _   d   e   v   _   d   a   t   a
      0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
      0000040 200   l 226 233 003  \0  \0  \0 200   l   6   X 375   ?  \0  \0
      0000060   @ 320 006  \0  \0  \0  \0  \0  \0  \0      \0  \0  \0  \0  \0
      0000100  \0  \0  \0  \0  \0  \0  \0  \0   M   P   _   v   p   p       p
      0000120   o   o   l       0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
      0000140  \0  \0  \0  \0  \0  \0  \0  \0 200   < 225 233 003  \0  \0  \0
      0000160 200   <   5   X 375   ?  \0  \0  \0 001  \0  \0  \0  \0  \0  \0
      0000200  \0  \0      \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
      0000220   M   P   _   v   p   p       p   o   o   l       0       (   n
      0000240   o       c   a   c   h   e   )  \0  \0  \0  \0  \0  \0  \0  \0
      0000260   @   : 225 233 003  \0  \0  \0   @   :   5   X 375   ?  \0  \0
      0000300  \0 001  \0  \0  \0  \0  \0  \0  \0  \0      \0  \0  \0  \0  \0
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
    4. 编译 dpdk_proc_info 程序,重启 primary 进程,添加 – -m 参数运行两次 dpdk_proc_info 程序并分别保存日志信息为 1.txt 与 2.txt,使用文本对比工具对比差异,找到新增的 memzone 名称,搜索代码。
      dpdk_proc_info -m 选项输出的 memzone 信息示例如下:
      ------------ MEMORY_ZONES -------------
      Zone 0: name:<rte_pdump_stats>, len:0x400040, virt:0x1005fffc0, socket_id:0, flags:0
      physical segments used:
        addr: 0x100400000 iova: 0x100400000 len: 0x200000 pagesz: 0x200000
        addr: 0x100600000 iova: 0x100600000 len: 0x200000 pagesz: 0x200000
        addr: 0x100800000 iova: 0x100800000 len: 0x200000 pagesz: 0x200000
      Zone 1: name:<MP_mb_pool_0>, len:0x182100, virt:0x10047de40, socket_id:0, flags:0
      physical segments used:
        addr: 0x100400000 iova: 0x100400000 len: 0x200000 pagesz: 0x200000
      Zone 2: name:<RG_MP_mb_pool_0>, len:0x20180, virt:0x10045db80, socket_id:0, flags:0
      physical segments used:
        addr: 0x100400000 iova: 0x100400000 len: 0x200000 pagesz: 0x200000
      Zone 3: name:<MP_mb_pool_0_0>, len:0x1724880, virt:0x1010db780, socket_id:0, flags:0
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13

    上面列举了四种方法,每种方法都能使用,但是实施过程需要花费的时间以及依赖的基础知识与工具有所差别。

    高版本定位过程

    dpdk 20 年的如下 commit 引入 trace 子系统来 trace dpdk 内部函数调用:

    commit 27db82c709dc466537b8437b0dec0619880d59c9
    Author: Jerin Jacob <jerinj@marvell.com>
    Date:   Thu Apr 23 00:33:19 2020 +0530
    
        trace: introduce new subsystem
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在支持 trace 的 dpdk 高版本可以使能 memzone 相关函数的 trace point,运行一次 secondary 进程,查看 trace 信息中 memzone 函数调用信息,找到没有 free 的 memzone 名称搜索代码来定位报本文描述的问题。

    babeltrace 解析的 trace 日志内容示例如下:

    # babeltrace /root/dpdk-traces/rte-2022-11-12-PM-06-20-08/
    [18:20:09.265384247] (+?.?????????) lib.eal.memzone.reserve: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "rte_pdump_stats", len = 0x400008, socket_id = 0, flags = 0x0, align = 0x40, bound = 0x0, mz = 0x100007000 }
    [18:20:09.282972301] (+0.017588054) lib.eal.memzone.lookup: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "mbuf_user_pool_ops", memzone = 0x0 }
    [18:20:09.282972613] (+0.000000312) lib.eal.memzone.lookup: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "mbuf_platform_pool_ops", memzone = 0x0 }
    [18:20:09.282985000] (+0.000012387) lib.eal.memzone.reserve: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "MP_mb_pool_0", len = 0x182100, socket_id = 0, flags = 0x6, align = 0x40, bound = 0x0, mz = 0x100007048 }
    [18:20:09.283240882] (+0.000255882) lib.eal.memzone.lookup: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "mbuf_user_pool_ops", memzone = 0x0 }
    [18:20:09.283241032] (+0.000000150) lib.eal.memzone.lookup: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "mbuf_platform_pool_ops", memzone = 0x0 }
    [18:20:09.283244566] (+0.000003534) lib.eal.memzone.reserve: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "RG_MP_mb_pool_0", len = 0x20180, socket_id = 0, flags = 0x0, align = 0x40, bound = 0x0, mz = 0x100007090 }
    [18:20:09.285245820] (+0.002001254) lib.eal.memzone.reserve: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "MP_mb_pool_0_0", len = 0x172487F, socket_id = 0, flags = 0x6, align = 0x40, bound = 0x0, mz = 0x1000070D8 }
    ......................................................................................
    [18:20:10.436267496] (+1.150217836) lib.eal.memzone.free: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "MP_mb_pool_0_0", addr = 0x101EDB780, rc = 0 }
    [18:20:10.436276496] (+0.000009000) lib.eal.memzone.free: { cpu_id = 0x0, name = "dpdk-testpmd" }, { name = "RG_MP_mb_pool_0", addr = 0x10125DB80, rc = 0 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    包含 lib.eal.memzone.reserve 的项目为 memzone 创建的 trace 日志,包含 lib.eal.memzone.free 的项目为 memzone free 的日志,找到只有创建没有 free 的 memzone 就相当于找到了泄露的位置。

    这种方式也是一种通用的定位此种 dpdk 内部数据结构泄露的方法,当相关数据结构的申请与释放接口支持 trace point 时可以直接使用,不支持时可以手动添加 trace point 来使用。

    总结

    许多问题的复杂性不在于问题本身,而是难以获取到足够根因闭环的信息。这些信息的缺失一部分来源于框架自身实现与信息收集工具的缺失,另一部分来自于维护者对框架的理解的不足。

    对这两点不足进行反思并持续改进,当维护者对框架实现了然于胸,可用的工具也一抓一大把时,许多看似困难的问题将会迎刃而解。

  • 相关阅读:
    身为产品经理该如何向客户推广API商品数据接口
    单源最短路径问题(Java)
    【Lua基础 第5章】 unpack()和pack()、Lua 中的文件 I/O、简单模式下io的部分方法、完全模式下file的部分方法、日期和时间、闭包使用
    1.屏幕分类和刘海屏的适配原则
    数据结构第三篇【链表的相关知识点一及在线OJ习题】
    JVM与JMM的关系
    园子开店记:被智能的淘宝处罚,说是“预防性的违规”
    LeetCode算法练习top100:(3)矩阵
    springboot快速入门一篇文章全
    memset会导致一大块内存进cache吗
  • 原文地址:https://blog.csdn.net/Longyu_wlz/article/details/127819374