zl程序教程

您现在的位置是:首页 >  硬件

当前栏目

PostgreSQL relcache在长连接应用中的内存霸占"坑"

2023-09-14 09:04:38 时间

阿里巴巴内部的某业务在使用阿里云RDS PG时,业务线细心的DBA发现,一些长连接占据了大量的内存没有释放。后来找到了复现的方法。使用场景有些极端。

有阿里巴巴内部业务这样的老湿机陪伴的RDS PG,是很靠谱的。

PostgreSQL 缓存

除了常见的执行计划缓存、数据缓存,PostgreSQL为了提高生成执行计划的效率,还提供了catalog, relation等缓存机制。

PostgreSQL 9.5支持的缓存代码如下

ll src/backend/utils/cache/

attoptcache.c catcache.c evtcache.c inval.c lsyscache.c plancache.c relcache.c relfilenodemap.c relmapper.c spccache.c syscache.c ts_cache.c typcache.c
长连接的缓存问题

这些缓存中,某些缓存是不会主动释放的,因此可能导致长连接霸占大量的内存不释放。

通常,长连接的应用,一个连接可能给多个客户端会话使用过,访问到大量catalog的可能性非常大。所以此类的内存占用比是非常高的。

有什么影响呢?
如果长连接很多,而且每个都霸占大量的内存,你的内存很快会被大量的连接耗光,出现OOM是避免不了的。
而实际上,这些内存可能大部分都是relcache的(还有一些其他的),要用到内存时,这些relcache完全可以释放出来,腾出内存空间,而没有必要被持久霸占。


在数据库中存在大量的表,PostgreSQL会缓存当前会话访问过的对象的元数据,如果某个会话从启动以来,对数据库中所有的对象都有过查询的动作,那么这个会话需要将所有的对象定义都缓存起来,会占用较大的内存,占用的内存大小与一共访问了多少站该对象有关。

复现方法(截取自stackoverflow某个问题),创建大量的对象,访问大量的对象,从而造成会话的relcache等迅速增长。
创建大量的对象
functions :
-- MTDB_destroy

CREATE OR REPLACE FUNCTION public.mtdb_destroy(schemanameprefix character varying)

 RETURNS integer

 LANGUAGE plpgsql

AS $function$

declare

 curs1 cursor(prefix varchar) is select schema_name from information_schema.schemata where schema_name like prefix || %;

 schemaName varchar(100);

 count integer;

begin

 count := 0;

 open curs1(schemaNamePrefix);

 loop

 fetch curs1 into schemaName;

 if not found then exit; end if; 

 count := count + 1;

 execute drop schema  || schemaName ||  cascade;;

 end loop; 

 close curs1;

 return count;

end $function$;

-- MTDB_Initialize

CREATE OR REPLACE FUNCTION public.mtdb_initialize(schemanameprefix character varying, numberofschemas integer, numberoftablesperschema integer, createviewforeachtable boolean)

 RETURNS integer

 LANGUAGE plpgsql

AS $function$

declare 

 currentSchemaId integer;

 currentTableId integer;

 currentSchemaName varchar(100);

 currentTableName varchar(100);

 currentViewName varchar(100);

 count integer;

begin

 -- clear

 perform MTDB_Destroy(schemaNamePrefix);

 count := 0;

 currentSchemaId := 1;

 loop

 currentSchemaName := schemaNamePrefix || ltrim(currentSchemaId::varchar(10));

 execute create schema  || currentSchemaName;

 currentTableId := 1;

 loop

 currentTableName := currentSchemaName || . || table || ltrim(currentTableId::varchar(10));

 execute create table  || currentTableName ||  (f1 integer, f2 integer, f3 varchar(100), f4 varchar(100), f5 varchar(100), f6 varchar(100), f7 boolean, f8 boolean, f9 integer, f10 integer);

 if (createViewForEachTable = true) then

 currentViewName := currentSchemaName || . || view || ltrim(currentTableId::varchar(10));

 execute create view  || currentViewName ||  as  ||

 select t1.* from  || currentTableName ||  t1  ||

  inner join  || currentTableName ||  t2 on (t1.f1 = t2.f1)  ||

  inner join  || currentTableName ||  t3 on (t2.f2 = t3.f2)  ||

  inner join  || currentTableName ||  t4 on (t3.f3 = t4.f3)  ||

  inner join  || currentTableName ||  t5 on (t4.f4 = t5.f4)  ||

  inner join  || currentTableName ||  t6 on (t5.f5 = t6.f5)  ||

  inner join  || currentTableName ||  t7 on (t6.f6 = t7.f6)  ||

  inner join  || currentTableName ||  t8 on (t7.f7 = t8.f7)  ||

  inner join  || currentTableName ||  t9 on (t8.f8 = t9.f8)  ||

  inner join  || currentTableName ||  t10 on (t9.f9 = t10.f9) ; 

 end if;

 currentTableId := currentTableId + 1;

 count := count + 1;

 if (currentTableId numberOfTablesPerSchema) then exit; end if;

 end loop; 

 currentSchemaId := currentSchemaId + 1;

 if (currentSchemaId numberOfSchemas) then exit; end if; 

 end loop;

 return count;

END $function$;

在一个会话中访问所有的对象
-- MTDB_RunTests

CREATE OR REPLACE FUNCTION public.mtdb_runtests(schemanameprefix character varying, rounds integer)

 RETURNS integer

 LANGUAGE plpgsql

AS $function$

declare

 curs1 cursor(prefix varchar) is select table_schema || . || table_name from information_schema.tables where table_schema like prefix || % and table_type = VIEW;

 currentViewName varchar(100);

 count integer;

begin

 count := 0;

 loop

 rounds := rounds - 1;

 if (rounds 0) then exit; end if;

 open curs1(schemaNamePrefix);

 loop

 fetch curs1 into currentViewName;

 if not found then exit; end if;

 execute select * from  || currentViewName;

 count := count + 1;

 end loop;

 close curs1;

 end loop;

 return count; 

end $function$;

test SQL:
prepare :
准备对象

postgres=# select MTDB_Initialize(tenant, 100, 1000, true);

访问对象
session 1 :

postgres=# select MTDB_RunTests(tenant, 1);

 mtdb_runtests 

---------------

 100000

(1 row)

访问对象
session 2 :

postgres=# select MTDB_RunTests(tenant, 1);

 mtdb_runtests 

---------------

 100000

(1 row)

观察内存的占用
memory view :

 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

 2536 digoal 20 0 20.829g 0.016t 1.786g S 0.0 25.7 3:08.20 postgres: postgres postgres [local] idle

 2453 digoal 20 0 6854896 187124 142780 S 0.0 0.3 0:00.68 postgres: postgres postgres [local] idle

smem

 PID User Command Swap USS PSS RSS 

 2536 digoal postgres: postgres postgres 0 15022132 15535203 16894900 

 2453 digoal postgres: postgres postgres 0 15022256 15535405 16895100 


.1. 应用层优化建议
对于长连接,建议空闲一段时间后,自动释放连接。
这样的话,即使因为某些原因一些连接访问了大量的对象,也不至于一直占用这些缓存不释放。
我们可以看到pgpool-II的设计,也考虑到了这一点,它会对空闲的server connection设置阈值,或者设置一个连接的使用生命周期,到了就释放重建。

.2. PostgreSQL内核优化建议
优化relcache的管理,为relcache等缓存提供LRU管理机制,限制总的大小,淘汰不经常访问的对象,同时建议提供SQL语法给用户,允许用户自主的释放cache。

阿里云RDS PG正在对内核进行优化,修正目前社区版本PG存在的这个问题。

https://www.postgresql.org/message-id/flat/20160708012833.1419.89062%40wrigleys.postgresql.org#20160708012833.1419.89062@wrigleys.postgresql.org

Every PostgreSQL session holds system data in own cache. Usually this cache

is pretty small (for significant numbers of users). But can be pretty big

if your catalog is untypically big and you touch almost all objects from

catalog in session. A implementation of this cache is simple - there is not

delete or limits. There is not garabage collector (and issue related to

GC), what is great, but the long sessions on big catalog can be problem.

The solution is simple - close session over some time or over some number

of operations. Then all memory in caches will be released.

Regards 

Pavel 

随时欢迎来杭交流PostgreSQL相关技术,记得来之前请与我联系哦。


阿里云服务器ECS共享型s6和n4性能对比CPU、内存、网络PPS和应用测试 阿里云服务器ECS共享型n4实例CPU采用2.5 GHz主频的Intel ® Xeon ® E5-2682 v4(Broadwell);ECS共享型s6实例2.5 GHz主频的Intel ® Xeon ® Platinum 8269CY(Cascade Lake),睿频3.2 GHz,计算性能稳定
【实用教程】在配备持久内存的实例上部署Redis应用 配备持久内存的实例(例如re7p、r7p、re6p)提供了超大CPU内存配比,Redis应用运行在这类实例上可以大幅度降低单GiB内存的成本。本文以部分操作系统为例,介绍如何在这类实例上快速部署Redis应用。
webshell内存木马的介绍与应用 内存马,也就是无文件格式webshell,检查难度系数很大,运用分布式数据库(apache、mvc这种)去运行恶意程序。适合情景为一些web应用为jar包运行,无网站文件目录、想写文件格式找不着网站文件目录相对路径等。内存马的注入一般搭配反序列化安全漏洞开展运用,如spring反序列化安全漏洞、fastjson反序列化、weblogic反序列化这种;也是有先提交着地文件格式,随后浏览注入内存马的,这类实际上 没有什么必要性,暂时性忽视。1个最实用的内存马方式便是https://ip:port/
内存表,通常被用于不需要持久化,变更频繁,访问RT低的场景。 目前社区版本PostgreSQL没有内存表的功能,postgrespro提供了两个插件可以实现类似内存表的功能。
PostgreSQL技术周刊第10期:PostgreSQL 调用 Rust 函数内存耗用研究 PostgreSQL(简称PG)的开发者们:云栖社区已有5000位PG开发者,发布了3000+PG文章(文章列表),沉淀了700+的PG精品问答(问答列表)。 PostgreSQL技术周刊会为大家介绍最新的PG技术与动态、预告活动、最热问答、直播教程等,欢迎大家订阅PostgreSQL技术周刊。