2025, Dec 10 21:02

Пулы подключений в Django 5: psycopg3‑пул на процесс или общий PGBouncer

Разбираем работу подключений в Django 5: встроенный пул psycopg3 на процесс, PGBouncer как общий пул, влияние потоков и async. Помогаем выбрать стратегию.

Когда сравнивают модель FastAPI «один процесс с общим пулом» с привычным разворачиванием Django за Gunicorn, путаница часто связана с тем, сколько рабочих процессов вы запускаете и какую форму параллелизма они дают. В Django 5.x появился встроенный пул подключений к PostgreSQL через psycopg3, а ещё давно существует вариант вынести пул в отдельный прокси — PGBouncer. Важно понять, где находится пул и кто действительно может пользоваться им одновременно — от этого и зависит верный выбор.

Что на самом деле запущено

В продакшене приложения Django обычно обслуживаются сервером вроде Gunicorn. Такой сервер может запускать ваш код по-разному. Процесс может быть полностью синхронным и обрабатывать лишь один запрос за раз. Тот же процесс может оставаться синхронным, но использовать несколько потоков — тогда несколько запросов идут параллельно внутри одного процесса. Либо процесс работает асинхронно, используя цикл событий и обслуживая несколько запросов одновременно, пока выполняется ввод‑вывод. Для пулов эти режимы важнее, чем «постоянство» воркера в философском смысле: по делу важно, могут ли несколько текущих запросов в одном процессе одновременно задействовать несколько подключений к базе.

Как устроен встроенный пул Django

Встроенный пул в Django 5.x использует psycopg3 и библиотеку psycopg_pool. Пул живёт внутри одного процесса и не разделяется между процессами. Внутри его обслуживает рабочий поток, управляющий подключениями, — концептуально это похоже на то, как ведёт себя пул asyncpg в одном процессе FastAPI. Ключевой момент: если вы запускаете несколько процессов, у каждого из них будет свой собственный независимый пул.

Где здесь PGBouncer

PGBouncer — это отдельный процесс. Он создает и управляет собственным пулом подключений в своём адресном пространстве, и с ним может работать любой воркер Django. Поскольку он вынесен за пределы приложения, такой пул по умолчанию общий для всех процессов.

Пример кода: пул, который не помогает однопоточному воркеру

Рассмотрим упрощённую модель. Однопоточный синхронный воркер способен обрабатывать лишь один запрос за раз. Даже если вы создадите пул с множеством подключений, такой воркер сможет взять из него только одно подключение на время обработки запроса. Остальные соединения простаивают.

import time
from queue import LifoQueue

class TinyPool:
    def __init__(self, size):
        self.bucket = LifoQueue()
        for i in range(size):
            self.bucket.put(f"conn-{i}")

    def take(self):
        return self.bucket.get()

    def give_back(self, conn):
        self.bucket.put(conn)

pool = TinyPool(size=5)

# Это имитация синхронного воркера, который обрабатывает ровно один запрос за раз.
# Хотя `size=5`, для каждого запроса используется только одно подключение.

def handle_sync_request_once():
    conn = pool.take()
    try:
        # выполняем некоторую работу с базой данных
        time.sleep(0.1)
    finally:
        pool.give_back(conn)

for _ in range(3):
    handle_sync_request_once()

В такой конфигурации пул есть, но у воркера нет возможности одновременно использовать больше одного подключения.

Почему так

Причина проста. Полностью синхронный однопоточный процесс выполняет один запрос, затем следующий. Внутри процесса нет конкуренции, которая оправдывала бы одновременное взятие нескольких подключений из пула. Пул начинает иметь смысл, когда в одном процессе могут параллельно идти несколько запросов — то есть в двух описанных случаях: синхронные воркеры с потоками или асинхронные воркеры.

Варианты решения и что меняется

Если ваши воркеры синхронные и однопоточные, встроенный пул Django не даёт практических преимуществ по сравнению с «постоянными» подключениями. Одного постоянного соединения на процесс достаточно, потому что одновременно выполняется лишь один запрос. Но если у вас несколько потоков на процесс или асинхронный сервер, встроенный пул подходит: каждый процесс создаёт и использует свой пул, и этот пул обслуживает несколько параллельных запросов внутри процесса. Нужен общий пул для всех процессов — используйте PGBouncer, он именно для этого и существует как отдельный процесс.

Уточнённый пример: повторное использование одного постоянного подключения для синхронного воркера

Для однопоточного синхронного воркера идею постоянного подключения можно смоделировать одним переиспользуемым «дескриптором», что наглядно показывает, почему многосвязный пул в таком режиме не нужен.

import time

_singleton = {"conn": None}

def get_persistent_handle():
    if _singleton["conn"] is None:
        _singleton["conn"] = "conn-0"
    return _singleton["conn"]

def handle_sync_request_with_persistence():
    conn = get_persistent_handle()
    # выполняем работу с базой, используя тот же дескриптор
    time.sleep(0.1)

for _ in range(3):
    handle_sync_request_with_persistence()

Этот простой набросок отражает суть постоянных подключений в однопоточном процессе: один процесс, одно длительное соединение, один запрос за раз.

Два часто задаваемых уточнения

Во‑первых, встроенный пул psycopg3 не создаёт один пул в воркере, чтобы другие воркеры делили его между процессами. Пул локален для процесса. Запустите несколько процессов — у каждого будет свой пул. Во‑вторых, PGBouncer действительно работает как отдельный процесс и сам поддерживает свой пул. Любой ваш воркер Django может к нему подключаться.

Почему это важно

Выбор стратегии управления соединениями — это про согласование уровня параллелизма с инструментом. Если воркеру никогда не нужно больше одного подключения одновременно, пул — это лишние накладные расходы без выгоды. Если один процесс может одновременно обрабатывать много запросов через потоки или цикл событий, пул держит соединения наготове и сокращает ожидание в конкурентных участках. А если нужен общий пул на все процессы, пригодится отдельный пулер вроде PGBouncer.

Вывод

Смотрите на границы процессов и параллелизм внутри процесса. Для синхронных однопоточных воркеров достаточно постоянных подключений. Для потоковых или асинхронных воркеров уместен пул на базе psycopg3 в Django — он существует на процесс. Если нужен общий пул для всех процессов, используйте внешний пулер PGBouncer. Разница между «постоянным» процессом и заменяемым воркером не определяющий критерий; важнее то, есть ли внутри процесса конкуренция, которая действительно умеет задействовать несколько подключений одновременно.