Настройка пула соединений PostgreSQL на VPS
Каждое соединение PostgreSQL потребляет RAM как отдельный процесс. Узнайте, почему 100 подключений приводят к ошибке FATAL: terminating connection due to OOM killer.
Почему на небольшом VPS память заканчивается раньше, чем достигается лимит max_connections
Пул соединений PostgreSQL на VPS — это не способ ускорения. Это средство поддержания работоспособности сервера с 4 GB оперативной памяти, так как каждое соединение с PostgreSQL представляет собой отдельный процесс операционной системы, занимающий собственную область памяти. Пул соединений позволяет использовать небольшое фиксированное количество реальных фоновых процессов для обслуживания большого количества дешевых клиентских подключений.
Значение по умолчанию для max_connections составляет 100. Это верхний предел, а не бюджет ресурсов. PostgreSQL не проверяет, способна ли ваша система фактически выдержать 100 фоновых процессов, выполняющих реальные запросы, поэтому сервер выходит из строя первым. Механизм OOM (Out of Memory) killer ядра выбирает процесс для завершения, и если он выбирает фоновый процесс PostgreSQL, база данных перезапускает весь кластер для обеспечения целостности разделяемой памяти. В логах отображается server process (PID 1234) was terminated by signal 9: Killed, а затем terminating any other active server processes. Все открытые соединения при этом разрываются, включая корректно работающие.
Память на сервере заканчивается, потому что каждое соединение является отдельным процессом, а параметр work_mem выделяется для каждой операции сортировки или хеширования, а не на каждое соединение. Оба этих фактора действуют мультипликативно.
Каждое соединение — это процесс, и каждый процесс потребляет память
PostgreSQL использует по одному процессу на каждое соединение. При подключении клиента postmaster создает (fork) отдельный бэкенд-процесс, который существует до тех пор, пока клиент не отключится. Это не поток (thread). У него есть собственные таблицы страниц, собственные кэши каталогов и собственные кэшированные планы запросов. Эти кэши растут по мере того, как соединение обращается к новым таблицам и выполняет различные запросы, поэтому долгоживущее соединение в приложении с активным ORM потребляет больше ресурсов, чем новое.
Разделяемая память (shared memory) действительно является общей. shared_buffers — это единая область выделения для всего кластера, отображаемая в адресное пространство каждого бэкенда. Частная память (private memory) не является общей, поэтому top вводит в заблуждение: размер резидентного набора (RSS) бэкенда включает в себя общие страницы, к которым обращался этот бэкенд. В результате суммирование RSS по 50 бэкендам приводит к тому, что shared_buffers учитывается 50 раз.
Измеряйте именно частную часть. PSS (proportional set size) делит размер каждой общей страницы на количество процессов, которые её используют, а USS (unique set size) учитывает только те страницы, которые принадлежат исключительно данному процессу.
sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'Столбец USS показывает объем памяти, который будет освобожден при завершении работы этого бэкенда. Это и есть ваша реальная стоимость одного соединения. В опубликованных отчетах обычно указывается, что простаивающий бэкенд потребляет несколько мегабайт, а бэкенд, выполнивший сложные ORM-запросы, — в несколько раз больше. Воспринимайте эти данные как типичные показатели, а не как ваши собственные. Единственная цифра, на которую стоит ориентироваться при планировании, — это показатель с вашего сервера при вашей нагрузке.
Выделение памяти на сессию легко упустить из виду. Параметр temp_buffers по умолчанию равен 8MB и выделяется для сессии при первом обращении к временной таблице. Эта память не возвращается системе до завершения сессии.
Параметр work_mem выделяется на каждую операцию, а не на соединение
Здесь многие ошибаются в расчетах. Значение work_mem по умолчанию составляет 4MB, и документация PostgreSQL прямо указывает, что это значит: «сложный запрос может выполнять несколько операций сортировки и хеширования одновременно, при этом каждой операции обычно разрешается использовать столько памяти, сколько указано в этом значении, прежде чем она начнет записывать данные во временные файлы». План запроса с тремя узлами сортировки может использовать объем памяти, равный трем work_mem, внутри одного бэкенда в один и тот же момент времени.
Операции хеширования потребляют больше. Значение hash_mem_multiplier по умолчанию равно 2.0, поэтому хеш-соединение или хеш-агрегация могут использовать work_mem, умноженное на два, что составляет 8MB при стандартных настройках. Параллельные запросы увеличивают потребление еще сильнее, так как каждый параллельный рабочий процесс является отдельным процессом со своим лимитом памяти.
Рассчитаем показатели для VPS с 4 GB оперативной памяти. Установим shared_buffers на 1 GB, оставим work_mem на уровне 4MB и допустим, что 100 соединений одновременно выполняют по одному запросу с двумя узлами хеширования. Это 100 умножить на 16MB, что дает 1.6 GB выделенной памяти сверх 1 GB общих буферов (shared buffers), еще до учета page cache и других процессов в системе. Теперь увеличим work_mem до 64MB, так как на сервере есть свободная память, и те же 100 соединений потребуют уже 100 умножить на 256MB. Никаких предупреждений не будет. Вы узнаете о проблеме только тогда, когда сработает OOM killer.
Вместо догадок можно проверить, не слишком ли мало значение work_mem. Установите log_temp_files = 0 в файле postgresql.conf и выполните перезагрузку конфигурации. Каждое сбрасывание данных на диск будет записывать строку с указанием имени файла и его размера, например temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520. Частые сбросы означают, что увеличение work_mem принесет пользу. Отсутствие сбросов означает, что увеличение параметра ничего не даст, но потребует память, которой у вас нет.
Арифметика пулов, которая приводит к проблемам
Никто не планирует открывать 240 соединений. Администраторы настраивают пул из 20 соединений, а затем запускают приложение на нескольких узлах.
The data behind this chart
[
{
"config": "1 worker",
"backends": 20
},
{
"config": "4 web workers",
"backends": 80
},
{
"config": "4 web + 2 background",
"backends": 120
},
{
"config": "2 hosts x 4 workers",
"backends": 160
},
{
"config": "3 hosts x 4 workers",
"backends": 240
}
]Четыре воркера Gunicorn, каждый из которых удерживает пул из 20 соединений, запрашивают 80 бэкендов. Добавьте два воркера для фоновых задач, и получится 120. Увеличьте количество до 3 hosts x 4 workers, и приложение будет запрашивать 240 бэкендов при max_connections, равном 100. Ни одна из этих конфигураций 5 не является ошибочной по отдельности. Пул создается для каждого процесса, и ни одна часть приложения не видит общего количества соединений.
Настройки библиотек по умолчанию только усугубляют ситуацию. В SQLAlchemy QueuePool по умолчанию использует pool_size=5 с max_overflow=10, что дает 15 соединений на процесс. HikariCP по умолчанию использует 10. В Django до версии 5.1 не было встроенного пула, использовалось одно соединение на воркер, поэтому приложения на Django сталкиваются с этой проблемой позже, но сразу в полном объеме, как только кто-то настраивает CONN_MAX_AGE или включает новую опцию "pool": True. Если вы запускаете приложение Django за Gunicorn и nginx, множителем является количество воркеров Gunicorn, а не количество серверов.
Что на самом деле меняет пул соединений Postgres на VPS
Пуллер — это процесс, который использует протокол PostgreSQL для взаимодействия с вашим приложением с одной стороны и поддерживает небольшой набор реальных соединений с сервером с другой. Он не ускоряет выполнение запросов. Он меняет то, кто отвечает за соединение, и количество реальных бэкендов.
Улучшаются два аспекта. Установка соединения перестает требовать выполнения fork и поиска в каталоге, которые заполняют пустой кэш бэкенда, так как пуллер отвечает на запрос клиента самостоятельно. Что еще важнее, количество реальных бэкендов перестает зависеть от количества соединений приложения, поэтому 500 клиентов могут использовать 20 бэкендов.
Ожидание — это и есть основная функция, и именно ее часто не понимают. Без пуллера 500 одновременных запросов получают по бэкенду и выполняются одновременно на двух ядрах CPU, из-за чего каждый из них работает медленно, а вся память расходуется в один момент. С пуллером 20 запросов выполняются, а остальные ждут несколько миллисекунд, поэтому каждый активный запрос получает реальную долю ресурсов CPU и завершается быстрее. Очередь перед небольшим пулом работает лучше, чем отсутствие очереди перед большим.
Пуллер не ограничивает другие ресурсы на машине. Если Postgres делит VPS с сервером приложений или с векторной базой данных на том же VPS, пуллер защищает только Postgres от вашего приложения. Установите жесткие лимиты и для соседних процессов: вы можете ограничить использование памяти и CPU сервисом с помощью systemd, чтобы один вышедший из-под контроля процесс не привел к падению базы данных. Место размещения самой базы данных влияет на то, как вы устанавливаете эти лимиты, что является практическим различием между запуском Postgres в Docker или напрямую на хосте.
Пулинг сессий против пулинга транзакций
Один параметр определяет всё остальное, и это pool_mode.
При пулинге сессий соединение с сервером закрепляется за клиентом на всё время жизни клиентского подключения и освобождается только при отключении клиента. Всё работает, так как пулер выступает в роли обычного прокси. Вы экономите только на стоимости установки соединения. Если приложение открывает 200 соединений, вам всё равно потребуется 200 бэкендов.
При пулинге транзакций соединение с сервером закрепляется за клиентом только на время выполнения одной транзакции. При COMMIT или ROLLBACK оно возвращается в пул, и его получает следующий ожидающий клиент. Именно это позволяет превратить 500 клиентов в 20 бэкендов. Это также то, что приводит к сбоям, и они происходят по архитектурным причинам: ваш следующий запрос может выполниться на другом бэкенде, отличном от предыдущего.
Значение по умолчанию в PgBouncer — pool_mode = session. Установите его, ничего не меняя, и вы получите «дешевую» часть функционала без каких-либо преимуществ. Третий режим, statement, возвращает соединение после каждого отдельного оператора и отклоняет многооператорные транзакции. Не трогайте его, если точно не знаете, зачем он вам нужен.
Какой режим транзакций перестает работать и почему
Все, что перечислено ниже, перестает работать по одной причине. Это состояние, которое существует внутри одного конкретного бэкенда, а пулинг транзакций не гарантирует, что вы дважды попадете на один и тот же бэкенд.
SETиRESETна уровне сессии.SET search_path,SET statement_timeout,SET TIME ZONEиSET ROLEприменяются к тому бэкенду, который выполнил этот оператор, и исчезают к началу вашей следующей транзакции. ИспользуйтеSET LOCALвнутри явной транзакции: она ограничена рамками этой транзакции и поэтому безопасна.LISTEN. Доставка уведомлений привязана к бэкенду, который выполнилLISTEN, а этот бэкенд передается другому клиенту сразу после завершения транзакции.NOTIFYпродолжает работать в режиме транзакций, что делает этот сбой неочевидным: отправка проходит успешно, но получение никогда не происходит. Если вам нуженLISTEN, откройте одно дополнительное соединение напрямую к порту 5432, минуя пулер.- Консультативные блокировки (advisory locks) уровня сессии.
pg_advisory_lock()удерживается сессией и снимается только при ее завершении. При пулинге транзакций ваш вызов разблокировки выполняется на другом бэкенде, поэтому блокировка остается активной до тех пор, пока PgBouncer не завершит соединение с сервером, что по умолчанию происходит черезserver_lifetime, то есть один час. Используйтеpg_advisory_xact_lock(), которая снимается в конце транзакции тем же бэкендом, который ее установил. PREPAREиDEALLOCATE, SQL-операторы. Никогда не доступны в режиме транзакций.WITH HOLDкурсоры и любые серверные курсоры, которые должны существовать дольше, чем их транзакция.- Временные таблицы, которые должны сохраняться после фиксации (commit).
CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWSсоздает таблицу во временной схеме одного бэкенда, а ваша следующая транзакция может попасть на другой бэкенд. LOAD.
Подготовленные выражения (prepared statements) на уровне протокола — это единственный пункт, который изменился. В PgBouncer 1.21.0 появилась их поддержка в режиме транзакций, а в 1.24.0 она включена по умолчанию путем установки max_prepared_statements в значение 200. В более старых сборках это значение равно 0, что означает отключено. В Ubuntu 24.04 поставляется PgBouncer 1.22.0, поэтому функция присутствует, но вам нужно установить max_prepared_statements самостоятельно. Если вы не уверены, как работает ваша сборка, безопаснее настроить это на стороне клиента: psycopg 3 перестает использовать серверные подготовленные выражения, если установить prepare_threshold в None.
В Django есть собственное название для этой проблемы. В документации указано, что «использование пулера соединений в режиме транзакций (например, PgBouncer) требует отключения серверных курсоров для этого соединения», поскольку «серверные курсоры доступны только в том соединении, в котором они были созданы». Установите DISABLE_SERVER_SIDE_CURSORS в True в настройках соответствующей базы данных, иначе каждый вызов .iterator() будет приводить к периодическим сбоям, которые проявляются только под нагрузкой.
Режим транзакций стоит использовать, но это своего рода контракт. Прочитайте список, проверьте свой ORM и библиотеку фоновых задач на соответствие этим требованиям, а затем переключайтесь.
Установка PgBouncer и перенаправление приложения на него
Приведенная ниже конфигурация предназначена для запуска на вашем собственном сервере: Ubuntu 24.04, где PostgreSQL уже ожидает подключений на 127.0.0.1, порт 5432.
sudo apt update
sudo apt install -y pgbouncer
pgbouncer --versionВ Ubuntu 24.04 доступен пакет PgBouncer 1.22.0. Актуальная версия от разработчика — 1.25.2 (по состоянию на август 2026 года). Проверьте, какая версия установлена у вас, так как поведение подготовленных выражений (prepared statements), описанное выше, зависит от версии.
Создайте роль, единственной задачей которой будет вход в консоль администратора PgBouncer, а затем создайте файл паролей. PgBouncer требуются секреты SCRAM (salted challenge response authentication mechanism) из pg_authid, а прочитать эту таблицу может только суперпользователь.
sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
> /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txtКопирование секретов, а не повторный ввод паролей, обеспечивает работоспособность этого механизма. PgBouncer может повторно использовать секрет SCRAM для входа в PostgreSQL только в том случае, если клиент также прошел аутентификацию через SCRAM, если секрет в файле побайтово совпадает с секретом в pg_authid (та же соль и количество итераций, а не просто тот же пароль), и если строка [databases] не содержит привязку к user=. Добавьте user=appuser в эту строку, и тогда PgBouncer потребуется пароль в открытом виде. Убедитесь, что владельцем файла является учетная запись, от имени которой работает служба, с помощью systemctl show pgbouncer -p User. Смена пароля в PostgreSQL означает необходимость пересоздания этого файла, иначе при следующем подключении будет возвращена ошибка password authentication failed.
Теперь создайте /etc/pgbouncer/pgbouncer.ini.
[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digitslisten_addr = 127.0.0.1 защищает пул соединений от доступа из публичного интернета, что важно, так как доступный извне пул — это точка аутентификации, которую вы, вероятно, не планировали публиковать. max_client_conn — это количество подключений от приложения, которые примет PgBouncer; это «дешевый» ресурс, поэтому значение может быть большим. default_pool_size — это количество реальных соединений с бэкендом, которые могут удерживаться для пары база данных-пользователь; это «дорогой» ресурс. max_db_connections ограничивает общее количество соединений к базе данных значением 80, оставляя запас в рамках max_connections для psql, резервного копирования и мониторинга. ignore_startup_parameters = extra_float_digits предотвращает отклонение PgBouncer драйверов (включая JDBC), которые отправляют этот параметр во время подключения.
sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pagerПри успешном запуске в логах появится строка о том, что PgBouncer ожидает подключений на 127.0.0.1:6432. Если запуск не удался из-за файла паролей, в логах будет указан путь, который не удалось прочитать; почти всегда это проблема прав доступа или владельца, а не синтаксиса. После этого измените строку подключения приложения с порта 5432 на порт 6432 и перезапустите его. Никаких других изменений в приложении не требуется.
Как проверить работу пула соединений
У PgBouncer есть консоль администратора, доступ к которой осуществляется через виртуальную базу данных с именем pgbouncer.
psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncerSHOW POOLS;
SHOW STATS;
SHOW CLIENTS;За состоянием SHOW POOLS нужно следить в первую очередь. cl_active — это количество клиентов, подключенных к серверному соединению, cl_waiting — количество клиентов в очереди на соединение, sv_active и sv_idle — количество фактически используемых и свободных соединений с бэкендом, а maxwait — время ожидания клиента в начале очереди в секундах. При нормальной нагрузке исправная работа означает, что cl_waiting равно 0, а maxwait равно 0. Если maxwait превышает одну-две секунды, значит, размер пула слишком мал или запросы выполняются слишком медленно; в этих случаях требуются разные методы исправления.
Прежде чем увеличивать default_pool_size, проверьте, в чем именно заключается проблема.
SELECT state, count(*) FROM pg_stat_activity
WHERE backend_type = 'client backend' GROUP BY state;Если большинство соединений с бэкендом находятся в состоянии idle in transaction, проблема не в размере пула. Приложение открывает транзакцию и выполняет внутри неё медленную операцию, например, HTTP-запрос, поэтому каждое соединение с бэкендом удерживается, не выполняя при этом SQL-запрос. Параметр idle_in_transaction_session_timeout поможет принудительно завершить такие сессии, но реальное решение проблемы находится в коде приложения. Если же все соединения с бэкендом находятся в состоянии active, пул действительно перегружен, и перед увеличением количества соединений стоит провести EXPLAIN (ANALYZE, BUFFERS) запросов.
Что касается выбора размера, наиболее часто упоминаемой отправной точкой является эвристика HikariCP: примерно удвоенное количество ядер плюс один. Для VPS с 2 ядрами это значение равно 5. Рассматривайте это как базовую рекомендацию, установите default_pool_size близко к этому значению и корректируйте его на основе maxwait. Маленькие пулы кажутся неэффективными, но на практике показывают лучшие результаты, так как ожидающее соединение не потребляет ресурсы, в то время как активное соединение расходует CPU, память и создает конкуренцию за блокировки.
Выбор между PgBouncer, PgDog и Pgpool-II
PgBouncer — решение для стандартных задач: один сервер PostgreSQL, один VPS и приложение, которое открывает больше соединений, чем может выдержать система. Он выполняет одну функцию, его конфигурация состоит из одного ini-файла, и он включен в репозитории Debian и Ubuntu. Обработка соединений происходит в одном потоке, чего достаточно для нагрузки уровня VPS; ограничение производительности проявляется только на значительно более мощных машинах.
На PgDog стоит обратить внимание, если решение о маршрутизации должно приниматься на том же сетевом узле, что и пулинг. Это прокси для масштабирования PostgreSQL, написанный на Rust. Он поддерживает пулинг транзакций и сессий, разделение чтения и записи через парсинг запросов, а также шардирование с маршрутизацией между шардами и двухфазную фиксацию транзакций. Используйте его, если у вас есть primary-сервер и одна или несколько реплик, и вы хотите направлять запросы на чтение к репликам, не внося изменения в логику приложения. Два предостережения. Продукт распространяется под лицензией AGPLv3, поэтому перед внедрением в production согласуйте с юридическим отделом вопрос использования в сети; позиция самого проекта заключается в том, что внутреннее использование и частные модификации не создают обязательств по раскрытию исходного кода. Проект молодой, релизы выходят еженедельно, а версии имеют индекс 0.x, поэтому фиксируйте конкретный тег релиза, а не используйте main.
git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.tomlДля сборки из исходного кода требуется актуальная стабильная toolchain Rust, CMake и C/C++ компилятор. На странице релизов также доступны готовые бинарные файлы для Linux и пакеты Debian, а на ghcr.io/pgdogdev/pgdog — образ контейнера. Конфигурация разделена на два файла. Первый содержит общие настройки и по одной записи для каждой базы данных; здесь они представлены в виде массива TOML с inline-таблицами, чтобы эти два типа данных было легко различать.
databases = [
{ name = "appdb", host = "127.0.0.1" },
]
[general]
port = 6432
default_pool_size = 10Второй файл содержит по одной записи для каждого пользователя в той же форме массива.
users = [
{ name = "appuser", database = "appdb", password = "change-this" },
]По умолчанию PgDog слушает порт 6432, как и PgBouncer, поэтому они не могут одновременно использовать порт по умолчанию на одном хосте.
Pgpool-II, версия 4.7.2 на июнь 2026 года, предлагает пулинг и балансировку нагрузки, а также механизм watchdog для автоматического переключения при сбоях. Дополнительные функции привносят дополнительные сценарии отказа, и перед выбором важно понять модель пулинга. Pgpool-II создает num_init_children дочерних процессов (pre-fork), и каждый процесс кэширует до max_pool соединений с сервером, поэтому предел количества бэкендов равен num_init_children, умноженному на max_pool. Каждый дочерний процесс обслуживает одного клиента за раз, поэтому количество клиентов, которые вы можете принять, равно num_init_children и фиксируется при запуске; простаивающий клиент всё равно занимает дочерний процесс. Если установить num_init_children в 100, а max_pool в 4, вы получите 400 авторизованных бэкендов, что в точности является той проблемой, для решения которой вы устанавливали пулер. Выбирайте Pgpool-II, если вам нужны функции переключения при сбоях и маршрутизации запросов, и тщательно выполняйте расчеты. Если ваша единственная цель — сократить количество бэкендов, это решение избыточно.
Вопрос об управляемом прокси и его самостоятельно разворачиваемом аналоге
Управляемые платформы продают это как отдельный продукт. AWS размещает RDS Proxy перед RDS, а Supabase ставит свой собственный пулер, Supavisor, перед Supabase Postgres. Оба решения выполняют описанную здесь задачу: дешево удерживают клиентские соединения и распределяют их между меньшим количеством реальных бэкендов. Supavisor имеет открытый исходный код и может быть развернут самостоятельно, поэтому выбор не сводится к противопоставлению проприетарного решения бесплатному.
Самостоятельно разворачиваемый аналог управляемого прокси — это не другая концепция. Это та же самая идея, но с конфигурационным файлом в ваших руках: PgBouncer в режиме транзакций, работающий на том же VPS, что и база данных, и слушающий 127.0.0.1. Существуют два реальных отличия. Управляемый прокси находится на расстоянии одного сетевого перехода, поэтому он добавляет задержку и продолжает удерживать клиентские соединения, пока база данных перезагружается под ним. PgBouncer на хосте базы данных добавляет переход через loopback, что практически бесплатно, но он завершает работу при падении этого хоста. Если вам нужно поведение, позволяющее пережить перезагрузку, вам также потребуется механизм обеспечения отказоустойчивости — именно здесь сложность Pgpool-II с его watchdog или проверок состояния в PgDog начинает себя оправдывать.
В этот список стоит добавить еще один вариант. Если количество соединений — это главный фактор, усложняющий ваше развертывание, то у встроенной базы данных нет модели соединений, требующей пулинга, поскольку она является библиотекой внутри вашего процесса, а не сервером на порту. Для одного сервера приложений с умеренным объемом записи использование SQLite в production на VPS устраняет эту проблему целиком, вместо того чтобы заниматься ее решением. Когда вам действительно понадобится настоящий сервер, сначала определите размер пула, а затем — размер машины.
FAQ
Нужен ли мне PgBouncer, если в приложении уже есть пул соединений?
Обычно да, так как пул приложения работает в рамках одного процесса и не видит остальные. Четыре воркера Gunicorn, каждый из которых держит пул из 20 80 бэкендов, плюс два фоновых воркера, дают в сумме 120. PgBouncer — единственный компонент, который видит общее количество и может его ограничить. Оптимальная схема — использовать оба уровня: небольшой пул внутри каждого воркера, чтобы запросы не тратили время на TCP-соединение, и PgBouncer в режиме transaction mode для ограничения реальных бэкендов.
Что именно перестает работать при переключении PgBouncer в режим transaction mode?
Все, что сохраняет состояние в одном бэкенде между транзакциями. Переменные уровня сессии SET и RESET, LISTEN, курсоры WITH HOLD, SQL-команды PREPARE и DEALLOCATE, advisory locks уровня сессии, временные таблицы, которые должны существовать после коммита, и LOAD. NOTIFY продолжает работать, из-за чего ошибки LISTEN выглядят как баг доставки, а не как проблема пулинга. В Django установите DISABLE_SERVER_SIDE_CURSORS в значение True. В psycopg 3 либо установите prepare_threshold в None, либо используйте PgBouncer версии 1.22 или новее с max_prepared_statements больше 0. Замените pg_advisory_lock() на pg_advisory_xact_lock().
Каким должен быть размер default_pool_size на VPS с 2 ядрами?
Меньше, чем кажется на первый взгляд. Широко известная эвристика HikariCP рекомендует удвоенное количество ядер плюс один, то есть около 5 для двух ядер, но это лишь отправная точка, а не готовый ответ. Установите значение, затем отслеживайте maxwait и cl_waiting в SHOW POOLS под реальной нагрузкой. Нулевые значения для обоих параметров означают, что размер пула достаточен. Рост maxwait указывает на то, что клиенты стоят в очереди. Прежде чем увеличивать количество соединений, проверьте pg_stat_activity: бэкенды, застрявшие в idle in transaction, обычно свидетельствуют об ошибке в приложении, которую увеличение числа соединений лишь замаскирует.
PgBouncer или PgDog?
PgBouncer подходит для одного сервера PostgreSQL на одной VPS, что покрывает большинство сценариев развертывания. Он есть в репозиториях Ubuntu, его поведение хорошо задокументировано, а вся конфигурация умещается в один ini-файл. PgDog стоит выбирать, когда требуется разделение чтения/записи между репликами или шардирование на том же уровне, где происходит пулинг, чтобы приложению не нужно было знать топологию. Перед внедрением PgDog согласуйте вопрос лицензии AGPLv3 с юридическим отделом вашей компании и зафиксируйте конкретный релиз, так как проект все еще находится на стадии версий 0.x и выпускает обновления еженедельно.