PostgreSQL умеет работать с прямыми подключениями, но цена каждого — от 2 до 10 МБ оперативной памяти и заметные накладные расходы на переключение контекста. Когда клиентов становится 10 000, сервер просто упирается в max_connections или начинает деградировать ещё до того, как ты упрёшься в память. PgBouncer решает эту задачу: он забирает на себя тысячи клиентских подключений и мультиплексирует их в жёстко контролируемое число соединений к PostgreSQL. Дальше разберём, как именно он это делает и что настраивать, чтобы схема работала без сюрпризов.
Почему 10K прямых соединений к PostgreSQL — это проблема
Каждое клиентское подключение форкает отдельный процесс бэкенда. Даже в idle-состоянии такой процесс потребляет память, а при активной работе добавляет расходы на синхронизацию и планировщик ОС. PostgreSQL не рассчитан на десятки тысяч конкурентных соединений — стандартные рекомендации упираются в сотни, в лучшем случае в несколько тысяч, и то с серьёзными оговорками по железу.
Параметр max_connections ограничивает общее число подключений, но попытка выставить его в 10 000 убьёт производительность раньше, чем ты примешь трафик. Даже если памяти хватит, внутренние блокировки и лавина контекстных переключений загрузят CPU так, что полезная работа почти остановится. Именно поэтому перед базой ставят пулер, который «схлопывает» клиентскую толпу до приемлемого числа серверных коннектов.
Как PgBouncer переваривает толпу клиентов
PgBouncer работает как reverse proxy для протокола PostgreSQL. Он принимает тысячи входящих подключений и переиспользует ограниченный пул соединений к бэкенду, выполняя мультиплексирование. Клиент открывает соединение к пгбаунсеру, отправляет запрос, получает ответ, и это же соединение тут же готово обслужить другого клиента — но уже через другой серверный коннект, который освободился.
Для highload-сценариев критически важен режим работы пула:
- session pooling — клиент закрепляется за одним серверным коннектом на всё время своей сессии. Масштабируется плохо: при большом числе клиентов быстро кончаются серверные коннекты, и PgBouncer вынужден либо ставить клиентов в очередь, либо ронять.
- transaction pooling — соединение к базе занято только на время выполнения транзакции. Как только транзакция завершилась, коннект возвращается в пул и тут же может обслужить другого клиента. Именно этот режим позволяет обслуживать 10 000+ клиентов при жалких сотнях соединений к PostgreSQL.
- statement pooling — ещё агрессивнее, коннект освобождается после каждого оператора, но ломает транзакционную логику большинства приложений. Для обычных веб-приложений его практически не используют без тотальной переработки кода.
Итог: чтобы выдержать 10K клиентов, переходишь на pool_mode = transaction и вычищаешь из кода всё, что завязано на состояние сессии.
Главный параметр, который путают: max_client_conn
Многие читают «max_client_conn» как ограничение на число коннектов к базе данных. На самом деле это максимальное количество клиентских подключений, которое PgBouncer примет одновременно. Количество же коннектов к PostgreSQL управляется другими параметрами — default_pool_size, max_db_connections, max_user_connections.
Почему это важно: если ты выставляешь max_client_conn = 10000, пгбаунсер обязан держать до 10 000 открытых сокетов для клиентов плюс собственные соединения к базе. Каждый сокет — это файловый дескриптор. ОС по умолчанию редко разрешает столько, поэтому рост max_client_conn всегда тянет за собой поднятие лимита файловых дескрипторов.
Оценка необходимого числа дескрипторов, базирующаяся на документации PgBouncer:
- Если используется одна учётная запись для доступа к нескольким базам:
FD ≤ max_client_conn + (max_pool_size × total_databases) - Если у каждой базы дополнительно свой пул пользователей:
FD ≤ max_client_conn + (max_pool_size × total_databases × total_users)
Для 10 000 клиентов при пуле в 25 соединений на базу и, скажем, 5 базах получаем минимум 10000 + (25 × 5) = 10125 дескрипторов только под пгбаунсер. К этому стоит добавить запас на служебные файловые дескрипторы, логи и прочее — 12 000–15 000 будет надёжным ориентиром.
Практический конфиг для 10K+ клиентов
Один из боевых конфигов для highload и мультитенантных систем выглядит так (синтаксис pgbouncer.ini):
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 25
min_pool_size = 10
reserve_pool_size = 10
reserve_pool_timeout = 5
max_client_conn = 10000
max_db_connections = 100
max_user_connections = 100
server_reset_query = DISCARD ALL
server_idle_timeout = 600
client_idle_timeout = 600
query_timeout = 30
Что здесь важно:
pool_mode = transaction— основа для обслуживания тысяч клиентов.max_client_conn = 10000— сколько клиентов мы готовы принять сразу. Оставшиеся встанут в очередь на уровне TCP-стека операционной системы, но тут уже упираемся вsomaxconn.max_db_connections = 100— глобальный потолок всех серверных соединений к PostgreSQL. Достигается кумулятивно через сумму всех пулов.default_pool_size = 25— столько коннектов к базе PgBouncer откроет для типичной нагрузки и будет переиспользовать.reserve_pool_size = 10иreserve_pool_timeout = 5— страховка на случай пика: если все основные соединения заняты дольше 5 секунд, активируется резерв, но не больше 10 дополнительных коннектов.server_reset_query = DISCARD ALL— перед тем как передать коннект следующему клиенту, PgBouncer выполнитDISCARD ALL, сбрасывая состояние сессии. Обязательная вещь для transaction pooling, иначе состояние от предыдущего клиента «утечёт» к следующему.
Логика адаптации под свою базу: смотри на max_connections PostgreSQL (например, 200). Оставь запас под другие процессы и административные подключения, а остальное распредели между пулами так, чтобы max_db_connections не превышал оставшееся число. Затем оцени реальное количество конкурентных запросов, а не клиентов, и подстрой default_pool_size. Пиковые значения компенсируй резервным пулом.
Считаем пул без ошибок: default_pool_size и reserve
Ошибка новичков — ставить default_pool_size равным числу клиентов или потоков приложения. Пул нужно считать от количества действительно параллельных запросов к базе в единицу времени. В транзакционном режиме один серверный коннект может обслужить сотни клиентов за секунду, если транзакции короткие.
Практический ориентир: веб-приложение с 10 000 одновременных клиентов может генерировать порядка 10 000 RPS, но в каждый момент времени лишь малая часть запросов реально исполняется на базе. Нагрузочные тесты показывают, что удерживая около 50 серверных соединений, можно стабильно обрабатывать ~10 000 RPS при быстрых запросах. Поэтому default_pool_size часто лежит в диапазоне 20–50.
Резервный пул (reserve_pool_size) — это дополнительный запас на кратковременные всплески. Если все основные коннекты заняты дольше reserve_pool_timeout, PgBouncer открывает до reserve_pool_size новых соединений. Это позволяет не ронять клиентов при резком пике, но и не держать лишние соединения постоянно. В высоконагруженных конфигах часто встречается reserve_pool_size = 10 с таймаутом 3–5 секунд.
Считая общее число соединений к базе, помни, что default_pool_size умножается на количество баз и пользователей. Если у тебя одна база и один юзер — всё просто. Если мультитенант с десятком баз — формула max_db_connections ≈ общее число пулов + reserve даст тебе верхнюю планку, которую не стоит превышать без крайней необходимости.
Готовим систему: лимиты ОС, дескрипторы, сеть
10 000 клиентских подключений создают серьёзную нагрузку на операционную систему. Первое, во что упирается PgBouncer — лимит открытых файлов. По умолчанию в большинстве дистрибутивов он составляет 1024 или 4096, чего при 10K клиентов категорически не хватает.
Что нужно сделать (общий подход):
- Увеличить
nofile(количество открытых файлов) для пользователя, под которым работает PgBouncer. Значение берётся из расчёта по формуле:FD ≈ max_client_conn + (max_pool_size × total_databases)плюс запас. Для 10 000 клиентов ориентируйся на 12 000–15 000 и задай с небольшим превышением. - Настроить параметры ядра (sysctl) для работы с большим числом сокетов:
net.core.somaxconn,net.ipv4.tcp_max_syn_backlogи, при большом количестве TIME_WAIT, тюнингtcp_tw_reuseи диапазон локальных портов. Конкретные значения зависят от дистрибутива и паттерна трафика, поэтому точные цифры не приводим — смотри документацию своего ядра и делай запас. - Проверить, что у базы данных на
max_connectionsхватает места для всех коннектов, которые PgBouncer в теории может открыть (max_db_connections). PostgreSQL тоже потребляет файловые дескрипторы — убедись, что лимиты подняты и для него.
Если после всех настроек PgBouncer при большом числе клиентов начинает тормозить, смотри в первую очередь на утилизацию CPU ядром сети и на насыщение очереди слушающего сокета (Recv-Q / ListenDrops).
Миграция с session-режима: на что нарваться и как обойти
Переход с session pooling на transaction pooling даёт взрывной рост масштабируемости, но ломает всё, что держалось на постоянном соединении. Самые частые жертвы:
- Временные таблицы. В транзакционном режиме после завершения транзакции коннект уходит другому клиенту, и временная таблица либо пропадает, либо «достаётся» чужой сессии. Решение: отказаться от временных таблиц или эмулировать их через CTE/подзапросы.
- Сессионные переменные и
SET-команды. Если приложение выставляетstatement_timeout,search_pathили любые другие параметры черезSETв начале сессии и рассчитывает, что они сохранятся между транзакциями, — в transaction pooling это перестанет работать. Каждая транзакция может выполняться на новом коннекте. Выход: применятьSET LOCALвнутри транзакции или передавать параметры через строку подключения/options вpgbouncer.ini(например,server_reset_query = DISCARD ALLсбрасывает всё, что могло висеть). - Курсоры, удерживающие состояние.
DECLARE CURSORживёт только в пределах транзакции (если она не содержит hold). При следующей транзакции коннект может быть другим, и курсор потерян. Перепиши логику на клиентскую итерацию по result set или используй пагинацию через LIMIT/OFFSET. LISTEN/NOTIFYи асинхронные уведомления. Если приложение слушает канал, в транзакционном режиме оно не может гарантировать, что останется на том же коннекте между получением уведомления и обработкой. Сценарии на основе LISTEN лучше оставить в session-пуле для конкретного клиента или перевести на внешнюю очередь.
Чеклист перед переходом:
- Найти в коде все
CREATE TEMP TABLEи заменить на обходные конструкции. - Убрать
SET-команды, которые выполняются вне транзакций и на которые полагается бизнес-логика. - Проверить, что транзакции атомарны и не требуют сохранения контекста между запросами.
- Выделить служебные подключения (административные, ETL), для которых можно оставить отдельный session-пул или прямое соединение.
FAQ
Чем transaction pooling отличается от session pooling для 10K клиентов?
При session pooling каждому клиенту выдаётся выделенный серверный коннект на всё время жизни сессии. 10 000 клиентов = 10 000 коннектов к PostgreSQL, что практически всегда невозможно. Transaction pooling выдаёт коннект только на время транзакции и тут же возвращает его в пул. 10 000 клиентов могут обслуживаться буквально 50–100 серверными соединениями.
Как рассчитать default_pool_size?
Отталкивайся не от числа клиентов, а от реального числа одновременно исполняемых запросов. Посмотри на метрики: с какой скоростью выполняются транзакции и сколько коннектов к базе реально заняты в пике. Для типичного веб-приложения с быстрыми запросами порядок 20–50 соединений часто перекрывает 10 000 RPS. Уточни на синтетике и добавь резервный пул.
Как не убить PostgreSQL сотнями серверных коннектов?
Жёстко ограничь max_db_connections и суммарный default_pool_size так, чтобы сумма всех возможных серверных соединений не превышала комфортного для PostgreSQL лимита (обычно сотни, а не тысячи). Следи, чтобы max_connections на базе всегда был с запасом для административных подключений.
Можно ли использовать PgBouncer с репликацией и балансировкой?
Да. PgBouncer можно настроить так, чтобы один виртуальный host в секции [databases] указывал на несколько физических серверов (primary + replicas). При транзакционном пулинге это работает надёжно, но учитывай, что чтение с реплики должно быть осознанным — транзакции только для чтения должны явно направляться на реплику. Для полноценной балансировки многие используют связку PgBouncer + haproxy или специализированные решения.
Что делать, если при 10K клиентов pgbouncer сам начинает тормозить?
Проверь лимиты файловых дескрипторов — это самая частая причина. Дальше смотри загрузку CPU на сетевом стеке и очередь слушающего сокета. Часто помогает поднять net.core.somaxconn и увеличить размер буферов приёма. Если тормоза идут от дискового ввода-вывода (логгирование), переведи логирование на асинхронный syslog или вообще отключи дебаг-режим.
Вывод
10 000+ клиентских подключений к PostgreSQL — задача не про прямое соединение, а про грамотное мультиплексирование. PgBouncer в режиме transaction pooling способен переработать эту толпу в управляемые десятки-сотни серверных коннектов, если правильно рассчитаны default_pool_size, резервный пул и подняты лимиты ОС. Ключ к успеху: подготовить приложение к «обезличенному» коннекту, убрать сессионное состояние и заложить запас по дескрипторам. Протестируй конфиг на стенде с нагрузкой, близкой к боевой, и только потом выкатывай в продакшн — иначе даже лучший пулер может стать точкой отказа.
Источники
- How to Handle 10K Connections with PgBouncer
- Postgres Pro Standard : Documentation: 10: pgbouncer
- How to Configure Connection Pooling with PgBouncer
- Boosting PostgreSQL Performance with PgBouncer
- PgBouncer at Scale: 10K+ Connections Multi-Tenant Postgres
- dinesh-k-elumalai/pgbouncer-multitenant-scale ...




.svg.webp)

