PgBouncer стоит между клиентами и PostgreSQL и создаёт у обеих сторон неверную картину мира. Клиент думает, что держит прямое соединение с сервером баз данных, а сам сервер видит одно постоянное соединение, за которым на стороне пулера стоят десятки разных клиентов. Из этого обмана вырастает всё остальное: и то, как пулер выдаёт серверные соединения, и то, какие ошибки вылезают в transaction-режиме, и то, почему один процесс упирается в одно ядро. Разберём механику по шагам.
Два сокета и одна связка
PgBouncer заводит отдельный объект PgSocketструктура, описывающая одно сетевое соединение вместе с его буфером и состоянием для клиента и для сервера. Клиентские сокеты берутся из кэша client_cache, где конструктор инициализирует буфер клиентским протоколом и присваивает сокету уникальный номер; серверные — из server_cache, где буфер инициализируется серверным протоколом и номер выдаётся из того же счётчика. То есть клиент и сервер — это два разных сокета с разными протоколами обработки, и «обман» начинается уже здесь.
Клиентский протокол направляет стартовые пакеты в handle_client_startup, а рабочие — в handle_client_work. Серверный протокол делает то же самое: пока сервер в состоянии SV_LOGINсостояние серверного сокета, в котором идёт установление соединения и логин на сервере PostgreSQL, пакеты идут в handle_server_startup, иначе — в handle_server_work. Связь между парой держится в поле link: сервер знает своего клиента через server->link, и когда отправка на сервер срывается, именно этот клиент отключается с сообщением об неожиданном конце файла.
Базы данных представлены объектами PgDatabase, которые создаются при добавлении базы и хранятся в общем списке. peer-базабаза, зарегистрированная для пересылки запросов на отмену другому процессу PgBouncer хранится отдельно с числовым peer_id, и поиск по нему идёт перебором списка. Пулы создаются для пары база-пользователь, а для пересылки отмен — отдельные peer-пулыпул без пользователя, через который запрос на отмену уходит другому процессу PgBouncer, у которых нет пользователя. Учётные данные ищутся через глобальное дерево пользователей.
Пулы и их порядок
Пул связывает базу с учётными данными пользователя. При создании он получает память из своего кэша, инициализирует списки клиентов и серверов и вставляется в общий список в правильную позицию — не в конец, а именно в отсортированное место. Порядок задают два компаратора.
Обычные пулы упорядочены сначала по имени базы, а при равенстве — по имени учётных данных, причём пул без учётных данных считается «больше». Peer-пулы упорядочены по peer_id базы. Комментарии в коде объясняют смысл обоих порядков одинаково: чтобы статистика собиралась быстрее.
Поиск пула идёт перебором списка пулов пользователя с сравнением базы; если совпадения нет, создаётся новый. Для peer-пулов логика проще: пул хранится прямо в объекте базы, и если его там ещё нет — создаётся.
Логин клиента: от стартового пакета до привязки к пулу
Логин начинается с handle_client_startup. Первым делом отбрасываются неполные пакеты: если пакет не влезает в буфер, ставится флаг ожидания полного пакета и запрашивается догрузка, а обработка возвращает отказ, чтобы повторить позже.
Дальше — серия проверок. Запрос TLS внутри уже установленного TLS отключает клиента. Запрос TLS принимается, если режим приёма TLS не отключён и соединение не через unix-сокет; иначе клиенту отвечают отказом. Если режим требует TLS, а его нет и соединение не unix — клиент отключается. Повторный стартовый пакет при уже установленном пуле (и когда клиент не ждёт ответа на запрос аутентификации) тоже отключает клиента.
Затем разбирается содержимое стартового пакета. Первым проходом ищется параметр replication: значение database даёт логическую репликацию, а булево значение — физическую или её отсутствие. Потом разбираются имя базы, пользователь, опции, имя приложения; параметры с префиксом _pq_. игнорируются, остальные либо кладутся в кэш переменных, либо, если они в списке игнорируемых, отбрасываются, либо клиент отключается как приславший неподдерживаемый параметр.
Проверяется наличие имени пользователя, имя базы по умолчанию совпадает с именем пользователя, и число активных клиентских объектов сравнивается с пределом max_client_conn — кроме подключения к служебной базе pgbouncer. Если протокол не согласован, а клиент прислал неподдерживаемую версию или расширения, ему отправляется предложение согласовать протокол.
Затем set_poolфункция, которая по имени базы и пользователю находит или регистрирует базу данных, подбирает учётные данные под выбранный тип аутентификации и проверяет лимиты соединений базы и пользователя, а для служебной базы сразу передаёт управление завершению установки пула. Она находит или регистрирует базу, для служебной базы сразу запускает предлогин и завершение установки пула, проверяет длину имени пользователя, выбирает учётные данные в зависимости от типа аутентификации и проверяет лимиты соединений базы и пользователя.
Клиент считается привязанным к пулу в момент, когда finish_set_pool успешно вызывает get_pool и присваивает результат в client->pool. Перед этим выставляется флаг получения стартового сообщения. Если аутентификация фиктивная или база фиктивная, пул не берётся. Для служебного пула вызывается постлогин, для собственного пользователя — завершение логина; иначе выбирается способ аутентификации по конфигурации и правилам доступа, и клиенту отправляется запрос аутентификации, либо он входит по сертификату, либо как unix-пользователь, либо логин сразу завершается.
Лимиты соединений и что видит клиент
Учёт клиента в счётчиках базы и пользователя ленивый: при первом вызове проверки базы флаг учёта ставится и счётчик базы увеличивается один раз; аналогично для пользователя. Это значит, что один и тот же клиент не может накрутить счётчик повторными проверками.
Лимит базы берётся из настройки максимального числа клиентских соединений базы. Если он не положителен, проверка проходит. Иначе при превышении клиент отключается с сообщением про превышение лимита соединений базы. Администратор базы, входящий в список администраторов, пропускается.
Лимит пользователя работает так же: если учётных данных или глобального пользователя нет, проверка проходит; ноль означает отсутствие ограничения; при превышении клиент отключается с сообщением про превышение лимита соединений пользователя.
При отключении счётчики базы и пользователя уменьшаются — но только если клиент в них учитывался. Функция отключения с кодом состояния вызывается из обычного отключения с пустым кодом состояния, то есть при превышении лимитов клиент не получает конкретный SQLSTATE.
Аутентификация
База для аутентификации выбирается в порядке: сначала имя, заданное у базы клиента, потом глобальная настройка, потом сама база клиента. Если выбранная база не найдена, отключена или является служебной, клиент отключается.
Проверка пароля зависит от типа аутентификации. Для простого пароля сравнивается текст, либо вычисляется хеш от имени и пароля, либо проверяется SCRAM-пароль. Для MD5 клиент присылает хеш от хеша пароля с именем и солью, и он сравнивается с сохранённым значением.
Запрос аутентификации тоже зависит от типа: для MD5 генерируется четырёхбайтовая соль и отправляется запрос с ней; для простого пароля, LDAP и PAM — запрос пароля; для SCRAM-SHA-256 — запрос SASL с именем механизма.
Если включён режим запроса пароля через саму базу, пулер берёт текст запроса из настроек базы или глобальной настройки, получает пул для базы аутентификации и отправляет запрос с именем пользователя, а клиент ждёт ответа. Ответ обрабатывается строго: ожидается ровно две колонки, читаются имя и пароль, а если пароль пуст, подставляется строка длиной три, которая заведомо ни с чем не совпадёт. При успехе учётные данные помечаются как фиктивные и установка пула завершается.
Сертификатный вход требует TLS и предоставленного клиентского сертификата; при наличии карты соответствий имена сверяются с сертификатом, иначе требуется совпадение имени сертификата с именем учётных данных. Вход как unix-пользователь требует unix-сокет и проверяет имя.
SCRAM-ключи кэшируются в учётных данных: хранятся соль и признак закэшированного верификатора. Очистка освобождает соль и сбрасывает признак, а обход дерева пользователей вызывает очистку для каждого узла.
Динамические учётные данные и их инвалидация
Динамические учётные данные ищутся в дереве конкретной базы, а не в глобальном дереве. Если записи нет, создаётся новая: копируется имя, находится или создаётся глобальный пользователь, узел вставляется в дерево базы. Затем всегда копируется пароль и ставится признак динамического пароля. PAM-учётные данные хранятся в отдельном глобальном дереве и пароль не записывают. Принудительные учётные данные хранятся отдельным объектом у базы.
При перезагрузке конфигурации инвалидация идёт по-разному. Изменение базы обходит список пулов и для пулов этой базы помечает серверы как требующие закрытия. Изменение авто-баз перерегистрирует их и помечает пулы авто-баз. Изменение адреса хоста помечает только те серверы, чей удалённый адрес совпадает, и только в пулах с совпадающим хостом.
Выдача серверного соединения
find_server сначала проверяет, нет ли у клиента уже связанного сервера — если есть, сразу успех. Дальше, если пул или база на паузе, сервер не берётся. Для репликационного клиента без запроса аутентификации запускается новое соединение, а сервер остаётся пустым. В остальных случаях берётся первый сокет из списка простаивающих серверов; сервер, помеченный как требующий закрытия, отключается как устаревшее соединение, а неготовый — как «грязный» простаивающий сервер.
Если простаивающего сервера нет, вызывается быстрая проверка отказа, которая может отклонить клиента. Найденный сервер связывается с клиентом и переводится в активное состояние. Если нужно применить изменения переменных, сервер помечается как устанавливающий переменные и неготовый, а клиент ставится на паузу. Если сервер не найден, клиент ставится на паузу и попадает в список ожидающих.
При освобождении сервера первый клиент из списка ожидающих активируется — но только если он не репликационный или для него отправляется запрос аутентификации. Отдельная функция поддельных ответов ожидающего клиента не выбирает: она берёт уже связанного с сервером клиента и для запроса разбора кладёт в очередь подтверждение разбора, для запроса закрытия — подтверждение закрытия, а на неизвестный тип запроса завершается аварийно.
Быстрый отказ вместо очереди
Функция быстрой проверки отказа решает, можно ли клиенту ждать сервер. Она пропускает клиента, если пула нет (фиктивная аутентификация), если последний логин в пул был успешным, или если в пуле есть доступные серверы — то есть число серверов пула минус число новых серверов не равно нулю.
Если ни одно условие не выполнено, клиент немедленно отключается с сообщением, включающим сохранённый текст последней ошибки логина к серверу. После этого пулер пытается запустить новое серверное соединение и возвращает отказ. Смысл в том, чтобы не копить очередь клиентов, когда рабочих серверных соединений нет вовсе.
Режимы пулинга и цена обмана
Режим пула определяется по цепочке: сначала режим глобального пользователянастройка pool_mode, заданная для пользователя целиком, а не для отдельной пары база-пользователь, при наследовании — режим базы, при наследовании и там — глобальная настройка. Наследование здесь означает, что на своём уровне режим не задан явно и оставлен значением POOL_INHERIT — «взять у следующего уровня цепочки»; спуск продолжается, пока не найдётся уровень с конкретным режимом. Обёртка, которой следует пользоваться, принудительно возвращает session-режим для репликационных соединений, иначе делегирует в эту цепочку.
Режимы означают разное время удержания сервера. В session-режиме сервер возвращается в пул после отключения клиента, и это режим по умолчанию. В transaction-режиме — после завершения транзакции. В statement-режиме — после завершения запроса, и транзакции из нескольких операторов в нём запрещены.
В transaction-режиме пулер не парсит SQL и знает о состоянии сессии только то, что сообщает протокол. При обработке готовности к запросу статус транзакции намеренно игнорируется. Именно из этого вырастает классическая ошибка «cached plan must not change result type»: один и тот же текст подготовленного запроса с разными типами аргументов или результата на переподготовленном сервере не совпадает. Обойти её можно, не переиспользуя один и тот же текст запроса с разными типами, а при миграции со сменой схемы — выполнить переподключение из админ-консоли, чтобы заставить запрос переподготовиться.
Размер пула и работа janitor
Целевой размер пула выбирается по цепочке: размер пользователя, если задан, иначе размер базы, иначе глобальное значение по умолчанию. Резервный размер — так же по цепочке: это дополнительные соединения сверх целевого размера, которые разрешено открыть пулу, когда клиент ждёт обслуживания дольше заданного времени. Минимальный размер берётся у базы, если он не отрицательный, иначе глобальное значение. Время жизни сервера — у базы, если оно не ноль, иначе глобальное.
Фоновый janitorслужебный цикл PgBouncer, который периодически обходит пулы и серверы и приводит их состояние в порядок в каждом цикле обходит список пулов: для неадминистративных пулов при отсутствии паузы активирует их, при паузе — приостанавливает, а при ожидании закрытия базы обрабатывает закрытие.
Проверка размера пула считает текущее число соединённых серверов и избыток как разницу с суммой целевого и резервного размеров. Если целевой размер положителен, лишние серверы отключаются из списков используемых или простаивающих с сообщением о слишком большом числе серверов, пока избыток положителен. Затем, если текущее число меньше минимального и меньше целевого, пауза не активна и есть клиенты или принудительные учётные данные, запускается новое соединение — так поддерживается минимальный размер пула.
Проверка неиспользуемых серверов отключает серверы по нескольким причинам: помеченные как требующие закрытия, «грязные» в простаивающем или используемом состоянии, простоявшие дольше таймаута простоя (с оговоркой про минимальный размер пула), исчерпавшие время жизни, а также при паузе. Если включена проверка простаивающих серверов и задан проверочный запрос, сервер по истечении задержки переводится в используемое состояние, что и запускает проверку.
Время жизни проверяется отдельно: пока возраст сервера меньше времени жизни, закрытие не происходит. Когда возраст достигает порога, вычисляется интервал между такими закрытиями как отношение времени жизни к целевому размеру пула, и сервер закрывается только если с прошлого закрытия прошло не меньше этого интервала — так массовые одновременные переподключения размазываются во времени.
Освобождение сервера
При освобождении сервера сначала проверяется время жизни: если сервер не в фазе логина и исчерпал его, он немедленно закрывается, а время последнего такого закрытия обновляется. Если у сервера есть незавершённые запросы, он закрывается с сообщением о том, что клиент отключился с запросами в работе — иначе ответы могли бы попасть другому клиенту. Помеченный как требующий закрытия сервер тоже закрывается.
Новое состояние освобождаемого сервера выбирается по настройкам: если задан запрос сброса и он применяется всегда либо режим пула session, сервер помечается как проверенный; иначе если задержка проверки равна нулю и задан проверочный запрос, сервер помечается как используемый; иначе — как простаивающий. Если у сервера есть клиенты, ожидающие отмены, он переходит в состояние отмены. Для репликации либо активируется связанный клиент, либо сервер отключается.
При переходе в простаивающее состояние немедленно вызывается переиспользование: берётся первый клиент из очереди ожидания и активируется. Комментарий в коде объясняет это «справедливым шансом» — клиенты обслуживаются в порядке очереди ожидания, а не в порядке освобождения серверов.
Разрыв связки обнуляет ссылки у клиента и сервера, а затем отключает клиента с переданной причиной: для активного или ожидающего клиента — с этой причиной, для клиента в состоянии отмены — с сообщением об успешной отправке запроса на отмену, в остальных случаях — с сообщением об ошибке конфигурации.
Незавершённые запросы и поддельные ответы
Каждый запрос, уходящий на сервер, регистрируется в очереди незавершённых запросов с типом и действием, чтобы позже сопоставить ответ сервера с исходным запросом клиента. Для поддельного действия при пустой очереди ответ отправляется сразу.
Снятие запроса из очереди происходит только если его тип входит в переданный список; при снятии освобождается связанный подготовленный запрос и возвращается признак пропуска. Отправка накопившихся поддельных ответов идёт по началу очереди и ставит признак дополнительной очереди после пакета; при ошибке выделения памяти отключаются и клиент, и сервер.
Если сервер отвалился при наличии незавершённых запросов, освободить его нельзя — он отключается с сообщением о запросах в работе, а разрыв связки отключает связанного клиента с той же причиной.
Отмена запросов и peering
У запроса на отмену есть ключ, в младших битах последнего байта которого хранится время жизни. Если оно равно нулю, запрос отбрасывается как исчерпавший время жизни. Перед пересылкой время жизни уменьшается вычитанием единицы из последнего байта — именно потому, что оно занимает младшие биты.
При включённом peering из ключа извлекается идентификатор процессаномер процесса PgBouncer в группе peered-процессов, задаваемый настройкой peer_id. Если идентификатор не совпадает с локальным, запрос уходит на пересылку: по идентификатору находится peer-база, берётся её пул, запрос привязывается к пулу и переводится в ожидание отмены, после чего открывается новое соединение. Если идентификатор совпадает с локальным, последние два бита ключа выставляются в единицу — так сравнение с сохранёнными ключами клиентов совпадёт, потому что у сохранённых ключей эти биты всегда равны единице.
При пересылке на peer отмена отправляется с ключом самого запроса, а не с ключом найденного клиента. Смысл всего механизма: запрос на отмену может прийти на другой процесс PgBouncer, и его нужно перенаправить туда, откуда он изначально пришёл.
Таймауты и отказы
Таймаут логина клиента проверяется в фоновой задаче: если он не положителен, проверка пропускается; иначе клиенты из списка логинящихся отключаются, когда их возраст превышает порог. Отдельно, если сервер не готов прислать приветствие, клиенты, ожидающие приветствия, отключаются с сообщением про таймаут логина при недоступном сервере.
Таймаут ожидания запроса обрабатывается для клиентов в очереди ожидания: возраст считается от начала запроса, выбирается эффективный таймаут с учётом настроек базы и пользователя, и при превышении клиент отключается.
При неудачном логине к серверу пулер разбирает пакет ошибки и логирует предупреждение. Если код состояния не равен коду «сейчас нельзя подключиться», вызывается отключение всех ожидающих клиентов с тем же кодом и сообщением — но только если рабочих серверов нет или приветствие ещё не получено.
DNS и переключение хостов
При сбое DNS серверное соединение разрывается с сообщением о неудачном разрешении имени. При успешном ответе подставляется порт из настроек базы и устанавливается соединение.
Если в строке подключения перечислено несколько хостов, выбирается один по индексу, вычисленному как остаток от деления счётчика на число хостов. В режиме round-robin счётчик увеличивается после выбора, то есть следующая попытка возьмёт следующий хост. В режиме disable счётчик увеличивается до выбора, если предыдущее соединение не удалось, — так переход к следующему хосту происходит только после сбоя. Если хост не unix-сокет и не разбирается как адрес напрямую, запускается DNS-разрешение.
Служебные представления показывают состояние DNS: список хостов выводит имя, время жизни записи и адреса, причём время жизни показывается как ноль, если оно уже истекло, иначе как остаток в секундах. Список зон выводит имя зоны, серийный номер и счётчик. Зоны собираются из имён хостов — всё после первой точки, — и периодически проверяется смена серийного номера: при изменениях имена переразрешаются, а при смене адреса соединения инвалидируются.
Администрирование и наблюдаемость
Пауза без аргумента переводит пулер в режим паузы и, если есть активные серверы, ждёт ответа. Пауза с аргументом-базой ставит признак паузы у базы и тоже ждёт, если у базы есть активные соединения.
Переподключение без аргумента проходит по всем пулам и помечает их базы как изменённые, с аргументом — конкретную базу. Пометка пула проставляет серверам признак необходимости закрытия и отключает серверы, ещё находящиеся в фазе логина.
Отключение базы ставит признак отключения и сразу отвечает. Команда завершения клиента ищет сокет по номеру во всех списках — активных и ожидающих клиентов, активных и ожидающих отмен, а также в peer-пулах — и отключает найденный с сообщением о принудительном отключении администратором. Ожидание закрытия ставит признак у базы и ждёт, если у базы есть активные соединения.
Что показывают представления: активные клиенты — это те, что связаны с сервером или простаивают без запросов; ожидающие клиенты — те, что отправили запрос, но ещё не получили сервер; активные серверы — связанные с клиентом; простаивающие серверы — неиспользуемые и готовые к запросам.
Связь параметров конфигурации
Теоретический максимум числа файловых дескрипторов в документации записан двумя разными способами: как сумма предельного числа клиентских соединений и произведения максимального размера пула на число баз и пользователей, и в другом месте — без множителя пользователей. Предельное число клиентских соединений — это максимум клиентов, принимаемых пулером; размер пула — максимум серверных соединений на пару пользователь-база, переопределяемый в секциях баз и пользователей.
Лимит соединений на базу ограничивает серверные соединения независимо от пользователя, лимит на пользователя — независимо от базы. Важное следствие: при достижении лимита закрытие клиентского соединения в одном пуле не освобождает серверное соединение немедленно — оно закроется по таймауту простоя, и только тогда для ожидающего пула откроется новое.
Резервный размер задаёт, сколько дополнительных соединений разрешено пулу, а таймаут резерва — через какое время необслуженный клиент получит соединение из резерва. Время жизни сервера настраивается для каждой базы отдельно, иначе берётся значение уровня экземпляра.
Предупреждение документации о том, что теоретический максимум не должен достигаться, если только кто-то намеренно не создаст специальную нагрузку, означает: такая пиковая нагрузка сама не возникает, но число файловых дескрипторов всё равно нужно задавать с запасом.
Прочие параметры и их компромиссы
Список игнорируемых стартовых параметров позволяет перечислить параметры, которые пулер не отслеживает, считая, что их обрабатывает администратор; всё остальное, кроме отслеживаемых, вызывает ошибку.
Отключение простого протокола запросов убирает возможность нескольких запросов в одном пакете, а вместе с ней — целый класс SQL-инъекций. Компромисс жёсткий: продолжат работать только клиенты, использующие исключительно расширенный протокол запросов.
Размер внутреннего буфера пакетов влияет на размер TCP-пакетов и потребление памяти; поскольку реальные пакеты клиентской библиотеки могут быть больше, увеличивать его не требуется.
Опция отложенного принятия соединений поддерживается только на Linux, при включении её значение жёстко задано — 45 секунд, по умолчанию она включена на Linux и выключена иначе.
Режим TLS для клиентов управляет тем, игнорируется ли TLS, используется по запросу клиента, обязателен для клиента или требует валидного клиентского сертификата. Режим TLS к серверам по умолчанию запрашивает TLS первым, а при отказе устанавливает соединение по обычному TCP без проверки сертификата.
Как сравнивали и что получилось
Сравнение построено на разборе архитектуры пяти пулеров: PgBouncer, Pgpool-II, RDS Proxy, PgCat и PgDog. Сопоставляются модели процессов, парсинг SQL, обработка отказа и учётные данные, а также документированные ограничения.
Модель процессов. PgBouncer — один процесс с одним потоком на событийном цикле libevent, без порождения процесса на клиента. Отсюда и способность держать десятки тысяч в основном простаивающих клиентов, и потолок в одно ядро CPU. Санкционированный обход — запуск нескольких процессов на одном порту через so_reuseport с секцией [peers] (начиная с 1.19.0), чтобы запрос на отмену, пришедший не в тот процесс, был перенаправлен в правильный. У каждого процесса свои пулы, поэтому перед установкой default_pool_size нужно умножать.
Pgpool-II использует модель PostgreSQL: родитель предфоркает детей, один клиент на ребёнка. Число процессов и одновременно обслуживаемых клиентов задаётся одним параметром num_init_children (по умолчанию 32). Каждый ребёнок держит приватный кэш до max_pool (по умолчанию 4) серверных соединений, подбираемых по пользователю, базе и стартовым параметрам, и кэши между детьми не разделяются.
Парсинг SQL. Pgpool-II разбирает каждый оператор (в 4.7 — парсером PostgreSQL 18), чтобы решить, можно ли отправить его на standby, но парсер вне базы не видит тела функций. Поэтому volatile-функции считаются записями, а всё более тонкое приходится перечислять вручную в write_function_list.
PgBouncer в transaction-режиме SQL не парсит и знает только то, что сообщает протокол. PostgreSQL 18 сообщает search_path, и 1.26.0 отслеживает всё, что сообщает сервер, так что на 18 эта конкретная утечка закрыта. Любой другой SET всё ещё утекает — рекомендуется SET LOCAL.
RDS Proxy пулит на уровне транзакции, но при виде того, что нельзя безопасно мультиплексировать, перестаёт пытаться и прикрепляет клиента к его серверному соединению до отключения. Для PostgreSQL в документированный список такого прикрепления входят: любой SET или set_config; SQL-уровневые PREPARE, EXECUTE, DEALLOCATE и DISCARD; временные таблицы, последовательности и представления; курсоры; LISTEN; сессионные advisory locks; вызов nextval или setval; загрузка библиотеки; и любой оператор длиннее 16 КБ.
Отказоустойчивость. PgBouncer не разделяет чтение и запись, не проверяет здоровье реплик и не выполняет переключение при отказе: запись базы может перечислять несколько хостов, и это всё. Pgpool-II, наоборот, даёт разделение чтения и записи с проверками задержки репликации, автоматическое переключение при отказе, онлайн-восстановление и watchdog с кворумом из трёх и более узлов, перемещающий виртуальный IP. Обратная сторона: pg_terminate_backend() может вызвать переключение, потому что завершённый бэкенд и мёртвый postmaster посылают одно и то же сообщение; в 4.3 добавлен failover_on_backend_shutdown, чтобы это отключить.
Что из этого следует на практике
Обман устроен симметрично, и все последствия растут из него. Клиент привязывается к пулу в момент успешного получения пула при завершении установки, но реальное серверное соединение он получает позже и не навсегда — в transaction- и statement-режимах оно возвращается в пул раньше, чем клиент отключится.
Лимиты базы и пользователя считают клиентов, а не серверные соединения, и при достижении лимита освобождение серверного соединения ждёт таймаута простоя — то есть закрытие клиента в одном пуле не даёт мгновенно открыть соединение в другом.
Быстрый отказ вместо очереди означает, что при неудачном последнем логине и отсутствии доступных серверов клиент отключается сразу, а не ждёт в очереди. Это защищает от накопления очереди, но превращает временную недоступность сервера в немедленные отказы клиентам.
В transaction-режиме пулер не парсит SQL и намеренно игнорирует статус транзакции при готовности к запросу, поэтому состояние сессии, не выраженное в протоколе, утекает между клиентами. Отсюда и ошибка про неизменность типа результата у подготовленного запроса, и рекомендация использовать SET LOCAL.
Один процесс упирается в одно ядро, и TLS с SCRAM расходуют это ядро свободно. Масштабирование идёт через несколько процессов на одном порту с секцией [peers], но у каждого процесса свои пулы — значит, перед установкой размера пула нужно умножать на число процессов.
Когда нужен только пулер, выбор — PgBouncer: он делает одну вещь, его режимы отказа документированы девятнадцать лет, и большинство управляемых сервисов PostgreSQL и Kubernetes-операторов уже умеют его запускать. Другие пулеры обещают больше, но с оговорками: RDS Proxy сдаётся при прикреплении, PgCat описывает очистку состояния как «best effort» и «неисчерпывающий список», а PgDog требует включения query_parser и `pub_sub_ch
Где смотреть в коде
- client.c: check_db_connection_count
- objects.c: reuse_on_release
- objects.c: unlink_server
- objects.c: check_fast_fail
- objects.c: clear_outstanding_requests_until
- objects.c: release_server
- client.c: sending_auth_query
- janitor.c: check_unused_servers