PostgreSQL内存上下文
PG使用共享内存在多进程之间进行数据共享。使用动态共享内存段dynamic shared memory segments在并行workers之间进行数据交换,这个内存在启动时分配固定大小。但是PG后端进程必须管理私有内存用于处理SQL语句。本文,介绍PG如何使用memory context,即内存上下文,来管理私有内存;以及如何检查内存使用情况。这对于编写服务器代码的人来说很有意思,但我要重点关注用户如何理解和调试SQL语句的内存消耗。
1.什么是内存上下文
PG由C语言编写,C语言的内存管理比较棘手,必须显式释放所有动态分配的内存。这就特别容易造成内存泄漏了,导致不断增加内存消耗。对于PG后端这样长期存在的进程来说是致命的。
为了减少内存泄漏,PG使用内存上下文管理自己的内存。内存上下文是可以按需增长的内存块。在PG中不直接调用malloc申请内存,而是从内存上下文中申请。根据需要,PG会扩展内存上下文。
内存上下文的优势:可以通过删除内存上下文,一次性释放所有内存。这就意味着不再需要追踪分配的内存,关注什么时候释放了,简化了内存管理,降低了内存泄漏的风险。PG查询执行器在开始处理一个语句时,创建ExecutorState context。如果需要申请内存,则从该内存上下文中申请。语句执行完时,执行器会删除ExecutorState,在查询执行结束后,不必担心内存泄漏。源码src/backend/utils/mmgr/README中详细介绍了内存上下文的设计与使用。
2.内存上下文的组织
内存上下文形成一个层次结构。最顶层的内存上下文是TopMemoryContext,存在于后台进程的整个生命周期。其他任何一个内存上下文都有一个父节点。当删除一个内存上下文时,会递归删除所有后继内存上下文。因此,不需要频繁明确地释放内存。如果在较短时间内需要几个内存块,例如处理执行计划的某个步骤,可以在ExecutorState中再创建一个内存上下文,在该步骤执行完时将其删除。如果执行器在此之前终止,则该内存上下文中任何内存都不会泄漏。
重要的内存上下文
TopMemoryContext |
内存上下文的最顶层,不需要删除。 |
CacheMemoryContext |
包含数据库元数据的缓存以及执行计划的换岑。如果数据库包含多个对象(例如表分区),或者有许多prepared语句,则会占用更多空间 |
MessageContext |
包含来自客户端的语句,有时还包含执行计划和解析数据 |
PortalContext |
与当前语句关联的内存(称为portal或者cursor) |
3.一个SQL语句使用多少内存
理论上执行计划的每一步都会被work_mem限制,但是不足以评估内存的使用。
1、单个语句可能有很多内存密集型执行步骤,因此会分配work_mem多次;
2、如果语句使用并行查询,会创建动态共享内存段,work_mem并不统计这个;
3、PG13之前,bytea二进制数据或者大PostGIS几何图形,会驻留在内存中,也不被work_mem限制
有一些方法可以帮助查看内存上下文中存储了多少内存。
3.1 pg_backend_memory_contexts查看内存上下文使用
pg_backend_memory_context视图限制了当前会话拥有的所有内存上下文。只能在语句之间查询该视图,但在执行SQL时查看才会更有用。为此,可以创建一个函数,将其构建到SQL语句中:
CREATE FU CREATE FUNCTION dump_my_mem() RETURNS void LANGUAGE plpgsql AS $$DECLARE r record; BEGIN FOR r IN SELECT name, ident, level, total_bytes FROM pg_backend_memory_contexts LOOP RAISE NOTICE '% % % %', repeat(' ', r.level - 1), r.name, r.total_bytes, r.ident; END LOOP; END;$$;
3.2 pg_log_backend_memory_contexts()记录内存上下文使用
pg_log_backend_memory_context(integer)函数可以将任意会话的内存上下文当前状态写入日志文件。参数是进程ID,可以通过pg_stat_activity查看。默认仅超级用户可以调用整个函数,但是你可以GRANT EXECUTE权限给其他用户。
通过这种方法,可以方便地检查长时间运行SQL的内存使用。问题是一个消耗大量内存的语句不需要长时间运行。捕捉一个简短的语句比较棘手。
3.3 debug来记录内存使用
如果想要检查内存使用,可以通过debug的方式精确控制语句的执行点。但需要熟悉PG代码,并gdb一个进程。
首先看下进程ID,我们使用12345作为一个例子:
gdb /path/to/postgresql/bin/postgres 12345 GNU gdb (GDB) Fedora Linux 13.1-3.fc37 Copyright (C) 2023 Free Software Foundation, Inc. [...] (gdb)
然后打个断点,一个有用的函数是ExecutorEnd:PG处理一个语句结束点:
(gdb) break ExecutorEnd Breakpoint 1 at 0x783271: file execMain.c, line 471. (gdb) cont Continuing.
执行有问题的语句,一旦执行到断点,就会触发内存上下文的dump:
Breakpoint 1, ExecutorEnd (queryDesc=0x2333fd8) at execMain.c:471 471 if (ExecutorEnd_hook) (gdb) print MemoryContextStats(TopMemoryContext) $1 = void
这会将内存上下文转储到日志文件。然后可以detach该进程,退出GDB:
(gdb) detach Detaching from program: /path/to/postgresql/bin/postgres, process 12345 [Inferior 1 (process 12345) detached] (gdb) quit
4.评估PG总内存使用
一个繁忙的数据库将有许多会话同时运行,很难说会有多少连接,以及他们执行的是简单还是复杂的语句。因此很难预测到PG使用多少内存。恰当地说,你所知道的work_mem的一切都是错误的,很显然Christophe Pettus提出了自己的公式:
50%的free memory + 文件系统buffers/连接数
可以看到,连接数有着至关重要的作用。如果想获得良好性能,需要使用大小合适的连接池。毕竟,足够大的work_mem是non-trival SQL语句良好性能的重要条件。
5.PG内存不足
我们当然不想遇到内存不足的情况,但是一旦发生,后果很大程度上取决于如何配置操作系统内核。使用默认配置,Linux将在内存耗尽时调用“out-of memory killer”。这个不友好的内核组件将向某些后台进程发送SIGKILL信号,无条件终止进程并释放内存。PG进程过早死亡,会断开所有连接,并导致崩溃恢复。崩溃恢复意味着直到PG恢复到上次最近的checkpoint,才能对外服务。
避免这种崩溃的正确方法是:设置内核参数vm.overcommit_memory到2和调整vm.overcommit_ratio。然后回得到一个常规“out of memory”错误,PG会将内存上下文dump到日志文件。该内存上下文转储非常有用,有助于理解后格SQL在哪里分配了所有的内存。
6.总结
拥有PG如何使用内存上下文管理私有内存的概念非常重要,即使你不是一个内核开发者。正确配置有助于理解内存上下文,同时也介绍了一些视图和函数来帮助检查内存上下文。
原文
https://www.cybertec-postgresql.com/en/memory-context-for-postgresql-memory-management/