我们正在评估使用PostgreSQL来实现多租户数据库,目前我们正在对单个数据库多个模式模型进行一些测试(基本上,所有租户在同一个数据库下拥有自己的模式,这些模式下有相同的数据库对象集合)。应用程序将维护一个连接池,该池将被所有租户/模式共享。
例如,如果数据库有500个租户/模式,每个租户都有200个表/视图,则表/视图的总数将为500 * 200 = 100,000。
由于连接池将被所有租户使用,因此最终每个连接都会访问所有表/视图。
在我们的测试中,当连接访问更多的视图时,我们发现后端进程的内存使用量增长非常快,并且其中大部分是私有内存。这些内存将保持到连接关闭。
我们有一个测试用例,其中一个后端进程使用了30GB以上的内存,并最终出现了内存不足错误。
为了帮助理解问题,我编写了代码来创建简化的测试用例 - MTDB_destroy:用于清除租户模式 - MTDB_Initialize:用于创建多租户数据库 - MTDB_RunTests:简化的测试用例,基本上逐个从所有租户视图中选择。
我所做的测试是在CentOS 5.4上的PostgreSQL 9.0.3上进行的。
为了确保我有一个干净的环境,我重新创建了数据库群集,并将大多数配置留在默认状态下, (唯一必须更改的是增加“max_locks_per_transaction”,因为MTDB_destroy需要删除许多对象。)
这就是我重现问题所做的操作:
例如,如果数据库有500个租户/模式,每个租户都有200个表/视图,则表/视图的总数将为500 * 200 = 100,000。
由于连接池将被所有租户使用,因此最终每个连接都会访问所有表/视图。
在我们的测试中,当连接访问更多的视图时,我们发现后端进程的内存使用量增长非常快,并且其中大部分是私有内存。这些内存将保持到连接关闭。
我们有一个测试用例,其中一个后端进程使用了30GB以上的内存,并最终出现了内存不足错误。
为了帮助理解问题,我编写了代码来创建简化的测试用例 - MTDB_destroy:用于清除租户模式 - MTDB_Initialize:用于创建多租户数据库 - MTDB_RunTests:简化的测试用例,基本上逐个从所有租户视图中选择。
我所做的测试是在CentOS 5.4上的PostgreSQL 9.0.3上进行的。
为了确保我有一个干净的环境,我重新创建了数据库群集,并将大多数配置留在默认状态下, (唯一必须更改的是增加“max_locks_per_transaction”,因为MTDB_destroy需要删除许多对象。)
这就是我重现问题所做的操作:
- 创建一个新的数据库
- 使用附带的代码创建三个函数
连接到新创建的数据库并运行初始化脚本
-- 初始化
select MTDB_Initialize('tenant', 100, 100, true);
-- 不确定vacuum analyze在这里是否有用,我只是运行它
vacuum analyze;
-- 检查已创建的表/视图
select table_schema, table_type, count(*) from information_schema.tables where table_schema like 'tenant%' group by table_schema, table_type order by table_schema, table_type;
打开另一个连接到新创建的数据库并运行测试脚本
-- 获取当前连接的后端进程ID
SELECT pg_backend_pid();
-- 打开Linux控制台并运行ps -p并观察VIRT、RES和SHR
-- 运行测试
select MTDB_RunTests('tenant', 1);
在首次创建运行测试的连接时,
VIRT = 182MB,RES = 6240K,SHR = 4648K。
经过一次测试后(共175秒),
VIRT = 1661MB,RES = 1.5GB,SHR = 55MB。
再次运行测试(共167秒),
VIRT = 1661MB,RES = 1.5GB,SHR = 55MB。
重复运行测试(共165秒),
VIRT = 1661MB,RES = 1.5GB,SHR = 55MB。
随着表数量的增加,测试中的内存使用量也会增加。
有人能帮忙解释一下这里到底发生了什么吗? 是否有办法控制PostgreSQL后端进程的内存使用情况?
谢谢。
Samuel
-- MTDB_destroy
create or replace function MTDB_destroy (schemaNamePrefix varchar(100))
returns int as $$
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 $$ language plpgsql;
-- MTDB_Initialize
create or replace function MTDB_Initialize (schemaNamePrefix varchar(100), numberOfSchemas integer, numberOfTablesPerSchema integer, createViewForEachTable boolean)
returns integer as $$
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 $$ language plpgsql;
-- MTDB_RunTests
create or replace function MTDB_RunTests(schemaNamePrefix varchar(100), rounds integer)
returns integer as $$
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 $$ language plpgsql;