Узел, вызывающий функцию postgres с временными таблицами, вызывающими "утечку памяти"

У меня есть программа node.js, вызывающая функцию Postgres (микроэкземпляр Amazon RDS), get_jobs в транзакции, 18 раз в секунду, используя node-postgres пакет от brianc.

Код узла - это просто расширенная версия базового примера клиентского пула brianc, примерно как...

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;

Я использовал шаблон временной таблицы, потому что я знаю, что jobs всегда будет небольшой фрагмент строк из really_big_table_1, в надежде, что это будет масштабироваться лучше, чем один запрос с несколькими объединениями и несколькими условиями условия. (Я использовал это с большим эффектом с SQL Server, и сейчас я не доверяю оптимизаторам запросов, но, пожалуйста, скажите мне, если это неправильный подход для Postgres!)

Запрос выполняется в 8 мс для небольших таблиц (по данным узла), достаточно времени для завершения одного "опроса" заданий перед началом следующего.

Проблема: Примерно после 3 часов опроса с такой скоростью серверу Postgres не хватает памяти и происходит сбой.

Что я уже пробовал...

  • Если я переписываю функцию без временных таблиц, Postgres не хватает памяти, но я часто использую шаблон временных таблиц, так что это не решение.

  • Если я остановлю программу узла (которая убивает 10 соединений, которые она использует для выполнения запросов), память освобождается. Простое заставление узла ждать минуту между сеансами опроса не дает того же эффекта, поэтому, очевидно, есть ресурсы, которые хранит серверная часть Postgres, связанная с пулевым соединением.

  • Если я бегу VACUUM во время опроса он не влияет на потребление памяти, и сервер продолжает свой путь к смерти.

  • Снижение частоты опроса только изменяет время, за которое сервер умирает.

  • Добавление DISCARD ALL; после каждого COMMIT; не имеет никакого эффекта

  • Явный вызов DROP TABLE jobs; DROP TABLE jobs_extra; после RETURN query () вместо ON COMMIT DROPна CREATE TABLEs. Сервер все еще падает.

  • За предложение CFrei, добавил pg.defaults.poolSize = 0 к коду узла в попытке отключить пул. Сервер по-прежнему зависал, но занял гораздо больше времени, и своп пошел намного выше (второй скачок), чем все предыдущие тесты, которые выглядели как первый скачок ниже. Позже я узнал, что pg.defaults.poolSize = 0 не может отключить пул, как ожидалось.

Использование памяти подкачки на сервере Postgres

  • Исходя из этого: "Автозаполнение не позволяет получить доступ к временным таблицам. Поэтому соответствующие операции вакуума и анализа должны выполняться с помощью команд сеанса SQL". Я попытался запустить VACUUM с узла сервера (как некоторые попытки сделать VACUUM команда "в сеансе"). Я не мог заставить этот тест работать. У меня есть много объектов в моей базе данных и VACUUM, работая на всех объектах, занимал слишком много времени для выполнения каждой итерации задания. Ограничивая VACUUM просто для временных таблиц было невозможно - (а) вы не можете запустить VACUUM в транзакции и (b) вне транзакции временные таблицы не существуют.:P РЕДАКТИРОВАТЬ: Позже на форуме IRC Postgres один полезный глава объяснил, что VACUUM не имеет отношения к самим временным таблицам, но может быть полезен для очистки строк, созданных и удаленных из pg_attributes что вызывает TEMP TABLES. В любом случае, 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 не освобождая все связанные ресурсы? Что еще могло удерживать память? Как мне выпустить это?

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 дней выполнения задания, и мой сервер не падает.

Я не изменил код узла вообще.

Я использовал это с большим эффектом с SQL Server, и я не доверяю ни одному оптимизатору запросов сейчас

Тогда не используйте их. Вы по-прежнему можете выполнять запросы напрямую, как показано ниже.

но, пожалуйста, скажите мне, если это неправильный подход для Postgres!

Это не совсем неправильный подход, это очень неловкий подход, поскольку вы пытаетесь создать что-то, что было реализовано другими для гораздо более легкого использования. В результате вы делаете много ошибок, которые могут привести ко многим проблемам, включая утечки памяти.

Сравните с простотой точно такого же примера, который использует pg-обещание:

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);
        });
}
Другие вопросы по тегам