为什么DPDK启动时创建Mempool要几十秒?——一次启动超时故障,彻底搞懂HugePage、Memseg与Memory Populate (上)
一、一次令人费解的启动故障某运营商部署了一套基于DPDK 25.11.2开发的高性能数据交换平台。服务器配置如下项目配置CPUIntel Xeon Gold 6430 ×2内存512GB DDR5HugePage128GB2MB HugePage网卡Intel E810 100G ×4NUMA双Socket系统上线前所有性能测试均顺利通过。但是正式交付后运维人员反馈了一个奇怪的问题每次系统重启后需要等待二十多秒业务才能真正开始转发数据。查看启动日志EAL: Detected CPU lcores: 128 EAL: Detected NUMA nodes: 2 ...... Creating mbuf pool ...随后日志便停止输出。CPU 使用率只有 2%4%。没有异常日志。没有死锁。没有 Crash。二十多秒以后Creating mbuf pool ... Done Port 0 started Port 1 started Worker started系统立即恢复正常。这种现象十分容易误导开发人员。很多人的第一反应都是HugePage 不够NUMA 配置错误网卡初始化慢SSD 太慢BIOS 配置有问题然而逐项排查之后HugePagecat /proc/meminfo | grep Huge输出HugePages_Total: 65536 HugePages_Free : 65536 HugePages_Rsvd : 0 Hugepagesize : 2048 kBHugePage 完全正常。NUMAdpdk-proc-info --mem查看所有 HugePage 均位于本地 Socket。CPUtop启动期间CPU Usage 2% 3% 4%几乎没有负载。如果 CPU 并没有忙那么二十多秒到底花在了哪里二、Perf 给出的答案令人意外为了找到真正耗时的位置我们使用perf对整个启动过程进行采样perf record -g ./dpdk_app随后生成火焰图。得到的结果几乎全部集中在下面这一条调用链附近main() └── rte_pktmbuf_pool_create() └── rte_pktmbuf_pool_create_by_ops() └── rte_mempool_create() └── rte_mempool_populate_default()占比超过82%也就是说整个程序启动过程中绝大部分时间都耗费在创建 Mempool。看到这里很多人又会产生一个误解不就是申请了一块 HugePage 吗实际上。这正是很多 DPDK 开发者最大的认知误区。三、Mempool 创建并不是 malloc()对于普通 Linux 程序而言buf malloc(size);真正做的事情非常简单找到空闲虚拟地址建立映射返回指针很多情况下甚至采用 Lazy Allocation。真正访问内存时才会分配物理页。但是DPDK 完全不是这样。例如struct rte_mempool *pool; pool rte_pktmbuf_pool_create( MBUF_POOL, 1048576, 512, 0, RTE_MBUF_DEFAULT_BUF_SIZE, socket_id);很多人看到1048576会认为创建了一百万个 mbuf。其实真正发生的事情远比这复杂。在这一句 API 返回之前DPDK 已经完成了一系列工作创建 Mempool 元数据计算对象布局Object Layout选择 Mempool Backend从 EAL 已建立的内存体系中获取可用内存将连续 HugePage 切分成百万级对象初始化每一个对象初始化每一个rte_mbuf建立 Per-lcore Cache将对象加入 Backend 管理结构。直到最后应用程序拿到的才只是struct rte_mempool *也就是说真正耗时的并不是申请内存而是构造一个能够支持千万级高速分配的数据结构。四、重新认识 DPDK 的内存体系很多文章介绍 DPDK 内存时都会画出下面这样的示意图HugePage ↓ Mempool ↓ mbuf虽然没有错。但是它忽略了整个 EAL 最重要的一层Memseg。实际上在 DPDK 25.11 中内存管理关系更接近下面这样。真正的层次关系是Linux HugePage │ ▼ EAL Memory Initialization │ ▼ Memseg List │ ▼ Memzone │ ▼ Mempool │ ▼ mbuf Object这里有一个非常重要的概念Mempool 并不会直接向 Linux 申请 HugePage。很多人第一次看到这里都会觉得意外。事实上在rte_eal_init()执行期间DPDK 已经完成了 HugePage 的发现、映射以及组织工作。也就是说当程序真正执行rte_pktmbuf_pool_create(...)的时候。所有 HugePage 已经存在。Mempool 只是从 EAL 已经建立好的内存体系中申请一块连续内存并把它加工成可高速分配的对象池。理解这一点是理解整个 DPDK 内存管理架构的关键。五、真正的主角其实是 EAL很多开发者认为程序启动流程是main() ↓ rte_pktmbuf_pool_create() ↓ HugePage实际上。真正发生的是main() ↓ rte_eal_init() ↓ 扫描 HugePage ↓ 建立 Memseg List ↓ 建立 Heap ↓ 建立 Memzone 管理器 ↓ 初始化 IOVA ↓ ...... ↓ rte_pktmbuf_pool_create()也就是说。当 Mempool 创建开始时整个 DPDK 内存世界实际上已经准备好了。那为什么创建一个对象池。仍然需要二十多秒答案就在Memory Populate。很多文章都会简单介绍Populate 就是填充对象。实际上。这是整个启动过程中最容易被低估、也是最耗时的阶段。六、Populate 到底在做什么假设我们创建1,048,576 个 mbuf每个对象占用2304 Bytes那么DPDK 需要处理超过2.3 GB连续 HugePage 空间。但是Populate 并不是简单地for (...) { obj; }对于每一个对象。DPDK 都需要完成类似工作对象地址计算 ↓ Object Header 初始化 ↓ Cookie 初始化 ↓ rte_mbuf 初始化 ↓ Private Area 初始化 ↓ Headroom 设置 ↓ 加入 Backend 空闲对象管理结构如果对象数量达到100 万那么上述流程需要执行100 万次。很多开发者看到这里会觉得一百万次循环对于今天的 CPU 来说应该只是几十毫秒。为什么会慢到二十多秒原因是真正耗时的并不是对象初始化本身。而是CPU 第一次访问这一大片内存时整个缓存体系、TLB、页表以及内存控制器开始真正工作。也就是说。Populate 不仅是在初始化对象。更是在完成整个 HugePage 区域的First Touch首次触碰。直到这里真正的幕后主角才开始登场。未完待续