1.背景
在客户线上环境中,发现pgsql的startup进程疑似存在内存泄漏:
注意这里不是单纯的RES增长,RES和SHR如果正常增长并不代表故障。只有RES - SHR这个差值持续增长才说明可能出现故障。
PG的内存有两部分:local_memory和shared_memory。shared_memory由PG服务启动时分配,供所有进程使用。
而top命令的RES虽然表示常驻内存,但共享内存也会被计入其中。虽然进程可能只使用几个共享库的函数,但SHR会统计整个共享库的大小。
而RES - SHR才是进程独占的内存大小。
2.定位
其实,查看pg源码,可以在源码中找到大量palloc调用但之后没有pfree的情况,是否这里都导致内存泄露呢?
其实不然,pg正是为了更好地管理内存,避免忘记pfree导致内存泄露的情况,引入MemoryContext对内存进行管理。
MemoryContext的本质就是对内存进行分类和分层。
如用户发出一条SQL语句,需要本地缓存语句,语法解析树等也需要缓存:MessageContext。
如不常改变的catalog relation等可以本地缓存,无需每次从磁盘读取:CacheMemoryContext。
如处理事务时,涉及元组扫描、索引扫描、元组排序,其结果可能需要事务结束后才释放:CurTransactionContext、ExecutorState、PortalHeapMemory。
pg将各种内存整理好,让系统中的内存分配操作都在各种语义对应的内存上下文中进行,因此可以通过释放内存上下文来释放其中的所有内存,无需每次palloc之后显式进行pfree。
TopMemoryContext作为根节点。因此可以通过打印内存上下文的变化定位到内存泄漏的具体位置。
ps -ef | grep startup
##gdb进去后 startup 进程内存增长会停住,打印完mctx后需要退出并等待增长再重新gdb进
gdb attach pid
##打印内存上下文
p MemoryContextStats(TopMemoryContext)
##在日志中查看gdb中打印的上下文
vi /data1/tangyujie/tdata/snd02/pg_log/postgres_XXX.log
实际调试时,TopMemoryContext total通常不变,used先增长,这里因为还有free没用完,在上下文中申请内存时先从free中分配。
观测到主要增长在TopMemoryContext中,这里使用gdb脚本,打条件断点,找到所有在TopMemoryContext这个上下文中使用palloc申请内存和pfree释放的函数栈。
# 在附加到进程之后,设置条件断点并将打印的信息保存到文档
# 设置堆栈信息输出文件
set logging file /data1/tangyujie/tdata/sdn02/stack_trace_1011.log
set logging on
# 附加到指定的进程
attach 159285
# 定义一个条件断点,在 palloc 函数中检查内存上下文
break palloc if CurrentMemoryContext == TopMemoryContext
break pfree if CurrentMemoryContext == TopMemoryContext
# 初始化 size 总和为 0
set $total_size = 0
# 进入循环,打印堆栈并继续执行程序,直到不经过断点时跳出循环
while 1
# 继续执行程序
continue
# 检查是否通过了断点
info breakpoints
# 如果不经过断点,则退出循环
if $bpnum == -1
p $total_size
break
end
# 将 size 变量的值添加到总和中
set $total_size = $total_size + size
p $total_size
# 打印函数调用堆栈到日志文件
bt
# 将输出刷新到日志文件
shell echo "" >> /data1/tangyujie/tdata/sdn02/stack_trace_1011.log
end
# 关闭日志记录
set logging off
在第一天错误将RES和SHR上涨即认为故障的情况下,跟踪了一个实际没有故障的进程,主要抓到的三个函数栈如下:
该栈palloc之后有pfree操作,通过打印内存上下文验证,palloc(1024)之后used增长1040 Bytes,pfree之后释放干净。
该栈palloc之后也有pfree,打印上下文验证,palloc(8192)申请了8208 Bytes,pfree之后全部释放干净。
三个栈都没有引入内存泄露。由于这个进程其实本来就没有复现故障,这里没有泄露才是预期行为。
后续重新复现问题后,按照上述操作抓栈,找到一个palloc后没有pfree的栈如下:
这里palloc(128)之后没有pfree,打印上下文验证,每经过一次这个栈调用后,TopMemoryContext used增长144 Bytes,确实存在memory leak,虽然PG有内存上下文管理内存,但由于slave节点的startup进程一直运行,因此这里在TopMemoryContext里palloc的内存直到进程结束才会一起释放。