Как организовать пул соединений с базой данных при большом количестве запросов в секунду?
Что проверяют
Понимаете ли вы, что размер пула ограничивает не желание приложения, а число соединений, которое выдержит база. Интервьюер ждёт расчёт: процессы, соединения на каждый и итог на стороне PostgreSQL. Следом обычно спрашивают про PgBouncer и про то, что будет, когда пул исчерпан.
Как отвечать
Начните с размера. По закону Литтла одновременно занято примерно RPS × среднее время запроса: 2000 запросов в секунду по 5 мс держат около 10 соединений. Большой пул не ускоряет, а перегружает базу: у PostgreSQL каждое соединение это отдельный процесс, а max_connections по умолчанию 100.
Дальше инструменты:
- SQLAlchemy:
create_engine(url, pool_size=..., max_overflow=..., pool_timeout=..., pool_pre_ping=True, pool_recycle=1800). По умолчаниюpool_size=5,max_overflow=10, ожидание 30 секунд.pool_pre_pingотсекает соединения, которые оборвал балансировщик или файрвол; - async:
asyncpg.create_pool(min_size=..., max_size=...)илиcreate_async_engineв SQLAlchemy, для psycopg 3 есть отдельный пакетpsycopg_pool; - внешний пулер: PgBouncer в режиме
transactionраздаёт несколько десятков серверных соединений тысячам клиентских. Цена: нельзя полагаться на состояние сессии (SET, advisory-блокировки, временные таблицы), а с asyncpg обычно выключают кэш подготовленных выражений черезstatement_cache_size=0; - арифметика процессов: пул живёт внутри процесса. 8 воркеров Gunicorn с
pool_size=10иmax_overflow=10дают до 160 соединений, это уже больше дефолтного лимита.
Соединение берут на время запроса и сразу отдают: with Session() as session:, и никаких вызовов чужих API, пока транзакция открыта.
Где спотыкаются
Ставят pool_size=100 «с запасом». При нескольких воркерах база упирается в max_connections и память, и всё становится медленнее.
Создают engine до форка Gunicorn с preload_app. Дочерние процессы наследуют одни и те же сокеты, и запросы разных воркеров перемешиваются в одном соединении. Engine создают после форка или вызывают dispose() в каждом воркере.
Копируют пример с session.execute('SELECT ...'). В SQLAlchemy 2.0 голую строку передать нельзя, нужен text(), и такой код падает сразу.