← Назад к вопросам

Как могут утечь все соединения из пула коннекшенов к базе данных и как это предотвратить?

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);

Чеклист предотвращения утечек

  1. Всегда использовать try/finally при getConnection()
  2. Предпочитать pool.query() вместо getConnection()
  3. Установить таймауты: idleTimeoutMillis, connectionTimeoutMillis
  4. Монитор в Production — логируй статус пула
  5. Graceful shutdown — закрыть все соединения при выключении
  6. Unit тесты — проверить, что соединения закрываются
  7. 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);
});
Как могут утечь все соединения из пула коннекшенов к базе данных и как это предотвратить? | PrepBro