节点调用postgres函数与临时表导致“内存泄漏”

时间:2016-04-20 09:22:40

标签: node.js postgresql node-postgres

我有一个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

Swap memory usage on Postgres server

  • 基于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 DROPCREATE TEMP TABLE (...)代替insert into ... (select...),而不是CREATE TEMP TABLE ... AS。服务器死了。

ON COMMIT DROP不释放所有相关资源吗?还有什么可以记忆?我该如何发布?

2 个答案:

答案 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天后内存使用情况稳定,我的服务器没有崩溃。

我根本没有更改节点代码。