Начнём с арифметики, которую в serverless-среде легко не замечать. Допустим, приложение запускается в шести процессах, и каждый процесс настроен держать до двадцати соединений с базой. Итого сто двадцать потенциальных клиентов PostgreSQL. Пока база живёт у управляемого провайдера за пулером, эта цифра никого не беспокоит: пул принимает много клиентских подключений и мультиплексирует их на небольшое число серверных. После переезда на собственный сервер пулера между приложением и базой, возможно, нет вовсе, и каждое из этих соединений становится отдельным серверным процессом PostgreSQL со своей памятью. Числа здесь условные, но механика именно такая.
Поэтому на вопрос «что меняется при переносе, кроме адреса базы?» честный ответ звучит так: меняется сама модель подключений. Адрес в строке подключения заменить просто. Куда сложнее заметить, что настройки, написанные под одну среду, молча переехали в другую, где работают по иным правилам.
Сколько соединений на самом деле просит приложение
Первый шаг не связан с базой. Нужно выписать каждый процесс или контейнер, который открывает соединения с PostgreSQL, и для каждого узнать верхнюю границу его пула. Не только веб-воркеры: фоновые обработчики очередей, планировщики задач, миграции при старте, административные скрипты, сервисы аналитики, которые подключаются «на минутку».
Получается простая формула: число экземпляров, умноженное на максимальный размер пула в каждом, плюс разовые клиенты. Это пиковое число, а не среднее, и именно оно определяет, что случится в худший момент: при деплое, когда старые и новые экземпляры живут одновременно, или при всплеске нагрузки, когда пулы заполняются целиком.
Особенно коварны автомасштабируемые схемы. Если платформа поднимает дополнительные экземпляры под нагрузкой, то каждый приносит свой пул, и суммарное число соединений растёт ровно тогда, когда базе тяжелее всего. В управляемой среде это сглаживал пулер. На новом сервере сглаживать нечему, пока вы сами этого не настроите.
| Источник | Экземпляров | Пул на экземпляр | Потенциально соединений |
|---|---|---|---|
| Веб-процессы | 6 | 20 | 120 |
| Обработчики очередей | 2 | 10 | 20 |
| Планировщик | 1 | 5 | 5 |
| Деплой (старые + новые веб-процессы) | +6 | 20 | +120 |
В нормальном режиме выходит 145, а в момент выката — до 265. Если на новом сервере по умолчанию разрешено около сотни соединений, то сценарий с выкатом упрётся в лимит раньше, чем вы успеете понять, что происходит. Конкретный лимит смотрите в конфигурации вашего сервера, а не в чужой статье, включая эту.
Два адреса в исходной среде и что им соответствует на новом сервере
Управляемый PostgreSQL с пулером обычно предлагает два способа подключения: прямой и через пулер. Для Neon это различие принципиально и для самого переноса: в документации Neon по импорту указано, что для pg_dump нужно использовать прямое непулированное соединение, а не endpoint через PgBouncer. Документ описывает перенос в Neon, а не обратно, но сама логика понятна: дамп — долгая сессия с собственными требованиями к состоянию соединения, и пулер, который переиспользует серверные подключения между клиентами, для неё не подходит.
Из этого следует практическое правило для переезда: выгрузку делаем с прямого адреса, а вот приложение, вероятно, всё это время ходило через pooled endpoint. И здесь скрыта ловушка. Приложение могло неосознанно опираться на особенности пула: на режим, при котором серверное соединение отдаётся клиенту только на время транзакции. Пока такой пул работал, код не замечал, что, например, настройки сессии или подготовленные выражения в нём ведут себя иначе, чем при прямом соединении. Или, наоборот, замечал и был написан с обходными путями.
На новом сервере у вас есть три разумных варианта, и выбирать между ними стоит осознанно.
- Приложение подключается к PostgreSQL напрямую. Проще всего и меньше всего движущихся частей. Подходит, если суммарное число соединений с запасом укладывается в лимит, а размеры пулов внутри приложения подобраны под него.
- Перед PostgreSQL ставится пулер на самом VPS. Имеет смысл, когда экземпляров много или они непостоянны. Но режим работы пулера надо выбирать под приложение, а не копировать из прошлой среды: режим с отдачей соединения на транзакцию накладывает ограничения на использование возможностей сессии, а режим на сессию экономит заметно меньше.
- Пулер и уменьшение пулов внутри приложения одновременно. Когда у каждого экземпляра пул небольшой, а общую очередь к базе держит внешний пулер, система ведёт себя предсказуемее всего.
Важно не переносить поведение управляемого пула на новую установку автоматически. То, что провайдер сделал для вас по умолчанию, — его конфигурация. На своём сервере её придётся либо воспроизвести осознанно, либо заменить чем-то другим, проверив приложение.
Версии, роли и расширения: проверяем до первого байта данных
Пока вы не скопировали ни одной таблицы, самое время выяснить, во что именно вы переезжаете. Дешёвая проверка до переноса избавляет от дорогой переделки после.
Версия сервера. Согласно документации pg_dump, инструмент не работает с сервером более новой major-версии, чем он сам, а восстановление в более старый сервер не гарантировано. Практический вывод: узнайте major-версию исходной базы и ставьте на VPS такую же или более новую. Утилиту pg_dump запускайте версии не ниже исходного сервера. Переезд на старшую версию — отдельное решение со своими проверками, лучше не совмещать его с переездом между средами без нужды.
Роли. Дамп одной базы не содержит глобальных объектов: роли и tablespaces в него не входят, для них предназначен pg_dumpall, о чём также сказано в документации pg_dump. Это значит, что после восстановления таблицы окажутся на месте, а владельцы объектов и выданные права могут ссылаться на роли, которых на новом сервере нет. Заранее выпишите роли, которыми пользуются приложение, миграции, отчётные скрипты и люди, и решите, как их создать: вручную с новыми паролями или выгрузкой глобальных объектов. Управляемые провайдеры нередко добавляют собственные служебные роли, переносить которые на обычный сервер бессмысленно, поэтому слепо применять выгрузку ролей не стоит: просмотрите её глазами.
Расширения. Посмотрите список установленных расширений в каждой базе и для каждого ответьте на два вопроса: доступно ли оно в пакетах вашей ОС для нужной версии PostgreSQL и совпадает ли его версия с исходной. Если расширение нужно приложению, его надо поставить до восстановления, иначе восстановление споткнётся на команде создания расширения или на объектах, которые от него зависят. Если расширение специфично для среды провайдера и приложению оно не нужно, это лучше выяснить сейчас, а не при разборе ошибок.
SELECT version();
SELECT extname, extversion FROM pg_extension ORDER BY 1;
SELECT rolname, rolcanlogin, rolsuper FROM pg_roles ORDER BY 1;Эти три запроса дают основу инвентаризации. Выполните их на исходной базе и на подготовленном сервере, сравните результаты и составьте список расхождений. Короткий список перед переездом лучше длинной переписки после.
Несколько баз — несколько прогонов
Если у проекта больше одной базы, учтите, что экспорт и восстановление выполняются для каждой базы отдельно: на это указывает и документация Neon, и сама природа pg_dump, работающего с одной базой. Составьте таблицу: база, владелец, размер, расширения, какие сервисы к ней подключены. Для каждой строки будет своё пробное восстановление и свой замер времени.
Пробное восстановление: репетиция без зрителей
Первое восстановление нужно не для переключения, а для знания. Вы узнаёте, сколько оно длится, какие ошибки выдаёт и сколько места занимает результат. По документации pg_dump, экспорт согласован, даже если с базой одновременно работают читатели и писатели, поэтому пробный дамп можно снимать с живой базы, не останавливая приложение. Используйте прямой endpoint, а не пулированный.
pg_dump -Fc --no-owner -d "$SOURCE_DIRECT_URL" -f app.dump
createdb -h 127.0.0.1 -U postgres appdb
pg_restore -d appdb --no-owner -j 2 app.dumpФормат -Fc относится к архивным: такие дампы восстанавливаются через pg_restore, тогда как обычный SQL-дамп — через psql. Архивный формат удобен тем, что позволяет выбирать объекты и распараллеливать восстановление. Флаг --no-owner в примере означает, что владельцем объектов станет та роль, под которой выполняется восстановление; подходит ли это вам, зависит от схемы ролей, и после восстановления права придётся выдать явно. Если вы предпочитаете сохранить владельцев, создайте роли заранее и уберите флаг.
Чтобы не потерять время на лишнюю возню, держите под рукой журнал: время начала и конца, размер файла дампа, список ошибок и предупреждений. Каждое предупреждение разберите: часть из них безобидна, но решать это нужно сознательно, а не по привычке.
После восстановления: статистика и сверка
Свежевосстановленная база не имеет накопленной статистики для планировщика, и первые запросы могут идти хуже, чем потом. Запустите сбор статистики по всей базе, прежде чем оценивать скорость. Затем сверьте содержимое: числа строк в крупных таблицах, максимальные значения последовательностей, наличие индексов и ограничений. Простейшая контрольная таблица из десятка основных таблиц с количеством строк в источнике и в копии ловит большинство грубых ошибок.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Прогон типовых операций приложения
Восстановленная база не доказывает, что приложение с ней справится. Поднимите копию приложения в тестовом окружении, направьте её на новую базу и выполните то, что пользователи делают чаще всего: вход, чтение основных списков, создание и изменение записей, фоновые задачи, отчёты. Отдельно проверьте всё, что использует расширения и специфические возможности.
Теперь вернёмся к соединениям. Тестовый прогон — единственное место, где можно спокойно измерить, как приложение ведёт себя с вашей схемой подключений. Соберите нагрузку, близкую к реальной, и наблюдайте не только время ответа, но и:
- число активных соединений в самой базе;
- сколько запросов ждёт свободного соединения в пуле приложения;
- как ведут себя долгие транзакции, которые удерживают соединение;
- что происходит при одновременном перезапуске всех экземпляров.
Последний пункт полезнее, чем кажется. Перезапуск сразу всех процессов даёт скачок числа соединений, и именно он показывает, достаточно ли запаса между суммой пулов и лимитом сервера. Если нет, у вас два рычага: уменьшить пулы внутри приложения или поставить пулер на VPS. Первый почти бесплатен и часто достаточен, потому что пул в двадцать соединений на процесс, который реально использует три-четыре, просто занимает ресурсы впустую.
Что даёт VPS и как он связан с числом соединений
У каждого соединения PostgreSQL свой серверный процесс, и память на них приходится делить с кэшем данных, рабочей памятью запросов и самой операционной системой. Поэтому выбор конфигурации сервера нужно соотносить не только с размером данных, но и с тем, сколько одновременных клиентов вы собираетесь обслуживать и какие запросы они выполняют. Тарифы VPS MAATRIX различаются по числу vCPU, объёму RAM и NVMe: от Ba с 1 vCPU, 2 ГБ и 40 ГБ до Djed с 16 vCPU, 32 ГБ и 320 ГБ. Эти параметры не обещают определённую ёмкость, они только отправная точка: сколько соединений и какую нагрузку выдержит ваша база на конкретном тарифе, покажет собственный замер. Root-доступ позволяет настроить PostgreSQL, пулер и системные параметры под своё приложение, а NVMe-диск полезен при восстановлении и записи.
Отдельно подумайте, где будет жить приложение. Если оно остаётся на другой площадке, к задержке добавится сетевой путь между приложением и базой, и каждое новое соединение станет дороже, потому что установка соединения требует нескольких сетевых обменов. Пул постоянных соединений и пулер рядом с базой в таком случае приобретают особый смысл. Если приложение переезжает на тот же VPS, сетевая часть почти исчезает, но процессы приложения и базы начинают делить одни ресурсы, и это нужно учитывать при оценке памяти.
Как устроить короткое прекращение записей
Пробный дамп можно снимать на живой базе, но для финального переноса нужна точка, после которой в источник ничего не пишется. Иначе данные, записанные между снятием дампа и переключением, потеряются. Поэтому план строится вокруг короткого окна.
Порядок действий примерно такой:
- Заранее уменьшите время жизни DNS-записи, если приложение подключается по имени, или подготовьте способ быстро поменять строку подключения в конфигурации.
- Переведите приложение в режим без записи: страница обслуживания или режим только чтения, остановка фоновых обработчиков и планировщика, которые пишут в базу.
- Убедитесь, что активных записывающих соединений не осталось: посмотрите на список сессий в источнике.
- Снимите финальный дамп с прямого endpoint и восстановите его на сервере. Время этого шага вы уже знаете по репетициям, и именно оно определяет длительность окна.
- Сверьте контрольные числа, выдайте права, обновите статистику.
- Переключите приложение на новую базу с новыми, пересчитанными размерами пулов и снимите режим обслуживания.
Если репетиция показала, что окно получается неприемлемо длинным, это сигнал не к спешке, а к пересмотру подхода. Например, к разделению данных или к другим способам непрерывной репликации. Они выходят за рамки этой статьи, а общие принципы переноса между серверами разобраны в материале про миграцию базы данных между серверами.
Условие отката
Заранее решите, что считается провалом и до какого момента назад можно вернуться без потерь. Пока источник остаётся доступным и после переключения в новую базу ещё не записано ничего нового, вернуть прежнюю строку подключения несложно. Как только новая база приняла записи, откат означает либо потерю этих записей, либо их ручной перенос. Поэтому дайте себе короткий период наблюдения сразу после переключения, прежде чем вы удалите старую среду, и не делайте необратимых шагов в первые минуты.
Размеры пулов на новом месте
Теперь можно собрать число, ради которого всё затевалось. Идея проста: сумма всех клиентских пулов и разовых подключений не должна подходить к лимиту сервера, а запас на административные сессии, мониторинг и резервное копирование надо оставить отдельно. Если приложение подключается через пулер, то считать нужно по-другому: лимит клиентских подключений к пулеру может быть больше, а число серверных подключений пулера к PostgreSQL ограничивается небольшим числом, подобранным под ядра и характер запросов.
Вернёмся к нашему условному примеру со 145 потенциальными соединениями. Есть несколько путей. Можно уменьшить пул веб-процесса с двадцати до пяти: если в тесте было видно, что одновременно занято три-четыре соединения, потери производительности не будет, а сумма упадёт до 30 для веба и 55 в целом, а во время выката до 85. Можно поставить пулер и позволить ему держать сравнительно небольшое число серверных соединений, а приложению оставить прежние пулы, но тогда придётся проверить, не зависит ли код от особенностей сессии, и выбрать режим пулера соответственно. И то и другое лучше, чем просто повысить лимит соединений на сервере.
Почему именно повышение лимита — худший первый ход? Потому что оно лечит симптом: ошибку отказа в соединении. Каждое дополнительное соединение — ещё один процесс с собственной памятью, и при настоящей нагрузке десятки одновременно работающих запросов конкурируют за ядра и диск. Больше соединений не означает больше выполненной работы: после какого-то порога запросы просто дольше ждут друг друга, а сервер начинает чаще обращаться к диску из-за нехватки памяти под кэш. Лимит соединений — защитный предохранитель, а не показатель мощности.
Первые дни после запуска: что наблюдать
После переключения полезно смотреть на несколько показателей, и у каждого есть смысл.
| Наблюдение | Что может означать | Разумный первый шаг |
|---|---|---|
| Соединения близки к лимиту сервера | Сумма пулов слишком велика или соединения не возвращаются | Пересчитать пулы, найти утечки соединений в коде |
| Запросы ждут свободного соединения в пуле приложения | Пул мал или транзакции держат соединение слишком долго | Найти долгие транзакции, затем решать, нужен ли больший пул |
| Много сессий в состоянии «idle in transaction» | Код открывает транзакцию и не завершает её | Исправить код, задать разумные тайм-ауты |
| Растёт задержка при загрузке процессора | Одновременно выполняется больше запросов, чем есть ядер | Ограничить параллелизм, оптимизировать запросы |
Обратите внимание, что в этой таблице нигде нет шага «поднять max_connections». Это не запрет, а порядок: сначала понять, кто занимает соединения и зачем, и только потом менять лимит, если запаса действительно не хватает и памяти достаточно.
Особого внимания заслуживает ожидание. Здоровая очередь к ограниченному ресурсу — это нормально: если запрос подождал несколько миллисекунд в пуле, никакой беды нет. Беда начинается, когда ожидание не ограничено по времени, и запросы накапливаются, пока не рухнет всё сразу. Задайте приложению тайм-аут ожидания соединения: пусть оно либо дождётся в разумный срок, либо вернёт понятную ошибку, которую можно обработать, повторить или показать пользователю. Тайм-ауты на стороне базы на выполнение запроса и на простой в транзакции тоже работают как предохранители, которые не дают одной зависшей операции держать ресурс вечно.
Что остаётся за кадром у самого переноса
Несколько вещей легко забыть, потому что они не видны в дампе. Резервное копирование и мониторинг на собственном сервере теперь ваши заботы: у управляемого провайдера они были частью сервиса, а здесь их надо настроить и проверить восстановлением. Не рассчитывайте, что они появятся сами. Сетевой доступ: решите, кто может подключаться к порту PostgreSQL, и ограничьте его адресами приложения, не открывая базу всему интернету. Шифрование соединения и пароли новых ролей тоже ваше дело. Наконец, обновления PostgreSQL и операционной системы теперь выполняются по вашему графику.
Всё это не повод отказываться от переезда, а список задач, которые раньше решал провайдер. Хорошая новость в том, что на VPS с root-доступом вы можете настроить их под своё приложение, а не принимать общий вариант. А пользователям приложения всё равно, какой сервер стоит за ним: им важно, чтобы запросы отвечали быстро и предсказуемо.
Подключение, которое умеет ждать и умеет отказать
Если свести задачу к одной картине, то выглядит она так. В прежней среде между приложением и базой стоял буфер, который гасил всплески и скрывал от кода, сколько соединений на самом деле открыто. После переезда буфер либо нужно построить самому, либо отказаться от него, подогнав пулы приложения под реальные возможности сервера. Работающая схема отличается не количеством соединений, а их предсказуемостью: каждый экземпляр знает свой предел, сумма пределов известна и помещается в лимит с запасом, ожидание ограничено по времени, а при превышении запрос получает быстрый и понятный отказ, а не растягивает очередь до остановки всей базы.
В такой схеме новый запрос, пришедший в момент пика, либо немного постоит в очереди, либо услышит вежливое «сейчас занято». Остальные клиенты при этом продолжают получать ответы, база не уходит в своппинг, а вы видите на графиках понятную картину и можете спокойно решать, растить ли сервер, оптимизировать запросы или менять размеры пулов. Именно это и отличает перенос, при котором переехал только адрес, от переноса, при котором переехала и модель подключений.