SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Настройка пула соединений PostgreSQL на VPS

Каждое соединение PostgreSQL создает отдельный процесс, что приводит к ошибке Killed и перезапуску базы при нехватке RAM. Узнайте, как пул соединений решает эту проблему.

Почему на небольшом 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 соединений, а затем запускают приложение на нескольких узлах.

ГрафикBackends requested per app topology, pool size 20 per worker (arithmetic)
Данные, на которых построен график
[
  {
    "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, возвращает соединение после каждого отдельного оператора и отклоняет многооператорные транзакции. Не трогайте его, если точно не знаете, зачем он вам нужен.

Какой режим транзакций нарушается и почему

Все перечисленное ниже не работает по одной причине. Это состояние, которое существует внутри одного конкретного backend, а пулинг транзакций не гарантирует, что вы получите тот же самый backend во второй раз.

  • SET и RESET на уровне сессии. SET search_path, SET statement_timeout и SET ROLE применяются к тому backend, который выполнил этот запрос, и исчезают к началу вашей следующей транзакции или остаются активными, когда этот backend передается другому клиенту. Используйте SET LOCAL внутри явной транзакции: она ограничена рамками этой транзакции и поэтому безопасна. PgBouncer отслеживает короткий список параметров, о которых PostgreSQL сообщает клиенту (включая TimeZone, client_encoding и application_name), и восстанавливает их на любом backend, который вы получаете. Начиная с PgBouncer 1.26.0, этот список включает search_path, но только если используется PostgreSQL 18 или новее.
  • LISTEN. Доставка уведомлений привязана к тому backend, который выполнил LISTEN, а этот backend передается другому клиенту сразу после завершения транзакции. NOTIFY продолжает работать в режиме транзакций, что делает эту ошибку неочевидной: отправка проходит успешно, но получение никогда не происходит. Если вам необходим LISTEN, откройте одно дополнительное соединение напрямую к порту 5432, минуя пулер.
  • Advisory locks на уровне сессии. pg_advisory_lock() удерживается сессией и снимается только при её завершении. При пулинге транзакций ваш вызов разблокировки выполняется на другом backend, поэтому блокировка остается активной до тех пор, пока PgBouncer не закроет соединение с сервером, что по умолчанию происходит через server_lifetime (один час). Используйте pg_advisory_xact_lock(), которая снимается в конце транзакции тем же самым backend, который её установил.
  • PREPARE и DEALLOCATE (SQL-инструкции). Недоступны в режиме транзакций.
  • WITH HOLD курсоры и любые серверные курсоры, которые должны существовать дольше, чем их транзакция.
  • Временные таблицы, которые должны сохраняться после коммита. CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWS создает таблицу во временной схеме одного backend, а ваша следующая транзакция может попасть на другой.
  • 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. По состоянию на октябрь 2026 года актуальная версия от разработчика — 1.26.0. Проверьте установленную версию, так как поведение подготовленных выражений (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_digits

Параметр listen_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 pgbouncer
SHOW 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. Он поддерживает пулинг транзакций и сессий, разделение запросов на чтение и запись путем их анализа, а также шардирование с маршрутизацией между шардами и двухфазную фиксацию транзакций. Используйте его, если у вас есть основной сервер и одна или несколько реплик, и вы хотите направлять запросы на чтение к репликам, не внося изменения в логику приложения. Учтите два момента. Продукт распространяется по лицензии 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

Для сборки из исходного кода требуются актуальная стабильная версия Rust, CMake и компилятор C/C++. На странице релизов также доступны готовые бинарные файлы для Linux и пакеты Debian, а образ контейнера находится по адресу ghcr.io/pgdogdev/pgdog. Конфигурация разделена на два файла. Первый содержит общие настройки и по одной записи для каждой базы данных; они оформлены как массив TOML с вложенными таблицами, чтобы их было легко различать.

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.3 (по состоянию на октябрь 2026 года) предлагает пулинг, балансировку нагрузки и механизм watchdog для автоматического переключения при сбоях. Дополнительные функции увеличивают количество возможных точек отказа, поэтому перед выбором важно понять модель пулинга. Pgpool-II заранее создает num_init_children дочерних процессов, каждый из которых кэширует до 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 и выпускает обновления еженедельно.

#postgres#pgbouncer#pgdog#connections#performance