背景
最近排查和解决了几处 HybridDB for PostgreSQL 内存泄漏的BUG。觉得有一定通用性。 这期分享给大家一些实现细节和小技巧。
阿里云上的 HybridDB for PostgreSQL 是基于 PostgreSQL 开发,定位于 OLAP 场景的 MPP 架构数据库集群。它不少的内部机制沿用了 PostgreSQL 的实现。其中就包括了内存管理机制 MemoryContext。
一:PostgreSQL 内存管理机制
PostgreSQL 对内存的使用方式主要分两大块
1. shared_buffer 和同类 buffer。 简单的说 shared_buffer 用于存放数据页面对应数据文件中的 block,这部分内存是 PostgreSQL 中各进程共享。这部分不在本文讨论。 2. MemoryContext 以功能为单位组织起来的树形数据结构,不同的阶段使用不同的 MemoryContext。
1. MemoryContext 的作用
简单的说 MemoryContext 的存在是为了更清晰的管理内存
- 合理管理碎片小内存。频繁的向 OS 申请和释放内存效率是很差的。MemoryContext 会以 trunk 为单位向 OS 申请成块的内存,并管理起来。当程序需求小内存时从 trunk 中分配,用完后归还给对应的 MemoryContext ,并不归还给 OS。
- 赋予内存功能和生命周期属性以功能为单位管理内存。不同功能和阶段使用对应的 MemoryContext。
- TopTransactionContext:一个事务的生命周期,事务管理相关数据放在 TopTransactionContext,当一个事务提交时该上下文被整个释放。
- ExprContext PostgreSQL 以行为单位处理数据,每一行数据的表达式计算都会在 ExprContext 完成,每处理完一行都会重置对应的 ExprContext。
- 树形的 MemoryContext 结构不同功能间的 MemoryContext 是以为树为单位组织起来的
- 每个数据库后端进程顶层是 TopMemoryContext
- TopMemoryContext 下有很多子 Context缓存相关的 CacheMemoryContext;
- 本地锁相关的 LOCALLOCK hash;
- 当前事务相关的 TopTransactionContext
- 注意 CacheMemoryContext 为何不属于 TopTransactionContext,那是由于 Cache 是独立于事务存在的,事务提交不影响 Cache 的存在。
- 删除或重置一个 MemoryContext,它的子 MemoryContext 也一并被删除或重置。
- 2. 不同模块的 MemoryContext
你可能明白了,实现不同的模块时,对待内存的方式可能区别很大。 比如:
1. 执行器在做表达式计算时,一些诸如字符串类型数据处理的函数,大多会比较随意的使用 palloc 分配内存,但直到函数返回,却并没有释放它们。 2. 在处理缓存模块处理数据时,却倍加小心的释放内存。
这是由于:
1. 执行器对数据的处理是以行为单位,都在 ExprContext 中,每处理完一行,会重置 ExprContext,以此释放相关的内存。 2. 缓存的生命周期很长,不会定期重置整个 MemoryContext。哪怕少量的内存泄漏,积攒的后果都很严重。这部分的实现容易出问题,也不好排查。
3. 常见的内存问题
虽然有很好的内存管理机制,但进程中内存间没有强隔离,也可能出现内存问题。
造成内存泄漏的原因很大可能是:
1. 在较长生存周期的 MemoryContext 中正常处理流程中没有释放内存。 2. 由于发生了异常,跳转到在异常处理阶段没有释放内存。 3. 没有使用内存管理机制,使用 OS 调用 malloc,free 处理内存(某些实现不合理的插件中可能出现)。 4. 在不正确的 MemoryContext 分配了内存,导致内存泄漏或数据丢失。 5. 写内存越界,这是最难找的问题,很容易造成数据库崩溃。
4. 问题排查小技巧
针对内存泄漏,常用两种方法排查
1. valgrind 最常见的大杀器,开发人员都懂的。这里就不详细介绍了。
2. 使用 GDB 也能大致定位问题
2.1 这是一段脚本,我们把它保存成文本文件(pg_debug_cmd)
define sum_context_blocks
set $context = $arg0 set $block = ((AllocSet) $context)->blocks
set $size = 0 while ($block)
set $size = $size + (((AllocBlock) $block)->endptr - ((char *) $block))
set $block = ((AllocBlock) $block)->next
end
printf "%s: %d\n",((MemoryContext)$context)->name, $size
end
define walk_contexts
set $parent_$arg0 = ($arg1)
set $indent_$arg0 = ($arg0)
set $i_$arg0 = $indent_$arg0 while ($i_$arg0)
printf " " set $i_$arg0 = $i_$arg0 - 1
end
sum_context_blocks $parent_$arg0 set $child_$arg0 = ((MemoryContext) $parent_$arg0)->firstchild
set $indent_$arg0 = $indent_$arg0 + 1 while ($child_$arg0)
walk_contexts $indent_$arg0 $child_$arg0 set $child_$arg0 = ((MemoryContext) $child_$arg0)->nextchild
end
end
walk_contexts 0 TopMemoryContext
2.2 获得疑似内存泄漏的进程PID,定时触发执行下面的 shell
gdb -p $PID < pg_debug_cmd > memchek/MemoryContextInfo_$(time).log
2.3 分析日志文件
日志文件以 MemoryContext 树的形式展示了一个时间点该进程的内存分配情况。根据时间的积累,可以很容易判断出哪一些 MemoryContext 可能存在异常,从而为内存泄漏指明一个方向。
(gdb)
TopMemoryContext: 149616
pgstat TabStatusArray lookup hash table: 8192
TopTransactionContext: 8192
TableSpace cache: 8192
Type information cache: 24480
Operator lookup cache: 24576
MessageContext: 32768
Operator class cache: 8192
smgr relation table: 24576
TransactionAbortContext: 32768
Portal hash: 8192
PortalMemory: 8192
PortalHeapMemory: 1024
ExecutorState: 24576
SRF multi-call context: 1024
ExprContext: 0
ExprContext: 0
ExprContext: 0
Relcache by OID: 24576
CacheMemoryContext: 1040384
pg_toast_2619_index: 1024
....
pg_authid_rolname_index: 1024
WAL record construction: 49776
PrivateRefCount: 8192
MdSmgr: 8192
LOCALLOCK hash: 8192
Timezones: 104128
ErrorContext: 8192
最后,文章的参考资料中也提供了一种类似的方法,供各位参考。
总结
PostgreSQL 内存管理机制的实现比较复杂,但用起来确却很简单,有一种特别的美感,推荐大家了解一下。