Узел, вызывающий функцию 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 TABLE
s. Сервер все еще падает.За предложение CFrei, добавил
pg.defaults.poolSize = 0
к коду узла в попытке отключить пул. Сервер по-прежнему зависал, но занял гораздо больше времени, и своп пошел намного выше (второй скачок), чем все предыдущие тесты, которые выглядели как первый скачок ниже. Позже я узнал, чтоpg.defaults.poolSize = 0
не может отключить пул, как ожидалось.
Исходя из этого: "Автозаполнение не позволяет получить доступ к временным таблицам. Поэтому соответствующие операции вакуума и анализа должны выполняться с помощью команд сеанса 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);
});
}