Kravchenko

Web Lab

АудитБлогКонтакты

Kravchenko

Web Lab

Разрабатываем сайты и автоматизацию на современных фреймворках под ключ

Услуги
ЛендингМногостраничныйВизитка
E-commerceБронированиеПортфолио
Навигация
БлогКонтактыАудит
Обратная связь
+7 921 567-11-16
info@kravlab.ru
с 09:00 до 18:00

© 2026 Все права защищены

•

ИП Кравченко Никита Владимирович

•

ОГРНИП: 324784700339743

Политика конфиденциальности

Пулинг соединений к базе (pgbouncer и пул в приложении): меньше падений и дешевле инфраструктура

Разработка и технологии31 января 2026 г.
Соединения к базе — дорогой ресурс. Правильный пулинг позволяет выдерживать всплески трафика, не уронить PostgreSQL и тратить меньше на серверы. В статье — как спроектировать и настроить pgbouncer и пул в приложении, типичные грабли и готовые конфиги.
Пулинг соединений к базе (pgbouncer и пул в приложении): меньше падений и дешевле инфраструктура

Оглавление

  • Зачем вообще нужен пул соединений
  • Как устроены соединения в PostgreSQL и где узкое место
  • Варианты пуллинга: в приложении и через pgbouncer
  • Базовая безопасная схема для большинства сервисов
  • Настройка pgbouncer: рабочий пример
  • Настройка PostgreSQL под пулинг
  • Размер пула в приложении: простая методика расчёта
  • Примеры кода: Node.js (pg) и Go (pgx)
    • Node.js (pg)
    • Go (pgx + pgxpool)
  • Типичные грабли и как их обходить
  • Наблюдаемость и алерты
  • Экономика: где бизнес выигрывает
  • Чек‑лист внедрения
  • Итоги

Зачем вообще нужен пул соединений

Каждое новое соединение к базе — это не просто «дырка в сокете». Для PostgreSQL это отдельный серверный процесс, память, планировщик, переключения контекста, рукопожатия, а иногда ещё и TLS. Когда приложение создаёт сотни и тысячи коротких соединений, база тратит больше времени на обслуживающий шум, чем на сами запросы. Итоги предсказуемы: скачки задержек, ошибки «не могу подключиться», рост потребления памяти и CPU.

Пул соединений решает эту проблему. Приложение отдаёт запросы в пул, а пул — дозирует их к базе через небольшое число постоянных серверных соединений. В результате:

  • всплески трафика не «прокалывают» базу тысячами подключений;
  • запросы исполняются стабильнее, потому что очередь контролируема;
  • расход памяти и CPU на сервере БД падает;
  • можно горизонтально масштабировать фронт без линейного роста max_connections на базе.

Как устроены соединения в PostgreSQL и где узкое место

PostgreSQL создаёт отдельный процесс на каждое клиентское соединение. Даже «пустой» процесс требует памяти (как правило, несколько мегабайт) и времени CPU. Когда таких соединений сотни и тысячи, увеличиваются:

  • потребление памяти (десятки гигабайт в сумме);
  • накладные расходы на переключение контекста между процессами;
  • вероятность блокировок и конкуренции за ресурсы ядра.

База начинает «пылесосить» ресурсы на обслуживание соединений вместо выполнения запросов. Пулинг решает именно эту часть.

Варианты пуллинга: в приложении и через pgbouncer

Есть два основных подхода:

  1. Пул в приложении.
  • Библиотека драйвера держит ограниченное число соединений и раздаёт их потокам запросов.
  • Плюсы: проще деплой, гибкий контроль таймаутов и деградации на уровне сервиса.
  • Минусы: если сервисов много, суммарно они всё равно могут открыть слишком много соединений к базе.
  1. Внешний пул (pgbouncer).
  • Лёгкий прокси для PostgreSQL, который мультиплексирует множество клиентских соединений в небольшой набор серверных.
  • Поддерживает режимы: session, transaction, statement. Чаще всего нужен transaction-пулинг для веб‑нагрузки.
  • Плюсы: централизованный контроль, защита базы от бурстов, мониторинг.
  • Минусы: ограничения на сессионное состояние (см. грабли ниже).

На практике лучше комбинировать: небольшой пул в каждом приложении плюс pgbouncer перед базой.

Базовая безопасная схема для большинства сервисов

  • Разместите pgbouncer рядом с базой (на том же хосте или в той же сети) и включите режим transaction pooling.
  • В приложениях включите пул драйвера, но ограничьте размер так, чтобы суммарно все сервисы не превышали полосу pgbouncer и лимит max_connections на базе.
  • Отключите сессионные функции, несовместимые с transaction pooling: подготовленные выражения на уровне сессии, долгоживущие временные таблицы и т.п.
  • Включите короткие и понятные таймауты: ожидание соединения/очереди, statement_timeout, idle_in_transaction_session_timeout.
  • Введите отказ с очереди (load shedding): если пул заполнен и ждать нельзя, возвращайте 503 быстро, а не вешайте всех пользователей.

Настройка pgbouncer: рабочий пример

[databases]
# Имена логические -> куда pgbouncer коннектится
app = host=127.0.0.1 port=5432 dbname=appdb auth_user=pgbouncer_user

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
unix_socket_dir =

# Режим пуллинга: для веб-нагрузки чаще всего transaction
pool_mode = transaction

# Сколько клиентских соединений pgbouncer готов принять
max_client_conn = 5000

# Сколько серверных соединений pgbouncer держит к реальной БД
# Это главное «окно» в базу: подберите под ядра/нагрузку
default_pool_size = 100
reserve_pool_size = 20

# Таймауты и «здоровье»
server_idle_timeout = 600
server_lifetime = 3600
query_timeout = 15s
query_wait_timeout = 2s

# Аутентификация
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# Приводим сессию к чистому состоянию
server_reset_query = DISCARD ALL
server_reset_query_always = 1

# Игнорируем параметры, мешающие мультиплексировать соединения
ignore_startup_parameters = extra_float_digits,options

# TLS при необходимости
# client_tls_sslmode = require
# client_tls_key_file = /etc/pgbouncer/client.key
# client_tls_cert_file = /etc/pgbouncer/client.crt
# client_tls_ca_file = /etc/pgbouncer/ca.crt

# Логи
log_connections = 1
log_disconnections = 1
stats_period = 60

Файл /etc/pgbouncer/userlist.txt:

"pgbouncer_user" "md5<хеш_пароля>"
"app_user" "md5<хеш_пароля>"

Важно: default_pool_size — это суммарные серверные соединения на БД-юзер/БД-пару. Если у вас несколько сервисов/пользователей, распределите лимит осознанно.

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

На стороне БД:

  • max_connections держите низким и предсказуемым (например, 100–300), опираясь на default_pool_size в pgbouncer.
  • Включите защитные таймауты:
    • statement_timeout = '5s' (или ваш безопасный предел);
    • idle_in_transaction_session_timeout = '30s'.
  • Оставьте запас админских коннектов: superuser_reserved_connections = 3.
  • Контроль «висячих» простоя: tcp_keepalives_idle, tcp_keepalives_interval, tcp_keepalives_count.
  • Логи: log_min_duration_statement для поиска долгих запросов.

Размер пула в приложении: простая методика расчёта

Чтобы прикинуть нужный размер пула, используйте простую модель очереди:

  • Среднее время SQL‑части запроса: L (секунд).
  • Желаемая пропускная способность по БД: X RPS.
  • Минимум активных соединений для этой пропускной: N = X * L.

Пример: у вас 100 RPS, средняя SQL‑нагрузка на запрос 20 мс (0,02 секунды). Тогда N = 100 * 0,02 = 2. Берём запас ×2–×3 на пики и разброс: 4–6 соединений. Для микросервиса это обычно 4–16. Если сервисов 20 штук, сумма перебора на базе недопустима: пусть каждый держит небольшой пул, а pgbouncer дозирует поверх.

Главное правило: рост числа инстансов сервиса не должен линейно увеличивать соединения к базе. В идеале суммарный лимит известен и контролируется через pgbouncer и max_connections.

Примеры кода: Node.js (pg) и Go (pgx)

Node.js (pg)

// package.json должен содержать "pg"
// npm install pg
const { Pool } = require('pg');

// Важно: ограничить пул и ввести таймауты
const pool = new Pool({
  host: process.env.DB_HOST || '127.0.0.1',
  port: Number(process.env.DB_PORT || 6432), // идём через pgbouncer
  database: process.env.DB_NAME || 'appdb',
  user: process.env.DB_USER || 'app_user',
  password: process.env.DB_PASSWORD || 'secret',

  // Пул приложения
  max: Number(process.env.DB_POOL_MAX || 8),
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,

  // Удобно для диагностики
  application_name: 'web-api',

  // Сразу просим сервер убивать слишком долгие запросы
  statement_timeout: 5000,
});

async function queryWithTimeout(sql, params, timeoutMs = 3000) {
  const client = await pool.connect();
  try {
    // Доп. страховка на уровне клиента
    const timeout = setTimeout(() => {
      try { client.release(); } catch (_) {}
    }, timeoutMs + 100);

    // Локально можно ужесточить timeout для запроса
    await client.query('SET LOCAL statement_timeout = $1', [String(timeoutMs)]);
    const res = await client.query(sql, params);
    clearTimeout(timeout);
    return res.rows;
  } finally {
    client.release();
  }
}

// Пример HTTP‑хэндлера с load shedding
const http = require('http');

const server = http.createServer(async (req, res) => {
  if (req.url === '/health') {
    res.writeHead(200); res.end('ok'); return;
  }

  const start = Date.now();
  try {
    const rows = await Promise.race([
      queryWithTimeout('SELECT now() AS ts', [], 1000),
      new Promise((_, reject) => setTimeout(() => reject(new Error('queue-timeout')), 800)),
    ]);

    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ts: rows[0].ts, took_ms: Date.now() - start }));
  } catch (e) {
    // Если очередь пула забита — сразу 503, чтобы не завалить базу
    const status = e.message === 'queue-timeout' ? 503 : 500;
    res.writeHead(status, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: e.message }));
  }
});

server.listen(8080, () => console.log('Listening on :8080'));

Советы:

  • Не делайте пул «безлимитным», иначе при всплеске все запросы повиснут в ожидании соединения.
  • Лучше быстро отказать части запросов (503), чем положить всю систему.

Go (pgx + pgxpool)

// go get github.com/jackc/pgx/v5/pgxpool
package main

import (
	context "context"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
)

func main() {
	dsn := fmt.Sprintf(
		"host=%s port=%s dbname=%s user=%s password=%s pool_max_conns=%s application_name=web-api",
		getEnv("DB_HOST", "127.0.0.1"),
		getEnv("DB_PORT", "6432"),
		getEnv("DB_NAME", "appdb"),
		getEnv("DB_USER", "app_user"),
		getEnv("DB_PASSWORD", "secret"),
		getEnv("DB_POOL_MAX", "8"),
	)

	cfg, err := pgxpool.ParseConfig(dsn)
	if err != nil {
		log.Fatalf("parse config: %v", err)
	}

	// Страхуемся от подвисших коннектов и «стариков»
	cfg.MaxConnLifetime = time.Hour
	cfg.MaxConnIdleTime = 30 * time.Second
	cfg.HealthCheckPeriod = 30 * time.Second
	cfg.MaxConnLifetimeJitter = 30 * time.Second

	// Просим сервер обрывать долгие запросы
	cfg.ConnConfig.RuntimeParams["statement_timeout"] = "5000" // мс

	// Ограничим ожидание соединения из пула
	cfg.MaxConnAcquireTimeout = 800 * time.Millisecond

	pool, err := pgxpool.NewWithConfig(context.Background(), cfg)
	if err != nil {
		log.Fatalf("connect pool: %v", err)
	}
	defer pool.Close()

	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("ok"))
	})

	http.HandleFunc("/time", func(w http.ResponseWriter, r *http.Request) {
		ctx, cancel := context.WithTimeout(r.Context(), 900*time.Millisecond)
		defer cancel()

		// Если пул забит, NewWithConfig учтёт MaxConnAcquireTimeout: вернётся ошибка
		var ts time.Time
		if err := pool.QueryRow(ctx, "SELECT now()::timestamptz").Scan(&ts); err != nil {
			status := http.StatusInternalServerError
			if err == context.DeadlineExceeded {
				status = http.StatusServiceUnavailable // быстрый отказ, чтобы не душить базу
			}
			w.WriteHeader(status)
			_, _ = w.Write([]byte(err.Error()))
			return
		}

		w.Header().Set("Content-Type", "text/plain")
		_, _ = w.Write([]byte(ts.Format(time.RFC3339Nano)))
	})

	log.Println("listen :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func getEnv(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}

Ключевые моменты:

  • MaxConnAcquireTimeout не даёт запросам вечно висеть в очереди пула.
  • statement_timeout на уровне соединения защищает базу от «вечных» запросов.
  • Лучше вернуть 503 быстро, чем положить всю систему.

Типичные грабли и как их обходить

  1. Подготовленные выражения (prepared statements) и transaction pooling.
  • В transaction‑режиме pgbouncer меняет серверные соединения от запроса к запросу. Сессионные prepared‑statement’ы теряются. Решения:
    • отключить prepared statements в драйвере (например, в PG JDBC — prepareThreshold=0, в некоторых ORM — своя настройка);
    • либо переключиться на session pooling (но тогда меньше выгоды от мультиплексирования);
    • либо использовать только «временные» prepared внутри транзакции.
  1. Сессионное состояние (SET search_path, SET ROLE, temp‑таблицы).
  • После запроса pgbouncer делает DISCARD ALL и состояние сбрасывается.
  • Если нужно настраивать параметры — делайте SET LOCAL ... внутри транзакции.
  1. Долгие «висящие» транзакции.
  • Они держат блокировки и мешают автоочистке (VACUUM). Включите idle_in_transaction_session_timeout на стороне БД и контролируйте это метриками.
  1. Шторма соединений при релизах.
  • Если десятки инстансов стартуют одновременно, они одновременно ломятся за коннектами.
  • Решение: добавьте джиттер при старте (случайная задержка 0–5 секунд), предварительное «прогревание» пула, лимит одновременных стартующих воркеров.
  1. Здоровья‑чеки, которые сами перегружают базу.
  • Если health‑probe делает запрос к БД каждую секунду с отдельным соединением, толку мало.
  • Решение: используйте лёгкий запрос через общий пул и разумную частоту, а главное — отделите «готовность обрабатывать трафик» от «доступности базы». Иногда сервис готов отдавать кэш даже если БД недоступна.
  1. Вечные соединения.
  • Без ротации соединений накапливаются утечки состояния, проблемы с балансировкой и сетевые артефакты.
  • Решение: ограничьте срок жизни соединений (server_lifetime в pgbouncer, MaxConnLifetime в драйвере), добавьте небольшую случайность.
  1. TLS между приложением и pgbouncer/БД.
  • Внутри защищённой сети часто отключают TLS ради производительности, но это риск при сложной сетевой топологии.
  • На практике TLS‑накладные расходы малы. Если есть сомнения — включайте TLS хотя бы с pgbouncer до БД.

Наблюдаемость и алерты

Что мониторить:

  • В pgbouncer: SHOW POOLS — активные/ожидающие клиенты, занятые серверные соединения. Тревога, если есть устойчивые ожидания (cl_waiting > 0) дольше N секунд.
  • В приложении: среднее и 95‑й перцентиль времени ожидания соединения из пула; доля быстрых отказов (503) из‑за переполнения очереди.
  • В БД: средняя длительность запросов, количество активных запросов, блокировки, autovacuum, deadlocks.

Полезные команды:

-- pgbouncer (через psql на порт pgbouncer)
SHOW STATS;
SHOW POOLS;
SHOW SERVERS;
SHOW CLIENTS;

Экспортеры метрик для Prometheus есть как для PostgreSQL, так и для pgbouncer — подключите их первыми.

Экономика: где бизнес выигрывает

  • Меньше серверов БД. Каждый «лишний» коннект стоит мегабайты памяти и доли CPU. С pgbouncer можно держать max_connections в 3–10 раз ниже.
  • Стабильнее задержки. Стабильная латентность — выше конверсия и меньше отказов корзины.
  • Предсказуемое масштабирование. Можно спокойно докатывать новые инстансы фронта — база не рушится от «штормов» подключений.
  • Проще аварийные режимы. Пулы с таймаутами и быстрыми отказами позволяют системе деградировать, а не падать.

Грубая прикидка: если без пула ваша база держала 1500 соединений, а с pgbouncer — 200–300, экономия памяти может составлять десятки гигабайт. Это либо меньше класс машины, либо больше запас под кэш и планы.

Чек‑лист внедрения

  • Включить pgbouncer в режиме transaction, настроить auth, таймауты и DISCARD ALL.
  • Снизить max_connections на сервере БД до разумного уровня и включить защитные таймауты.
  • В каждом сервисе ограничить размер пула, ввести таймаут ожидания соединения и быстрые отказы.
  • Отключить prepared statements на уровне сессии или перейти на session pooling, если они критичны.
  • Перенести SET‑параметры в SET LOCAL внутри транзакций.
  • Добавить джиттер старта и прогрев пула при релизах.
  • Подключить метрики pgbouncer и БД, настроить алерты на рост очередей и долгие запросы.
  • Провести нагрузочные прогоны: проверить поведение при переполнении очереди и деградации сети.

Итоги

Пулинг соединений — простая по идее, но очень эффективная практика. Комбинация pgbouncer и небольших пулов в приложениях даёт контролируемую очередь запросов, держит базу в стабильном состоянии, снижает расходы и делает систему предсказуемой на пиках. Внедрение занимает день‑два, а отдача заметна сразу: исчезают «шторма соединений», улучшается latency, падают расходы на классы машин. Это тот случай, когда инженерная дисциплина напрямую экономит деньги и улучшает продуктовые метрики.


PostgreSQLpgbouncerпул соединений