lenec ru

← все посты

Postgres FATAL: too many connections — как лечить

11K

Postgres падает не от количества SQL-запросов и не от размера базы. Чаще всего у людей в проде он начинает выдавать вот это:

FATAL: sorry, too many clients already

Сообщение прямое: достигнут лимит max_connections. Сервис, который попытался открыть соединение, не получает его и крашится. На пять минут позже уже половина бэкенда не работает, потому что новые поды/процессы тоже не могут подключиться.

В моём опыте корень проблемы почти никогда не в самом Postgres. Он в том, что приложение открывает много коннектов, не закрывает их, или пытается обращаться к БД из десятков параллельных воркеров без пула.

Что выставлено по умолчанию

Дефолт Postgres — max_connections = 100. На managed-хостингах часто чуть выше, но в любом случае это не «бесконечно». Проверить:

SHOW max_connections;

Сколько сейчас открыто и кто открыл — главный запрос для диагностики:

SELECT datname, usename, application_name, state, count(*)
FROM pg_stat_activity
GROUP BY datname, usename, application_name, state
ORDER BY count DESC;

В реальной жизни этот вывод чаще всего показывает картину типа: 80 idle-коннектов от одного приложения и 5 активных. Сразу видно: connection pool настроен криво или его нет вовсе.

Не поднимай max_connections наугад

Соблазн — увеличить лимит. Сделать max_connections = 500 и забыть. Так нельзя.

Каждое соединение в Postgres — это отдельный процесс ОС с собственным work_mem и стеком. На каждое уходит несколько мегабайт ОЗУ ещё до того, как пошли запросы. На typical-конфиге 4 ГБ ОЗУ удерживать 500 коннектов — это просто не работает: Postgres начнёт OOM-иться.

Ориентир: max_connections * (work_mem * сложные_запросы) + shared_buffers + накладные — должно влезать в ОЗУ с запасом. Для большинства небольших проектов 100–200 — потолок.

Правильное лечение: пул на стороне клиента

node-postgres / pg

Если ты создаёшь новое new Client() на каждый запрос и забываешь закрыть — соединения утекают. Используй Pool:

import { Pool } from 'pg';

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 10_000,
  connectionTimeoutMillis: 5_000,
});

Pool сам открывает до max коннектов, переиспользует их, закрывает простаивающие через idleTimeoutMillis. На запрос делаешь pool.query(...) — он берёт коннект из пула и возвращает его обратно автоматически.

Drizzle на основе pg

То же самое — Drizzle принимает pool снаружи:

import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
});

export const db = drizzle(pool);

Несколько процессов

Главная ловушка — у тебя 10 worker-процессов, и каждый держит пул на 10 коннектов. Итого 100 коннектов, и это в норме, на пиках — больше. Если на сервере несколько таких приложений, на пиках упёрся в max_connections.

Считай по формуле workers * pool.max ≤ max_connections - резерв. Резерв — на репликацию, бэкапы, миграции, мониторинг.

Когда мало клиентских пулов — ставь PgBouncer

Если приложений много (микросервисы, serverless-функции, несколько серверов), каждое со своим пулом — лимит съедается мгновенно. Решение — PgBouncer между приложениями и Postgres. Он сам держит подключения к Postgres и раздаёт их сотням клиентов.

Простой конфиг pgbouncer.ini для transaction-mode:

[databases]
app = host=127.0.0.1 port=5432 dbname=app

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25

В этом сетапе приложения подключаются к 6432 (PgBouncer), а тот держит максимум 25 коннектов к самому Postgres. Тысяча клиентов — без проблем.

Подвох: в transaction-режиме нельзя использовать prepared statements между транзакциями и сессионные настройки. На большинстве веб-приложений это не проблема, но если у тебя SET LOCAL или подготовленные запросы — выбирай session-mode и считай лимиты с учётом этого.

Что делать, если уже горит

Сервис лёг, в логах too many clients. Быстро — найти и убить idle-коннекты:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle'
AND state_change < now() - interval '5 minutes'
AND pid <> pg_backend_pid();

Это освободит места в лимите. Дальше уже разбираешься, кто их открыл.

В порядке работы добавь в Postgres idle_in_transaction_session_timeout, чтобы зависшие транзакции не удерживали коннекты бесконечно:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '5min';
SELECT pg_reload_conf();

Это разорвёт коннекты, которые открыли транзакцию и забыли закоммитить (например, из-за бага в приложении).

Особенно опасный сценарий: serverless

Каждый холодный старт serverless-функции открывает новый коннект. На пиках их сотни. Без пула где-то посередине Postgres ляжет.

Тут есть несколько подходов:

  • PgBouncer перед БД (классика);
  • специальные коннекторы — RDS Proxy, Neon serverless driver, Supabase pooler;
  • если БД совсем рядом и трафик невелик — переиспользовать коннект между инвокациями (хранить pool в module scope, контейнер запоминает).

Серверлесс с обычным new Client() в каждой функции — путь к too many connections за две недели роста.

Чек-лист, по которому я разбираю инцидент

  • Сколько коннектов сейчас? SELECT count(*) FROM pg_stat_activity;
  • Сколько из них idle, сколько активных, кто application_name?
  • Соответствует ли workers * pool.max лимиту?
  • Стоит ли где-то PgBouncer? Если нет — пора.
  • Не висят ли idle-in-transaction коннекты? Включить timeout.

Поднимать max_connections можно, но только осознанно: считать память, мерить, не делать с потолка. Реальное лечение — пул и контроль за тем, кто и когда открывает коннект к БД.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.