PostgreSQL內存占用


PostgreSQL relcache在長連接應用中的內存霸占"坑"

背景
阿里巴巴內部的某業務在使用阿里雲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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM