Ошибка PostgreSQL «Too Many Connections»: решение с помощью PgBouncer

Автор: Javohir Abdullayev · · Сервер и DevOps

Ошибка PostgreSQL «Too Many Connections»: решение с помощью PgBouncer

Введение: неожиданное появление проблемы

Каждый backend-разработчик или DevOps-инженер хотя бы раз в жизни сталкивается с этой пугающей ошибкой: FATAL: sorry, too many clients already. Недавно в одном из наших проектов среднего масштаба в сфере электронной коммерции произошла именно такая ситуация. После запуска маркетинговой акции посещаемость сайта резко возросла. В результате Django API перестал отвечать на запросы, а пользователи начали видеть ошибки 500 и 502.

Проверив мониторинг сервера, мы увидели, что ни процессор (CPU), ни оперативная память (RAM) не были перегружены. Проблема крылась в базе данных: PostgreSQL исчерпал лимит разрешенных соединений (connections) и перестал принимать новые запросы.

Почему соединения в PostgreSQL быстро заканчиваются?

Архитектура PostgreSQL устроена так, что под каждое новое клиентское подключение выделяется отдельный процесс. Каждый такой процесс потребляет ощутимый объем оперативной памяти сервера (обычно от 5–10 МБ и более). По умолчанию в PostgreSQL установлено значение max_connections = 100.

Наш Django-проект работал под управлением Gunicorn. 8 воркеров, каждый с несколькими потоками, плюс фоновые воркеры Celery одновременно устанавливали соединения с базой данных. С ростом числа пользователей каждый воркер Gunicorn удерживал свое соединение открытым. В итоге лимит в 100 подключений был исчерпан всего за пару минут.

Ошибочное решение: увеличение max_connections

Первой мыслью было самое простое решение: открыть postgresql.conf и увеличить max_connections до 500 или 1000. Однако это грубая ошибка. Управление сотнями активных процессов в PostgreSQL приводит к резкому замедлению из-за переключения контекста (context switching) в операционной системе, что в конечном счете может привести к падению (крэшу) всего сервера.

Настоящее решение: пул соединений PgBouncer

Чтобы решить эту проблему профессионально, мы решили установить PgBouncer. PgBouncer — это легковесный и чрезвычайно быстрый менеджер пула соединений (connection pooler) для PostgreSQL. Он работает как промежуточное звено между приложением и базой данных.

PgBouncer поддерживает 3 режима пулинга:

  • Session pooling: соединение выделяется клиенту на все время сессии и удерживается до ее завершения.
  • Transaction pooling: соединение возвращается в пул сразу после завершения транзакции. Это наиболее эффективный и популярный режим.
  • Statement pooling: соединение освобождается после выполнения каждого отдельного SQL-запроса (не поддерживает транзакции).

Для нашего Django-приложения мы выбрали режим Transaction pooling. Благодаря этому 500 веб-запросов могут последовательно и без сбоев обрабатываться всего через 20–30 активных соединений с PostgreSQL.

Установка и настройка PgBouncer

Установка PgBouncer на сервер Ubuntu выполняется очень просто:

sudo apt update
sudo apt install pgbouncer -y

После установки редактируем конфигурационный файл /etc/pgbouncer/pgbouncer.ini:

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

[pgbouncer]
logfile = /var/log/postgresql/pgbouncer.log
pidfile = /var/run/postgresql/pgbouncer.pid
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
reserve_pool_size = 5

Добавляем учетные данные пользователей в файл /etc/pgbouncer/userlist.txt:

"db_user" "md5_parol_hesh_qiymati"

Запускаем и проверяем службу PgBouncer:

sudo systemctl enable pgbouncer
sudo systemctl restart pgbouncer

Подключение Django-проекта к PgBouncer

В настройках Django-проекта (settings.py) меняем порт базы данных со стандартного 5432 на порт PgBouncer — 6432:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'my_database',
        'USER': 'db_user',
        'PASSWORD': 'secret_password',
        'HOST': '127.0.0.1',
        'PORT': '6432',
    }
}

Важное примечание: при использовании режима Transaction pooling параметр Django CONN_MAX_AGE необходимо установить в 0, иначе могут возникнуть непредвиденные ошибки сессий.

Результаты и выводы

После внедрения PgBouncer мы провели повторное нагрузочное тестирование (load testing). Результаты превзошли ожидания:

  • Даже при одновременных 800 параллельных запросах PostgreSQL стабильно работал всего с 25 соединениями.
  • Ошибка FATAL: sorry, too many clients already полностью исчезла.
  • Потребление оперативной памяти сервера снизилось на 40%.
  • Время ответа API (latency) стабильно удерживалось в диапазоне 45–60 мс.

Если ваш проект на Python или Django сталкивается с замедлением из-за нагрузки на базу данных, вместо слепого увеличения лимитов в настройках PostgreSQL рекомендуем настроить PgBouncer. Это надежное, экономичное и высокоэффективное решение.

Теги: #PostgreSQL #PgBouncer #Django #DevOps #Backend