Postgres №39
Поговорим про пулы и проблемы соединений.
При всех достоинствах Постгреса нужно знать его недостатки. Один из них следующий: Постгрес плохо справляется с большим числом подключений. Это следует из того, как устроена его серверная часть.
При запуске сервера запускается мастер-процесс. Когда к нему кто-то
подключается, процесс клонирует себя системным вызовом fork. Получаются два
процесса: мастер занимается общими задачами, а второй — слушает и отвечает на
сообщения клиента. Когда подключается кто-то еще, мастер снова клонирует себя и
так далее.
Для обмена информацией служит общий участок памяти — shared memory. Такой участок можно создать в Unix-подобных системах. Если один процесс что-то изменит в этой памяти, друге получат изменения мгновенно.
Разработчики давно обсуждают как уйти от модели “новый процесс на соединение”. Предлагаются нативные треды, но когда они будут — я не знаю.
Нужно понимать, что системный вызов fork — очень дорогой. Он копирует всю
информацию о процессе, а именно: регистры процессора, стек, номера потоков и
открытых файлов. Поэтому каждое новое соединение — это вызов fork и ожидание,
пока операционная система клонирует процесс.
Даже если предположить, что вместо fork Постгрес использует треды, помните: запуск нового потока — это тоже системный вызов и конкуренция за время планировщика. А кроме того, частые сетевые подключения — это тоже системные вызовы.
Настройка, которая определяет максимальное число соединений, называется
max_connections. Увидеть ее можно командой SHOW:
SHOW max_connections;
У себя я вижу число 100. Это весьма адекватная цифра: число соединений не должно превышать нескольких десятков и уж тем более — сотен. Ситуация, когда вам не хватает 300 соединений, говорит об одном из двух. Либо у вас биллинг федерального значения и супер-пупер хайлоад. Либо приложение неправильно работает с соединениями: не закрывает их, теряет, удерживает без всякой нужды. Гораздо вероятней второе, а не первое.
Соединениями управляют на двух уровнях: сетевом (прокси) и в приложении. Во втором случае это пул соединений. Это объект, который имитирует обычное соединение, но на самом деле открывает их несколько. Когда мы выполняем запрос, пул помечает одно из них занятым и передает сообщения через него. Когда мы закрываем соединение, этого не происходит — оно помечается свободным и доступно другим потокам приложения.
Самый известный пул в Джаве называется HikariCP (от слова “харакири”). Он очень
быстрый: разработчики писали, что анализировали байт-код и путем экспериментов
уменьшили его минимума. В пуле нет блокирующих операций вроде synchronized:
все сделано через CAS-примитивы. Написаны свои версии классов List и подобных,
которые не проверяют выход за границы — это гарантируется в коде.
У HikariCP множество характеристик и шаблонов поведения. Например, сколько соединений открывать на старте; до какого предела расти; если предел достигнут, а кому-то нужно соединение, то сколько ждать; как долго клиент может удерживать соединение; как определять потерянное соединение, которое взяли и не вернули; как проверять, что соединение живое и так далее.
Хорошая практика в том, чтобы использовать пул всегда независимо от типа приложения. Этим вы гарантируете минимальную гигиену в работе с базой. Даже если у вас одно соединение, пул все равно необходим: он откроет его и будет держать открытым. Приложение не будет без конца стучаться в сокет, проходить авторизацию и клонировать серверный процесс.
Последнее — частая ошибка ребят, пришедших их PHP. В их мире не принято следить за ресурсами. Открыл файл, соединение, сокет и работай с ними, а закрывать на надо. Зачем, если скрипт отработает, и все закроется само? В системах, отличных от скриптовых, состояние не исчезает. Если открыли соединение, его нужно закрыть, но при этом следить, чтобы “открыл-закрыл” это не повторялось постоянно.
Пул решает эту проблему — берет на себя заботу об открытии и закрытии. Если одного соединения перестанет хватать, это дело настройки: увеличьте предел с 1 до 2. Стратегия может быть разной, например не открывать при запуске ничего, а делать это по требованию. Либо наоборот — открыть столько, сколько указано в лимите, чтобы каждый запрос выполнятся с уже открытым соединением.
Сколько выделить соединений пулу — вопрос сложный и решается банальным подбором. Начните с адекватной цифры вроде четырех и посмотрите на результат: как быстро приложение отвечает на запросы в многопоточном режиме. Собирайте метрики пула через JMX или обычные логи. Особенна важна метрика ожидания: как долго очередной поток ждет, пока пул выдаст ему соединение.
Другой важный показатель — сколько в пуле свободных соединений. Если их много, очевидно вы задрали лимит. Скорее всего, в этом соединении нуждается другой экземпляр приложения.
Поэтому — гадать не нужно, все сводится к метрикам.
Это была первая часть темы о пулах и соединениях. В следующих выпусках продолжим.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter