暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ptmalloc2 在 PostgreSQL 引起的内存无效占用问题

原创 内核开发者 2025-02-26
196

问题的发现

在某 PolarDB-PostgreSQL 实例中频繁出现 Out-of-Memory(OOM)故障。通过监控工具发现,尽管进程的总内存占用较高,但 MemoryContext 所管理的内存使用量却相对较少。初步推测可能是内存泄漏导致的问题。

为此,我们首先通过分析进程的 smaps 文件,确认内存泄漏主要发生在堆区(Heap)。随后,利用 perf 工具锁定了几处关键的内存分配位置,并结合 PostgreSQL 源代码进行深入排查,但未能找到明确的泄漏点。

在同事的建议下,我们使用了 malloc_stats 函数对 malloc 的获取了heap下的详细信息。结果显示,系统内存占用较高,这并非由内存泄漏引起。经过进一步研究,我们了解到这是由于 malloc 自身的内存缓存机制导致的。为了释放这部分缓存内存,我们调用了 malloc_trim 函数,强制清理了未使用的缓存区域,从而有效缓解了 OOM 问题。

ptmalloc2内存申请释放特点

要想明白问题的原因,需要了解下malloc的内存申请和释放逻辑。

Linux是使用的虚拟内存,以 32 位机器为例,首先被载入的是 .text 段,然后是 .data 段,最后是 .bss 段。上面的空间供内核使用,应用程序不可以直接访问。应用程序的堆栈从最高地址处开始向下生长,.bss 段与栈之间的空间是空闲的,空闲空间被分成两部分,一部分为 heap,一部分为 mmap 映射区域。Heap 和 mmap 区域都可以供用户自由使用,但是它在刚开始的时候并没有映射到内存空间内,是不可访问的。

在向内核请求分配 heap, mmap 空间之前,对这些空间进行访问会导致 segmentation fault。用户程序可以直接使用系统调用来管理 heap 和 mmap 映射区域,但更多的时候程序都是使用 C 语言提供的 malloc() 和 free() 函数来动态的分配和释放内存。-- 《Glibc 内存管理 Ptmalloc2 源代码分析》

在内存管理领域,堆(Heap)操作通常由 glibc 提供的 malloc 函数族实现。常见的 malloc 实现包括 ptmalloc2、tcmalloc 和 jemalloc 等。在 PostgreSQL 中,默认采用的是 ptmalloc2 实现。ptmalloc2 的内存分配与释放机制具有以下特点:

  1. 成对的 malloc/free 调用:malloc 和 free 函数必须成对使用。用户在申请内存后,需根据实际需求在合适的位置调用 free 进行内存释放,以避免内存泄漏。
  2. Heap 和 Mmap 内存分配机制:用户程序可使用的堆内存分为两部分:Heap 和 Mmap。Heap 内存由低位地址向高位地址分配,而 Mmap 内存则由高位地址向低位地址分配。在单进程单线程架构的程序中,Heap 内存通常表现为一段连续的地址空间。Heap 的操作实际上是通过调用 sbrk 系统调用来实现的,而 sbrk 涉及较多的内核态操作。因此,频繁的小块内存分配会显著影响程序性能。
  3. 基于 Chunk 的内存管理:ptmalloc2 通过 Chunk(内存块)来管理内存分配。其中,最高位的 Chunk 被称为 Top Chunk。ptmalloc2 在收缩内存时,会从 Top Chunk 开始操作。如果与 Top Chunk 相邻的 Chunk 无法释放,则 Top Chunk 以下的所有 Chunk 均无法释放。因此,在进行内存分配时,应避免将生命周期较长的内存申请提前,以防止内存无法有效释放。
  4. Free 后的内存管理策略:free 函数释放的内存不会立即归还给操作系统,而是首先被放入内部的 Bins(回收池)中。ptmalloc2 会根据其内部策略对这些内存块进行合并和释放操作。因此,可能会出现 free 后内存未立即归还给操作系统,从而导致内存占用量持续增长的现象。

PostgreSQL的MemoryContext

在《图解 PolarDB PostgreSQL 版 MemoryContext》一文中,深入探讨了 PostgreSQL 的 MemoryContext 体系。该体系本质上是基于 PostgreSQL 内部逻辑,通过底层调用 malloc/free 接口来实现内存管理:

  1. 降低内存泄漏风险:鉴于 PostgreSQL 源码的复杂性,MemoryContext 的引入能够有效降低内存泄漏的可能性。通过合理的内存分配与释放机制,确保内存资源在使用过程中得到严格管控,从而避免因代码复杂性导致的内存泄漏问题;
  2. 优化性能开销:MemoryContext 根据不同功能模块的内存使用特性,精准设置内存申请的大小和释放时机。这种精细化的管理策略能够有效避免频繁调用 malloc/free 所带来的性能开销,从而显著提升系统的整体性能表现;
  3. 避免内存碎片化:通过将不同生命周期的内存分配到相同的位置进行统一管理,MemoryContext 能够有效避免因 malloc 内存分配导致的碎片化问题。这种集中管理的方式确保了内存的高效利用,同时减少了因内存碎片化可能引发的内存无法释放的问题;

PostgreSQL的MemoryContext已经根据malloc的特点进行了多项优化,但是在缓存上缺少应对的手段。

解决方案

在深入理解了 malloc 的内存分配与释放机制后,我们可以明确:上述内存问题的根本原因在于 PostgreSQL 调用 free 后,由于 ptmalloc2 的内部逻辑未能立即将这部分内存归还给操作系统,从而导致了内存占用的持续增长。虽然 malloc 提供了 malloc_trim 接口用于解决此类问题,但该接口存在一定的局限性。具体来说,malloc_trim 可以强制释放存储在 Bins 中的 Chunk,但如果 Top Chunk 被占用,malloc_trim 仍然无法释放内存。

针对这一问题,可以采用以下几种解决方案:

  1. 使用 GDB 强制调用 malloc_trim

通过 gdb 附加到目标进程,并调用 malloc_trim 来强制释放内存。这种方法虽然有效,但属于较为“侵入式”的操作,可能会对程序的正常运行产生一定影响。具体操作如下:

gdb -p <进程PID>
(gdb) call malloc_trim(0)

然而,这种方法仅适用于临时性的问题排查,不建议作为长期解决方案;

  1. 通过信号异步调用 malloc_trim

一种更为优雅的解决方案是通过向目标进程发送信号,触发 malloc_trim 函数的调用。这种方式可以在不中断程序正常运行的前提下,异步释放内存。在邮件列表中提到的“Trim the heap free memory”方案中,已经展示了通过信号触发 malloc_trim 的可行性。具体实现可以参考以下伪代码:

void handle_signal(int sig) {
malloc_trim(0);
}

// 在程序初始化时注册信号处理函数
signal(SIGUSR1, handle_signal);

通过向目标进程发送 SIGUSR1 信号,可以触发 malloc_trim 的调用,从而释放内存。进而可以增加一些进程来监控内存的情况,定期对进程进行发送信号,以达到自动化的能力;

在PostgreSQL邮件列表内Trim the heap free memory提供了问题复现的方法和具体的代码,问题还在持续讨论中,欢迎大家积极参与讨论;

  1. 优化内存管理策略

从长远来看,最佳的解决方案是将 PostgreSQL 的 MemoryContext 机制与 ptmalloc2 的内存释放策略进行深度融合。通过优化两者的协同工作,避免无效的内存释放操作,从而从根本上解决此类问题。具体优化方向包括:

  1. 调整 MemoryContext 的内存分配策略:根据 ptmalloc2 的特性,优化 MemoryContext 的内存分配顺序,避免长生命周期的内存申请过早占用 Top Chunk;
  2. 利用 ptmalloc2 的现有机制:深入研究 ptmalloc2 的内部逻辑,通过合理的内存分配和释放策略,减少内存碎片化,从而降低对 malloc_trim 的依赖;
  3. 修改 ptmalloc2 源码:如果上述优化手段仍无法满足需求,可以考虑修改 ptmalloc2 的源码,以更好地适配 PostgreSQL 的内存管理需求。但需要注意的是,修改底层内存分配器的源码可能会引入新的兼容性问题,需谨慎操作;
  4. 替换内存分配器

如果上述优化手段仍无法有效解决问题,可以考虑使用其他内存分配器(如 jemalloc 或 tcmalloc)来替代 ptmalloc2。这些内存分配器在多线程场景下表现优异,但在单进程单线程场景下可能会出现内存膨胀的问题。因此,在选择替代方案时,需要根据实际应用场景进行权衡。具体建议如下:

  1. 评估内存分配器的适用性:在引入 jemalloc 或 tcmalloc 之前,需对其在单进程单线程场景下的性能表现进行充分评估,确保其能够有效解决当前问题,同时不会引入新的性能瓶颈;
  2. 进行兼容性测试:在实际部署前,需对 PostgreSQL 进行全面的兼容性测试,确保新内存分配器与现有代码无缝集成;

综上所述,解决 PostgreSQL 中由 ptmalloc2 引起的内存问题,需要从多个角度入手。在短期内,可以通过 GDB 或信号触发 malloc_trim 来缓解问题;但从长远来看,优化内存管理策略或更换内存分配器将是更为有效的解决方案。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论