背景
阿里巴巴內部的某業務在使用阿里雲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存在的這個問題。