Перейти к содержанию

Пул соединений

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

Продолжим с PostgreSQL 17.11, Python 3.12 и Psycopg 3.3.6. Пул устанавливается отдельным пакетом psycopg_pool==3.3.3, подтверждённым официальными release notes. Снимок catalog-db-advanced/lesson-22 находится в архиве продолжения. Пакеты, программа и запросы не запускались; фактическую нагрузку мы не создавали.

Временное владение соединением

Пул не является общей транзакцией всех пользователей. Он управляет выдачей отдельных соединений. Операция берёт ресурс, выполняет ограниченную работу и освобождает его для следующего потребителя. Нельзя сохранить borrowed connection в глобальной переменной и продолжить использовать после возврата: его уже может получить другая операция.

В маленьком примере читаются два опубликованных курса фронтенда. Полный read_pool.py явно создаёт пул закрытым, открывает его перед работой и закрывает в finally:

pool = ConnectionPool(
    conninfo=os.environ['PROFESSORWEB_LAB_DSN'],
    kwargs={'autocommit': True},
    min_size=1, max_size=4, max_waiting=8, timeout=2,
    configure=configure_connection, open=False,
)
try:
    pool.open(wait=True, timeout=5)
    with pool.connection() as conn:
        rows = conn.execute(
            'SELECT id,slug,title FROM catalog.courses '
            'WHERE topic_id=%s AND status=%s ORDER BY id',
            (1, 'published'),
        ).fetchall()
    for row in rows:
        print(*row)
finally:
    pool.close()

Это основной фрагмент полного файла. configure_connection в нём проверяет учебное имя базы и задаёт application_name, не печатая DSN. Соединение настроено с autocommit для независимого чтения, поэтому служебный SELECT не оставляет обычный незавершённый транзакционный блок. Личный пароль остаётся частью вашей локальной конфигурации будущего повторения.

Ожидается HTML с ключом один и JavaScript с ключом два. Черновая производительность не проходит фильтр. Один такой вызов не обязан создать все четыре допустимых соединения: максимальный размер — граница, а не обещанный фактический счётчик. В примере нет параллельных HTTP-клиентов, поэтому нельзя вывести достигнутую производительность из числа max_size.

Разные пределы ожидания

max_size=4 ограничивает число ресурсов одного пула. max_waiting=8 ограничивает очередь ожидающих выдачи. timeout=2 задаёт предел ожидания получения соединения в выбранном договоре клиента. Эти числа являются параметрами учебной политики, не измерением времени SQL. Поведение описано в руководстве пулов Psycopg.

Ожидание ресурса отличается от выполнения запроса. Клиент может долго стоять в очереди к пулу, хотя сам SELECT короткий. И наоборот, он может быстро получить свободное соединение, затем ждать строковую блокировку. Предел выдачи соединения не превращается автоматически в statement_timeout PostgreSQL. Для каждого слоя выбирают свою понятную границу.

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

Параметр ожидания открытия пула также относится к подготовке ресурсов. open(wait=True, timeout=5) позволяет клиенту дождаться начальной готовности по выбранному пределу. Ошибка неправильного подключения не является ускорением сайта или поводом увеличить очередь. Сначала исправляют параметры учебной среды и читают причину отказа без раскрытия пароля.

Предел существует на процесс

Веб-приложение может иметь несколько процессов. Каждый создаёт свой пул. Если шесть процессов разрешают по четыре соединения, верхняя граница только этой части приложения равна двадцати четырём, а не четырём. Дополнительно работают задачи, администрирование и другие сервисы. Это обычная арифметика конфигурации, не наблюдённое число соединений нашей базы.

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

Нужно также решить, где создаётся пул относительно жизненного цикла процесса. Не следует открывать реальные соединения в родительском процессе, а затем предполагать безопасное наследование тех же ресурсов рабочими процессами. В учебном скрипте один процесс и явное открытие; будущий веб-сервер должен согласовать эту границу со своей архитектурой.

Состояние возвращаемого ресурса

В предыдущих уроках соединение содержало транзакцию и настройки сессии. При повторном использовании такое состояние не должно неожиданно перейти к следующему потребителю. Транзакционный контекст и правильный возврат важны даже тогда, когда библиотека помогает откатывать неуспешную работу. Нельзя оставлять незавершённое действие за пределами borrowed block и рассчитывать на случайного следующего пользователя.

Наш клиент использует квалифицированные имена catalog.courses и не меняет search_path при каждом чтении. Это уменьшает скрытые зависимости. Если приложению нужны пользовательские настройки сессии, оно должно явно определить их установку и сброс. Пул не отменяет договор схем, ролей и часовых поясов.

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

Возврат соединения не равен завершению намерения

Контекст выдачи соединения ограничивает время, в течение которого клиент владеет этим ресурсом. Он не должен охватывать рендеринг всей страницы, загрузку картинки из внешнего сервиса или ожидание ответа редактора. После получения необходимых строк курсор закрывается, соединение возвращается, а подготовка пользовательского ответа продолжает работать с обычными значениями памяти.

У независимого чтения в нашем примере выбран autocommit. Если следующая функция требует нескольких согласованных изменений, она создаёт явную короткую transaction внутри заимствования. Такие границы нужно видеть отдельно: получить соединение, выполнить одну предметную операцию, вернуть ресурс. Нельзя считать один checkout универсальной транзакцией на весь пользовательский запрос без анализа выполняемой работы.

Пул также не является кешем карточек. Две выдачи соединения не обещают одинаковую цену курса или тот же снимок базы. Для согласованного отчёта применяется договор изоляции, а для сокращения одинакового чтения — отдельно спроектированный кеш со своим сроком актуальности. Подмена одного другим приводит к странным ожиданиям, когда обновлённая карточка оказывается видна или не видна якобы «из-за пула».

Представим четыре занятых соединения, каждое ждёт ответ внешнего API, сохраняя открытый SQL-контекст. Пятый посетитель задержится до выполнения своего SELECT ещё до работы планировщика. Добавление индекса этот участок ожидания не меняет. Поэтому диагностика должна разделять очередь за ресурсом и запрос внутри ресурса, а исправление часто начинается с более короткой границы владения соединением.

Что наблюдать

У пула есть собственные статистические сведения о запросах ресурсов и ожидании. PostgreSQL показывает состояние соединений отдельно. Эти два слоя помогают отличить заполненную очередь от долгого SQL или незавершённой транзакции. В статье не приведены выдуманные значения статистики: они появятся только при будущей реальной работе вашей среды.

application_name позволяет узнать подготовленный учебный клиент в списке сессий. Это техническая подпись приложения, не секрет и не идентификатор конкретного читателя. Не нужно добавлять в неё пароль, полный запрос пользователя или персональные данные ради удобства диагностики.

Перед будущим ручным запуском прочитайте полный файл, отдельный requirements и README. Восстановление данных выполняется отдельно и не включено в read_pool.py. Ожидаемый итог — прежние две публичные карточки при ограниченном владении ресурсом. Теперь соединения имеют понятные пределы, время жизни и политику ожидания, вместо неограниченного открытия на каждое действие посетителя.

Оглавление курса.