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

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 除了常见的执行计划缓存、数据缓存,PostgreSQL为了提高生成执行计划的效率,还提供了catalog, relation等缓存机制。PostgreSQL 9.5支持的缓存如下 ll src/backend/utils/cache/ attoptcache.c catcache.c

背景

阿里巴巴内部的某业务在使用阿里云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相关技术,记得来之前请与我联系哦。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
2月前
|
开发框架 .NET PHP
网站应用项目如何选择阿里云服务器实例规格+内存+CPU+带宽+操作系统等配置
对于使用阿里云服务器的搭建网站的用户来说,面对众多可选的实例规格和配置选项,我们应该如何做出最佳选择,以最大化业务效益并控制成本,成为大家比较关注的问题,如果实例、内存、CPU、带宽等配置选择不合适,可能会影响到自己业务在云服务器上的计算性能及后期运营状况,本文将详细解析企业在搭建网站应用项目时选购阿里云服务器应考虑的一些因素,以供参考。
|
3月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
99 1
|
3月前
|
存储 缓存 监控
如何使用内存监控工具来优化 Node.js 应用的性能
需要注意的是,不同的内存监控工具可能具有不同的功能和特点,在使用时需要根据具体工具的要求和操作指南进行正确使用和分析。
91 31
|
3月前
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
76 3
|
3月前
|
监控 JavaScript 算法
如何使用内存监控工具来定位和解决Node.js应用中的性能问题?
总之,利用内存监控工具结合代码分析和业务理解,能够逐步定位和解决 Node.js 应用中的性能问题,提高应用的运行效率和稳定性。需要耐心和细致地进行排查和优化,不断提升应用的性能表现。
224 77
|
3月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
3月前
|
存储 缓存 JavaScript
如何优化Node.js应用的内存使用以提高性能?
通过以上多种方法的综合运用,可以有效地优化 Node.js 应用的内存使用,提高性能,提升用户体验。同时,不断关注内存管理的最新技术和最佳实践,持续改进应用的性能表现。
171 62
|
3月前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
4月前
|
运维 JavaScript Linux
容器内的Nodejs应用如何获取宿主机的基础信息-系统、内存、cpu、启动时间,以及一个df -h的坑
本文介绍了如何在Docker容器内的Node.js应用中获取宿主机的基础信息,包括系统信息、内存使用情况、磁盘空间和启动时间等。核心思路是将宿主机的根目录挂载到容器,但需注意权限和安全问题。文章还提到了使用`df -P`替代`df -h`以获得一致性输出,避免解析错误。
125 0
|
4月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
102 1

相关产品

  • 云原生数据库 PolarDB