使用临时表调用 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 TABLEs 上的ON COMMIT DROPs。服务器仍然崩溃。

根据 CFrei 的建议,将 pg.defaults.poolSize = 0 添加到节点代码以尝试禁用池。服务器仍然崩溃,但花费的时间要长得多,并且交换比之前的所有测试都高得多(第二个峰值),看起来像下面的第一个峰值。后来发现pg.defaults.poolSize = 0may 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 EXISTSCREATE 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 函数的节点导致“内存泄漏”的主要内容,如果未能解决你的问题,请参考以下文章

Postgres 从动态 sql 字符串创建本地临时表(在提交删除时)

Postgres - 在函数之外声明变量[重复]

Spark:在查询中两次使用临时表?

退出函数时删除临时表

postgres函数之游标

在 Postgres 中,如何在更改表后重新验证(“类型检查”)函数和过程?