使用临时表调用 postgres 函数的节点导致“内存泄漏”
Posted
技术标签:
【中文标题】使用临时表调用 postgres 函数的节点导致“内存泄漏”【英文标题】:Node calling postgres function with temp tables causing "memory leak" 【发布时间】:2016-08-12 20:46:22 【问题描述】:我有一个 node.js 程序在事务中调用 Postgres(Amazon RDS 微实例)函数 get_jobs
,使用 brianc 的 node-postgres
包每秒 18 次。
节点代码只是brianc's basic client pooling example的增强版,大致像...
var pg = require('pg');
var conString = "postgres://username:password@server/database";
function getJobs(cb)
pg.connect(conString, function(err, client, done)
if (err) return console.error('error fetching client from pool', err);
client.query("BEGIN;");
client.query('select * from get_jobs()', [], function(err, result)
client.query("COMMIT;");
done(); //call `done()` to release the client back to the pool
if (err) console.error('error running query', err);
cb(err, result);
);
);
function poll()
getJobs(function(jobs)
// process the jobs
);
setTimeout(poll, 55);
poll(); // start polling
所以 Postgres 得到了:
2016-04-20 12:04:33 UTC:172.31.9.180(38446):XXX@XXX:[5778]:LOG: statement: BEGIN;
2016-04-20 12:04:33 UTC:172.31.9.180(38446):XXX@XXX:[5778]:LOG: execute <unnamed>: select * from get_jobs();
2016-04-20 12:04:33 UTC:172.31.9.180(38446):XXX@XXX:[5778]:LOG: statement: COMMIT;
...每 55 毫秒重复一次。
get_jobs
是用临时表写的,类似这样的
CREATE OR REPLACE FUNCTION get_jobs (
) RETURNS TABLE (
...
) AS
$BODY$
DECLARE
_nowstamp bigint;
BEGIN
-- take the current unix server time in ms
_nowstamp := (select extract(epoch from now()) * 1000)::bigint;
-- 1. get the jobs that are due
CREATE TEMP TABLE jobs ON COMMIT DROP AS
select ...
from really_big_table_1
where job_time < _nowstamp;
-- 2. get other stuff attached to those jobs
CREATE TEMP TABLE jobs_extra ON COMMIT DROP AS
select ...
from really_big_table_2 r
inner join jobs j on r.id = j.some_id
ALTER TABLE jobs_extra ADD PRIMARY KEY (id);
-- 3. return the final result with a join to a third big table
RETURN query (
select je.id, ...
from jobs_extra je
left join really_big_table_3 r on je.id = r.id
group by je.id
);
END
$BODY$ LANGUAGE plpgsql VOLATILE;
我使用了the temp table pattern,因为我知道jobs
将始终是从really_big_table_1
中提取的一小部分行,希望这将比具有多个连接和多个where 条件的单个查询更好地扩展。 (我在 SQL Server 上使用这个效果很好,现在我不信任任何查询优化器,但请告诉我这是否是 Postgres 的错误方法!)
查询在小表上运行时间为 8 毫秒(从节点开始测量),有足够的时间在下一项开始之前完成一项“轮询”作业。
问题:以这种速度轮询大约 3 小时后,Postgres 服务器内存不足并崩溃。
我已经尝试过...
如果我在没有临时表的情况下重新编写函数,Postgres 不会耗尽内存,但我经常使用临时表模式,所以这不是解决方案。
李>如果我停止节点程序(这会杀死它用于运行查询的 10 个连接),内存就会释放。仅仅让节点在轮询会话之间等待一分钟不会产生相同的效果,因此显然 Postgres 后端与池连接相关联正在保留资源。
如果我在轮询进行时运行VACUUM
,它不会影响内存消耗,并且服务器会继续死机。
降低轮询频率只会改变服务器死机前的时间量。
在每个COMMIT;
之后添加DISCARD ALL;
无效。
在RETURN query ()
之后显式调用DROP TABLE jobs; DROP TABLE jobs_extra;
,而不是CREATE TABLE
s 上的ON COMMIT DROP
s。服务器仍然崩溃。
根据 CFrei 的建议,将 pg.defaults.poolSize = 0
添加到节点代码以尝试禁用池。服务器仍然崩溃,但花费的时间要长得多,并且交换比之前的所有测试都高得多(第二个峰值),看起来像下面的第一个峰值。后来发现pg.defaults.poolSize = 0
may not disable pooling as expected。
基于this:“临时表不能被autovacuum访问。因此,应该通过会话SQL命令执行适当的vacuum和analyze操作。”,我尝试从VACUUM
运行节点服务器(作为一些尝试使 VACUUM
成为“会话中”命令)。我实际上无法让这个测试工作。我的数据库中有很多对象,VACUUM
对所有对象进行操作,执行每个作业迭代的时间太长。将VACUUM
限制在临时表是不可能的——(a)您不能在事务中运行VACUUM
,并且(b)在事务之外临时表不存在。 :P 编辑:稍后在 Postgres IRC 论坛上,一位有用的小伙子解释说 VACUUM 与临时表本身无关,但对于清理由 TEMP TABLES 导致的 pg_attributes
创建和删除的行很有用。在任何情况下,VACUUMing“在会话中”都不是答案。
DROP TABLE ... IF EXISTS
在 CREATE TABLE
之前,而不是 ON COMMIT DROP
。服务器仍然死机。
CREATE TEMP TABLE (...)
和 insert into ... (select...)
而不是 CREATE TEMP TABLE ... AS
,而不是 ON COMMIT DROP
。服务器死机了。
那么ON COMMIT DROP
是否没有释放所有相关资源?还有什么可以保存记忆?如何释放它?
【问题讨论】:
你能显示你用来执行查询的节点代码吗? 当您使用require('pg-native')
而不是pg
时,此错误是否仍然存在?如果每次请求池连接时都创建一个新连接怎么办? pg.defaults.poolSize = 0
.
@robertklep 我已经添加了代码。 @CFrei 将在我们测试后报告 - 每个周期都需要一段时间。
[我不明白这个 node-js 的东西] 这是否意味着你每 55ms 开始一个新的 DBMS 连接,并且没有正确关闭它?
@wildplasser 节点代码正在使用 node-postgres 模块来池连接。使用这种方式,node-postgres 最多创建 10 个连接(默认值,以及我们正在使用的连接)并重新使用它们来防止昂贵的数据库连接设置和破坏进程。在此处阅读更多信息github.com/brianc/node-postgres/wiki/pg
【参考方案1】:
我在 SQL Server 上使用这个效果很好,我现在不信任任何查询优化器
然后不要使用它们。您仍然可以直接执行查询,如下所示。
但请告诉我这是否是 Postgres 的错误方法!
这并不是一种完全错误的方法,只是一种非常尴尬的方法,因为您正在尝试创建其他人已经实现的东西以便更容易使用。结果,您犯了许多错误,可能导致许多问题,包括内存泄漏。
与使用pg-promise 的完全相同示例的简单性进行比较:
var pgp = require('pg-promise')();
var conString = "postgres://username:password@server/database";
var db = pgp(conString);
function getJobs()
return db.tx(function (t)
return t.func('get_jobs');
);
function poll()
getJobs()
.then(function (jobs)
// process the jobs
)
.catch(function (error)
// error
);
setTimeout(poll, 55);
poll(); // start polling
使用 ES6 语法时变得更加简单:
var pgp = require('pg-promise')();
var conString = "postgres://username:password@server/database";
var db = pgp(conString);
function poll()
db.tx(t=>t.func('get_jobs'))
.then(jobs=>
// process the jobs
)
.catch(error=>
// error
);
setTimeout(poll, 55);
poll(); // start polling
在您的示例中,我唯一不太了解的地方是使用事务来执行单个SELECT
。这不是事务通常的用途,因为您没有更改任何数据。我假设您正在尝试缩小您拥有的一段真实代码,该代码也会更改一些数据。
如果您不需要交易,您的代码可以进一步简化为:
var pgp = require('pg-promise')();
var conString = "postgres://username:password@server/database";
var db = pgp(conString);
function poll()
db.func('get_jobs')
.then(jobs=>
// process the jobs
)
.catch(error=>
// error
);
setTimeout(poll, 55);
poll(); // start polling
更新
但是,不控制前一个请求的结束是一种危险的方法,这也可能会产生内存/连接问题。
安全的方法应该是:
function poll()
db.tx(t=>t.func('get_jobs'))
.then(jobs=>
// process the jobs
setTimeout(poll, 55);
)
.catch(error=>
// error
setTimeout(poll, 55);
);
【讨论】:
我假设事务只用于触发临时表的ON COMMIT DROP
,作为一种垃圾回收。
@robertklep,好吧,可能是这样,我只是不清楚那部分。无论如何,如果需要,我为示例提供了交易;)
嘿,vitally,我很欣赏您的库具有许多出色的功能,但是如果您确定我的代码中的“可能导致许多问题的错误,包括内存泄漏”,您的回答会更有说服力.话虽如此,我会试试你的图书馆,如果它解决了我的问题,请告诉你。
@poshest 运气好吗? :)
嘿 Vitaly,我非常确信问题出在 Postgres 上,事实证明是这样(见我的回答)。所以我最终不需要更改节点代码。如果我们决定在应用程序范围内采用 Promise 模式,我肯定会使用你的库。【参考方案2】:
使用 CTE 创建部分结果集而不是临时表。
CREATE OR REPLACE FUNCTION get_jobs (
) RETURNS TABLE (
...
) AS
$BODY$
DECLARE
_nowstamp bigint;
BEGIN
-- take the current unix server time in ms
_nowstamp := (select extract(epoch from now()) * 1000)::bigint;
RETURN query (
-- 1. get the jobs that are due
WITH jobs AS (
select ...
from really_big_table_1
where job_time < _nowstamp;
-- 2. get other stuff attached to those jobs
), jobs_extra AS (
select ...
from really_big_table_2 r
inner join jobs j on r.id = j.some_id
)
-- 3. return the final result with a join to a third big table
select je.id, ...
from jobs_extra je
left join really_big_table_3 r on je.id = r.id
group by je.id
);
END
$BODY$ LANGUAGE plpgsql VOLATILE;
规划器将按照我希望使用临时表实现的方式按顺序评估每个块。
我知道这并不能直接解决内存泄漏问题(我很确定 Postgres 对它们的实现有问题,至少它们在 RDS 配置中的表现方式)。
但是,查询有效,它是按照我的预期方式计划的查询,并且在运行作业 3 天后内存使用情况现在很稳定,并且我的服务器没有崩溃。
我根本没有更改节点代码。
【讨论】:
以上是关于使用临时表调用 postgres 函数的节点导致“内存泄漏”的主要内容,如果未能解决你的问题,请参考以下文章