← Назад к вопросам
Как могут утечь все соединения из пула коннекшенов к базе данных и как это предотвратить?
3.0 Senior🔥 301 комментариев
#Базы данных и SQL#Кэширование и производительность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Как могут утечь все соединения из пула коннекшенов к базе данных и как это предотвратить?
Утечка соединений — это критическая проблема в Production, которая приводит к падению приложения. Когда соединения заканчиваются, новые запросы не могут подключиться к БД.
Как утекают соединения?
Причина #1: Не закрыли соединение после использования
// ✗ ПЛОХО: Соединение никогда не вернулось в пул
import { pool } from './db';
async function getUser(id: string) {
const connection = await pool.getConnection();
const user = await connection.query('SELECT * FROM users WHERE id = $1', [id]);
// ✗ Забыли connection.release()!
return user;
}
// После 10 запросов (размер пула = 10): все соединения исчерпаны
// Следующий запрос: Error: connect ECONNREFUSED (не может получить соединение)
Причина #2: Ошибка в коде блокирует release()
// ✗ ПЛОХО: Если getUser выбросит исключение, release не выполнится
async function getUser(id: string) {
const connection = await pool.getConnection();
if (!id) {
throw new Error('ID required'); // release не выполнится!
}
const user = await connection.query(...);
connection.release();
return user;
}
Причина #3: Async операция повис (deadlock, timeout)
// ✗ ПЛОХО: Соединение ждёт ответ от БД, который никогда не придёт
async function slowQuery() {
const connection = await pool.getConnection();
// Запрос зависает (deadlock, network issue, etc)
await connection.query('SELECT * FROM huge_table');
// Соединение остаётся в пуле, но "занято"
connection.release();
}
Причина #4: Забыли await
// ✗ ПЛОХО: Promise не ждём, соединение утекает
async function processUsers() {
const users = await pool.query('SELECT * FROM users');
users.forEach(user => {
updateUser(user.id); // ✗ Забыли await!
// updateUser может использовать соединение, но мы не ждём
});
}
Как это предотвратить?
Метод 1: try/finally
// ✓ ХОРОШО: release() выполнится в любом случае
async function getUser(id: string) {
const connection = await pool.getConnection();
try {
if (!id) {
throw new Error('ID required');
}
const user = await connection.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return user;
} finally {
connection.release(); // Выполнится всегда!
}
}
Метод 2: pool.query() — использовать напрямую
// ✓ ЛУЧШЕ: pool управляет соединением автоматически
async function getUser(id: string) {
const { rows } = await pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return rows[0];
// pool автоматически вернёт соединение в пул
}
Метод 3: Обёртка с автоматическим release
// ✓ ЛУЧШЕЕ: Создать helper функцию
async function withConnection<T>(
callback: (connection: Connection) => Promise<T>
): Promise<T> {
const connection = await pool.getConnection();
try {
return await callback(connection);
} finally {
connection.release();
}
}
// Использование:
async function getUser(id: string) {
return withConnection(async (conn) => {
const { rows } = await conn.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return rows[0];
});
}
Метод 4: Таймауты на соединения
// ✓ ХОРОШО: Закрыть соединения, которые зависают
import { Pool } from 'pg';
const pool = new Pool({
user: 'user',
password: 'password',
host: 'localhost',
port: 5432,
database: 'mydb',
max: 10, // Max соединений в пуле
idleTimeoutMillis: 30000, // Закрыть если не используется 30 сек
connectionTimeoutMillis: 5000, // Макс время получить соединение
});
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
Диагностика утечек соединений
Метод 1: Логирование
const pool = new Pool(...);
// Монитор пула
setInterval(() => {
console.log(`
Total clients: ${pool.totalCount}
Idle clients: ${pool.idleCount}
Waiting queue: ${pool.waitingCount}
`);
}, 5000);
// Результат:
// Total clients: 10 Idle clients: 8 Waiting queue: 2
// Ждущих запросов = проблема с утечками!
Метод 2: Мониторинг в PostgreSQL
-- Проверить все соединения
SELECT
datname,
usename,
count(*) as connections
FROM pg_stat_activity
GROUP BY datname, usename;
-- Проверить idle соединения
SELECT
pid,
usename,
query_start,
state
FROM pg_stat_activity
WHERE state = 'idle';
Метод 3: Алерты APM
// NewRelic / DataDog интеграция
import * as newrelic from 'newrelic';
setInterval(() => {
const idleRatio = pool.idleCount / pool.totalCount;
newrelic.recordMetric('Custom/db/idle-ratio', idleRatio);
if (idleRatio < 0.2) {
newrelic.recordCustomEvent('PoolLeak', {
idle: pool.idleCount,
total: pool.totalCount,
});
}
}, 10000);
Real-World Пример: Express приложение
// ✓ ПРАВИЛЬНО: Все соединения управляются
import express from 'express';
import { pool } from './db';
const app = express();
app.get('/users/:id', async (req, res) => {
try {
// pool.query автоматически управляет соединением
const { rows } = await pool.query(
'SELECT * FROM users WHERE id = $1',
[req.params.id]
);
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: err.message });
}
});
app.get('/health', async (req, res) => {
const stats = {
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
};
if (stats.idle === 0 && stats.waiting > 0) {
return res.status(503).json({
status: 'unhealthy',
message: 'Database pool exhausted',
stats
});
}
res.json({
status: 'healthy',
stats
});
});
app.listen(3000);
Чеклист предотвращения утечек
- Всегда использовать try/finally при getConnection()
- Предпочитать pool.query() вместо getConnection()
- Установить таймауты: idleTimeoutMillis, connectionTimeoutMillis
- Монитор в Production — логируй статус пула
- Graceful shutdown — закрыть все соединения при выключении
- Unit тесты — проверить, что соединения закрываются
- Load тесты — найти утечки под нагрузкой
Graceful shutdown
const pool = new Pool(...);
process.on('SIGTERM', async () => {
console.log('SIGTERM received, closing connections...');
await pool.end();
console.log('Pool closed');
process.exit(0);
});