我有一个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;
......每55ms重复一次。
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
的行的一小部分,希望这会比具有多个联接的单个查询更好地扩展和多个条件。 (我使用这个对SQL Server有很好的效果,我现在不相信任何查询优化器,但请告诉我这是否是Postgres的错误方法!)
查询在小表上运行8ms(从节点开始测量),在下一个作业开始之前有足够的时间完成一个作业“轮询”。
问题:以此速率轮询约3小时后,Postgres服务器内存不足并崩溃。
我已经尝试了......
如果我重新编写没有临时表的函数,Postgres不会耗尽内存,但我会大量使用临时表模式,所以这不是解决方案。
如果我停止节点程序(它杀死它用来运行查询的10个连接),内存就会释放。仅仅让节点在轮询会话之间等待一分钟不会产生相同的效果,因此显然存在与池化连接相关联的Postgres后端的资源。
如果我在进行轮询时运行VACUUM
,它对内存消耗没有影响,服务器继续死亡。
降低轮询频率只会改变服务器死亡前的时间。
在每个DISCARD ALL;
之后添加COMMIT;
无效。
在DROP TABLE jobs; DROP TABLE jobs_extra;
上RETURN query ()
而不是ON COMMIT DROP
之后明确调用CREATE TABLE
。服务器仍然崩溃。
根据CFrei的建议,在节点代码中添加pg.defaults.poolSize = 0
以尝试禁用池。服务器仍然崩溃,但需要更长时间,并且交换比之前的所有测试看起来像下面的第一个峰值高得多(第二次加标)。我后来发现pg.defaults.poolSize = 0
may not disable pooling as expected。
基于this:“autovacuum无法访问临时表。因此,应通过会话SQL命令执行适当的真空和分析操作。”,我试图运行{{1来自节点服务器(有些人尝试使VACUUM
成为“会话中”命令)。我实际上无法让这个测试工作。我的数据库中有很多对象,VACUUM
对所有对象进行操作,执行每个作业迭代的时间太长。仅限VACUUM
临时表是不可能的 - (a)您无法在事务中运行VACUUM
,(b)在事务外部不存在临时表。 :P编辑:后来在Postgres IRC论坛上,一位有用的小伙子解释说VACUUM与临时表本身无关,但对于清理TEMP TABLES导致的VACUUM
创建和删除的行非常有用。在任何情况下,VACUUMing“在会话中”都不是答案。
pg_attributes
之前 DROP TABLE ... IF EXISTS
,而不是CREATE TABLE
。服务器仍然死机。
ON COMMIT DROP
和CREATE TEMP TABLE (...)
代替insert into ... (select...)
,而不是CREATE TEMP TABLE ... AS
。服务器死了。
ON COMMIT DROP
不释放所有相关资源吗?还有什么可以记忆?我该如何发布?
答案 0 :(得分:0)
我使用它对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);
});
}
答案 1 :(得分:0)
使用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天后内存使用情况稳定,我的服务器没有崩溃。
我根本没有更改节点代码。