Представим условный, но очень узнаваемый случай. Дамп базы снят, pg_restore отработал и не выдал ни одной красной строки, таблицы на месте, количество строк совпадает. Приложение запускается и на первой же странице поиска отвечает ошибкой: function similarity(text, unknown) does not exist. Через минуту выясняется второе: когда расширение всё-таки появилось, приложение получило permission denied for table orders, потому что подключается под ролью, которой в новой базе ничего не разрешено.

Здесь нет мистики. Строки таблиц составляют только часть того, на что опирается приложение. Остальное можно назвать окружением базы: версия сервера, установленные расширения, владельцы объектов, права, параметры подключения и шифрования. На Heroku всё это сложилось незаметно и работало само. На своём VPS каждый пункт придётся сделать сознательно. Ниже разобран порядок, в котором перенос Heroku Postgres превращается из «восстановили дамп» в «приложение работает», и в центре стоит репетиция на отдельной базе.

Сначала запрос, потом дамп

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

  • какие таблицы и последовательности он затрагивает;
  • какие функции, типы или операторы из расширений использует: поиск по триграммам, uuid-генераторы, hstore, citext, полнотекстовые словари;
  • под какой ролью выполняется: у веб-процесса и у воркера строка подключения могут быть разными;
  • какие настройки соединения ему нужны: шифрование, часовой пояс, число одновременных подключений.

Получится короткая таблица, которая позже станет сценарием приёмки. Для условного магазина она может выглядеть так:

Пример прикладных операций магазина и зависимостей от базы
ОперацияЧто нужно от базыЧто ломается при потере
Поиск товара по названиюрасширение для триграмм, индекс на его основефункция не найдена или поиск уходит в полный перебор
Оформление заказаINSERT и UPDATE в заказы, USAGE на последовательностиошибка прав на вставке, хотя чтение работает
Ночной отчётотдельная роль воркера с SELECTотчёт молча падает, пока никто не смотрит логи
Вход в админкурасширение для регистронезависимых emailпользователи с «разным» регистром не находятся

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

Что именно вы снимаете с Heroku

Heroku PGBackups делает экспорт в custom-формате pg_dump, и, по документации, такой экспорт можно восстановить в другую PostgreSQL (Heroku: Importing and exporting Heroku Postgres databases). Это хорошая новость: путь стандартный, никакого проприетарного формата. Та же страница предупреждает, что дамп и восстановление требуют ресурсов и могут влиять на загруженную базу. Для репетиции это значит: не запускайте несколько тяжёлых выгрузок подряд в рабочее время и не делайте их «просто посмотреть». Один дамп для репетиции в спокойный период, затем работа только с копией.

Теперь важный нюанс, который формат дампа не отменяет. pg_dump экспортирует одну базу. В документации PostgreSQL сказано, что он делает согласованный снимок даже при параллельной работе читателей и писателей, а глобальные объекты, включая роли и tablespaces, в дамп одной базы не входят: для них есть pg_dumpall (PostgreSQL: pg_dump). То есть роли, под которыми приложение подключалось, дамп вам не привезёт. Прав на объекты внутри базы это тоже касается отдельно: в дампе обычно остаются записи о владельцах и командах GRANT, но сами роли, на которые они ссылаются, на новом сервере должны существовать.

Версия: проверить до восстановления, а не после ошибки

Первый вопрос к целевому серверу: какой major-версии PostgreSQL на нём работает и какой pg_dump/pg_restore вы используете. Правило из документации звучит строго: pg_dump не работает с сервером более новой major-версии, чем сам инструмент, а восстановление в более старый сервер не гарантировано. Практический вывод для переезда выстраивается из двух линий.

  • Целевая версия не ниже исходной. Переносить базу «вниз», с более новой версии на более старую, не стоит: даже если часть объектов восстановится, гарантий нет, а проблемы проявятся не сразу.
  • Клиентские утилиты не старше исходного сервера. Если вы снимаете дамп сами с Heroku, ваш pg_dump должен быть не старше его версии. Если используете уже готовый файл из PGBackups, главное, чтобы pg_restore на VPS понимал его формат.

Версии проверяются двумя короткими командами, по одной с каждой стороны:

SELECT version();
SHOW server_version;

И отдельно в оболочке VPS:

pg_restore --version
psql --version

Если исходная и целевая версия совпадают по major, вопросов меньше. Если целевая выше, это допустимый и обычный путь, но тогда репетиция особенно ценна: поведение планировщика, права по умолчанию и некоторые функции между major-версиями отличаются, и проявится это на ваших запросах, а не в списке изменений. Стоит один раз прогнать на копии самые тяжёлые запросы и сравнить, не изменились ли планы. Здесь нет универсальной цифры; ориентир только ваш: тот же запрос на тех же данных на старой и новой базе.

Версия сервера не единственное, что надо сверить заранее. Прикиньте размер: дамп в custom-формате меньше самой базы с индексами, но развёрнутая база займёт места больше файла. Диска на VPS должно хватить на сам файл дампа, на развёрнутую базу и на запас под индексы, построенные при восстановлении, журналы и временные файлы. Сложите это на бумаге до заказа сервера, а не после остановки восстановления на середине.

Что не лежит в строках таблиц

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

Расширения: проверить наличие до восстановления

Расширения устроены хитро. В дампе сохраняется команда CREATE EXTENSION, но не код расширения. Если на новом сервере соответствующий модуль не установлен, команда упадёт, а следом посыплются объекты, которые от него зависят: индексы, функции, колонки специальных типов. Часть таких ошибок pg_restore по умолчанию переживает и продолжает работу, в итоге получается «почти нормальная» база без нужных индексов.

Поэтому список нужных расширений собирают на исходной базе:

SELECT extname, extversion FROM pg_extension ORDER BY extname;

Затем проверяют, доступны ли они на целевом сервере:

SELECT name, default_version FROM pg_available_extensions ORDER BY name;

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

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

Роли: не копировать платформенные вслепую

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

SELECT rolname, rolsuper, rolcanlogin FROM pg_roles ORDER BY rolname;

Из полученного списка выделите три группы.

  • Роли, под которыми подключается ваш код. Их нужно воспроизвести на VPS. Имя можно сохранить или поменять, но тогда придётся поменять и все упоминания в правах, и строки подключения. Практичнее создать осмысленные имена, например shop_owner для владельца схемы и shop_app для приложения, и перепривязать объекты.
  • Служебные роли платформы. Не переносить. Их права и привилегии рассчитаны на среду, которой у вас больше нет, а перенос суперпользовательских атрибутов открывает лишние возможности для ничего не дающих учётных записей.
  • Роли, которые вы создавали сами: для аналитики, отчётов, интеграций. Их надо вспомнить и воспроизвести. Здесь пригодится pg_dumpall с флагом, ограничивающим вывод глобальными объектами, но результат стоит читать глазами и применять выборочно, а не заливать целиком.

Отсюда вытекает рабочее решение: восстанавливать дамп с опциями, которые не тянут за собой чужих владельцев и прав, а затем назначать их явно. У pg_restore для этого есть --no-owner и --no-acl; в паре с --role можно развернуть объекты от нужного владельца. После этого права приложения выдаются своими командами:

GRANT CONNECT ON DATABASE shop TO shop_app;
GRANT USAGE ON SCHEMA public TO shop_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO shop_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO shop_app;

Это самый простой вариант, а не единственно правильный. Если у прежнего приложения были урезанные права (например, воркер отчётов только читал), воспроизводите именно их, а не «выдаём всё, чтобы работало». Миграция полезный повод навести порядок в правах, но делайте это осознанно: за одну репетицию лучше найти все места, где приложению не хватило прав, чем потом расширять доступ по очереди после жалоб.

Отдельно про последовательности. Вставка в таблицу с автоинкрементом требует права на последовательность, и ошибка «permission denied for sequence» классический след забытого GRANT ... ON ALL SEQUENCES. Заодно сверьте и значения: после восстановления текущее значение последовательности должно быть не меньше максимального идентификатора в таблице, иначе новая запись упрётся в дубликат ключа. Проверка занимает один запрос на таблицу и экономит неприятный первый вечер.

Владельцы и права по умолчанию

Права, выданные на существующие таблицы, не распространяются на таблицы, которые появятся позже. Если приложение само применяет миграции (а большинство фреймворков так делает), то новые объекты будет создавать та роль, под которой идут миграции. Чтобы приложение имело доступ к ним, настройте права по умолчанию для роли-владельца:

ALTER DEFAULT PRIVILEGES FOR ROLE shop_owner IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO shop_app;

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Репетиция: восстановление в отдельную базу

Теперь можно восстанавливать. Принцип один: репетиция идёт в отдельную базу, на изолированном адресе, и не касается рабочих данных, пока они ещё на Heroku. Само восстановление мы воспринимаем как проверку гипотезы «наш рецепт среды собирает работающую базу», а не как событие переезда.

Изоляция выглядит просто. PostgreSQL на VPS слушает только локальный интерфейс или закрытый адрес, внешний доступ закрыт файрволом, а к репетиционной базе вы подключаетесь через SSH-туннель или с самого сервера. Копия боевых данных не должна оказаться доступной из интернета только потому, что вы торопились. Для проверки приложения поднимите отдельный экземпляр (на этом же VPS или на втором) с отдельным файлом настроек, который смотрит на репетиционную базу. Важно, чтобы тестовый экземпляр не мог случайно отправить письма, списать оплату или дёрнуть внешние API от имени настоящих пользователей; отключите или замените ключи интеграций, пока идёт проверка.

Последовательность выглядит так:

  1. Создать на VPS роли владельца и приложения и пустую базу с владельцем shop_owner.
  2. Убедиться, что нужные расширения доступны для установки.
  3. Посмотреть содержимое архива командой pg_restore --list: так видно, какие записи об расширениях, владельцах и правах в нём есть и что именно вы собираетесь применять.
  4. Восстановить с флагами --no-owner и --no-acl, указав базу назначения. Архивные форматы восстанавливаются именно через pg_restore, а обычный SQL-файл через psql: убедитесь, что используете инструмент, подходящий формату.
  5. Выдать права приложению и настроить права по умолчанию.
  6. Сохранить вывод восстановления и прочитать его целиком, а не только последние строки.

Последний пункт заслуживает отдельного слова. Сообщение «ошибок нет» и пустой вывод разные вещи: утилита могла быть запущена с настройками, подавляющими часть сообщений, а вывод мог уйти не туда. Читайте журнал. Если вы запускаете восстановление с флагом остановки на первой ошибке (--exit-on-error), то получите короткий и честный сигнал; без него утилита продолжит работу и оставит вам список мелких ошибок, среди которых нужно найти действительно важные. Для репетиции полезнее второй режим: за один прогон вы увидите все проблемы сразу. Для боевого переноса лучше первый: любую ошибку нужно осознать, а не пролистать.

Чем отличается чистое восстановление от «чистого вида»? Тем, что после него не нужно ничего чинить руками, чтобы работало. Если после репетиции вам пришлось что-то выполнить вручную, этот шаг обязан попасть в письменный сценарий, потому что в день переноса вручную вы будете делать то же самое в условиях нехватки времени.

Приложение против восстановленной базы

Теперь к проверке, ради которой всё затевалось. Количество таблиц и строк подтверждает, что содержимое доехало. Работоспособность подтверждается только действиями. Вернитесь к таблице операций из первого раздела и пройдите её по пунктам на тестовом экземпляре.

Полезно разделить проверку на три уровня.

  • Целостность данных. Сравните число строк в ключевых таблицах с исходной базой на момент дампа, суммы по важным числовым колонкам (например, сумма заказов за месяц), максимальные идентификаторы и значения последовательностей. Это дешёвая проверка, которая ловит обрезанное восстановление.
  • Схема и среда. Убедитесь, что расширения установлены (pg_extension), индексы на месте и валидны, а у роли приложения есть нужные права: проверить это быстро можно, подключившись под ней и выполнив по одной операции каждого типа.
  • Поведение приложения. Войти, создать, изменить, удалить, запустить фоновую задачу, построить отчёт. Именно здесь вскрываются запросы, которые зависят от параметров сессии, сортировки или версии сервера.

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

Заодно сравните производительность. Данные только что восстановлены, а значит, статистика планировщика пуста, и первые запросы могут идти по неудачным планам. Выполните сбор статистики (ANALYZE) и только после этого оценивайте скорость. Сравнивать нужно на ваших запросах из списка, а не на синтетике, и без выводов вида «в столько-то раз быстрее»: абстрактные числа здесь не нужны, важно лишь, что критичные операции укладываются в привычное для вас время.

Соединения: TLS и параметры подключения

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

Поэтому параметры подключения нужно проверять по целевой конфигурации, а не переносить по памяти:

  • Шифрование на сервере. Включено ли оно в postgresql.conf, есть ли сертификат и ключ, кто и как их выпускал. Если приложение и база живут на одном VPS и общаются через локальный сокет, шифрование между ними не нужно; если база на отдельном сервере, нужно.
  • Режим клиента. Параметр sslmode в строке подключения: от «не шифровать» до «шифровать и проверять имя хоста по сертификату». Если вы раньше использовали режим без проверки, а теперь поднимаете собственный сертификат, самое время настроить полноценную проверку, а не копировать послабление. Сертификат должен соответствовать имени, по которому приложение обращается к базе.
  • Доступ по сети. Адреса, на которых слушает сервер (listen_addresses), и правила в pg_hba.conf: кто, откуда, под какой ролью и каким способом аутентификации может подключаться. Правило должно разрешать именно приложение, а не всю подсеть.
  • Лимиты соединений. Число подключений на новом сервере задаётся вашей настройкой, а не платформой. Сложите пулы всех процессов приложения (веб, воркеры, планировщик) и убедитесь, что сумма не превышает лимит сервера с запасом для административных подключений.

Тестируйте строку подключения так, как её увидит приложение: из того же окружения, под той же ролью, с теми же параметрами шифрования. Проверка с вашего ноутбука под суперпользователем ничего не доказывает. Если приложение развёрнуто иначе, чем на Heroku (отдельные процессы вместо dyno, свой менеджер процессов), эту часть переезда разбирает статья Из Heroku на VPS: чем заменить dyno, Procfile и дополнения: она нужна, чтобы строка подключения попадала в процессы тем способом, которым вы привыкли.

Финальный перенос: остановить записи и переключить

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

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

Порядок финального переноса может быть таким:

  1. Предупредить пользователей и включить режим обслуживания, чтобы веб-часть перестала принимать записи.
  2. Остановить воркеры, планировщики и всё, что пишет в базу в фоне. Проверить в pg_stat_activity, что активных записывающих сессий не осталось.
  3. Снять свежий дамп. Помните, что это нагрузка на исходную базу (на что указывает документация Heroku), но в режиме обслуживания она уже никому не мешает.
  4. Восстановить в целевую базу по тому же сценарию, что и на репетиции, с теми же флагами и теми же последующими шагами по ролям, расширениям и правам.
  5. Выполнить короткую проверку целостности: счётчики и суммы по ключевым таблицам, значения последовательностей.
  6. Поменять параметры подключения: переменные окружения или конфигурацию, где хранится строка соединения. Перезапустить процессы приложения, чтобы они взяли новые настройки.
  7. Выключить режим обслуживания только после проверки прикладных операций.

Старую базу на Heroku не удаляйте сразу. Она нужна как точка сравнения и аварийный выход, пока вы не убедитесь, что новая база принимает реальную нагрузку. При этом помните про простую ловушку: после того как приложение начало писать в новую базу, возврат на старую означает потерю этих новых записей. Решение «откатываемся» должно приниматься в первые минуты, пока различие невелико, и по заранее оговорённому условию, а не в панике через сутки.

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

Где всё это жить на VPS

Роль сервера в этой истории прикладная. Базе нужен диск, достаточно быстрый для восстановления и работы индексов, память для кэша и соединений, а вам нужен root-доступ, потому что без него не поставить пакеты расширений, не настроить pg_hba.conf, сертификаты и файрвол. У VPS MAATRIX заявлены NVMe-диски, процессоры AMD EPYC и root-доступ; в публичном предложении перечислены локации UK, US, RU и FR, а оплата доступна картой РФ и через СБП. Серверы создаются автоматически за пару минут после оплаты, поэтому репетиционную площадку можно поднять быстро, не дожидаясь, пока закончатся раздумья.

Тарифы различаются ресурсами: от Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) до Djed (16 vCPU, 32 ГБ RAM, 320 ГБ). Эти цифры описывают конфигурацию, а не обещают, сколько запросов или пользователей выдержит ваша база, поэтому выбирать надо по собственным измерениям: размер развёрнутой базы, число соединений, самые тяжёлые запросы с репетиции. Репетицию разумно провести на том же тарифе, на который вы собираетесь переезжать: тогда время восстановления и поведение запросов будут относиться к реальным ресурсам, а не к воображаемым. Если базе не хватает, это обнаружится до переноса, а смена тарифа обсуждается заранее, а не посреди окна обслуживания. Резервное копирование базы на новом сервере тоже становится вашей задачей: наличие автоматических копий во всех тарифах в публичном предложении не подтверждено, так что план снятия и, главное, проверки восстановления нужно предусмотреть самостоятельно. Ровно так же, как это делалось на репетиции.

Первое настоящее действие

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

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