В реальных проектах апгрейд PostgreSQL часто «цепляет» сразу несколько задач: вы хотите ускорить диск, аккуратно переехать на NVMe, навести порядок с LVM-томами и при этом обновить версию Postgres, не раздувая окно простоя. Ниже — рабочий админский сценарий: перенос PGDATA (datadir) на NVMe/LVM, подготовка systemd к новому пути, затем апгрейд через pg_upgrade и использование rsync для сокращения финального downtime.
Статья рассчитана на Linux с systemd (Debian/Ubuntu/RHEL-подобные). Пути и имена юнитов могут отличаться, но логика одинаковая.
Что именно мы делаем и почему так
Есть два основных подхода к обновлению PostgreSQL:
Логическая миграция (pg_dump/restore, logical replication): обычно дольше, зато гибче по трансформациям и по «разводу» окружений.
Физический апгрейд через
pg_upgrade: быстро по данным, но требует совместимых условий и дисциплины по расширениям/библиотекам.
Если задача — минимизировать простой, pg_upgrade почти всегда выигрывает. А перенос datadir на NVMe/LVM лучше организовать так, чтобы:
тома и монтирование были готовы заранее;
сервис PostgreSQL однозначно указывал на новый путь (через параметры запуска/юнит), а не «где-то в одном из конфигов»;
финальная остановка была короткой: большую часть данных перегоняем заранее через
rsync, а в окно простоя — только дельту.
Предварительные проверки: версия, место, расширения
Перед любыми переносами соберите вводные:
текущая версия PostgreSQL и целевая версия (например, 14 → 16);
фактический размер
PGDATA(и отдельно объёмpg_wal, если он вынесен/разросся);список расширений и их наличие для новой версии (особенно
postgis,timescaledb, сторонние FDW).
psql -U postgres -c "SELECT version();"
psql -U postgres -c "SHOW data_directory;"
psql -U postgres -c "SELECT extname, extversion FROM pg_extension ORDER BY 1;"
du -sh /var/lib/postgresql
Важно:
pg_upgradeапгрейдит конкретный кластер (конкретныйPGDATA). Если у вас несколько кластеров/инстансов на хосте, планируйте и проверяйте каждый отдельно.
Если вы не уверены, что текущий сервер «впишется» в новое железо/конфигурацию, полезно заранее прочитать про эксплуатационные нюансы: тюнинг autovacuum и индексов в PostgreSQL (часто именно он влияет на скорость пост-апгрейдного «прогрева»).
Для больших баз важно заранее оценить I/O и запас по месту. Если вы переезжаете на новый сервер под PostgreSQL, чаще всего практичнее брать отдельный VDS под базу и не смешивать её с вебом и очередями, чтобы предсказуемо управлять диском и нагрузкой.

Готовим NVMe + LVM под новый datadir
Почему «NVMe + LVM» — хорошая связка: NVMe даёт IOPS/латентность, а LVM упрощает рост тома и обслуживание. При этом LVM не заменяет бэкапы и репликацию.
Разметка и создание тома
Предположим, NVMe виден как /dev/nvme0n1. Создадим PV/VG/LV и файловую систему. Названия подстройте под свои стандарты.
lsblk
sudo pvcreate /dev/nvme0n1
sudo vgcreate vg_pg /dev/nvme0n1
sudo lvcreate -n lv_pgdata -L 200G vg_pg
sudo mkfs.ext4 -m 0 -E lazy_itable_init=0,lazy_journal_init=0 /dev/vg_pg/lv_pgdata
Создаём точку монтирования и монтируем:
sudo mkdir -p /pgdata
sudo mount /dev/vg_pg/lv_pgdata /pgdata
sudo chown postgres:postgres /pgdata
sudo chmod 700 /pgdata
Добавьте запись в /etc/fstab (лучше по UUID):
sudo blkid /dev/vg_pg/lv_pgdata
sudoedit /etc/fstab
UUID=REPLACE_ME /pgdata ext4 noatime,nodiratime 0 2
Проверяем, что автомонтирование работает:
sudo umount /pgdata
sudo mount -a
mount | grep /pgdata
Права и SELinux/AppArmor
На SELinux enforcing новый каталог может «молча» ломать запуск PostgreSQL. Идея простая: новый путь должен быть разрешён политикой для postgresql. Конкретные команды зависят от дистрибутива и ваших политик безопасности, поэтому проверку делайте по логам после пробного старта (см. ниже про диагностику через journalctl).
Находим текущий PGDATA и как он задаётся в systemd
Частая причина проблем: каталог перенесли, а точку правды, где сервис берёт datadir, не обновили. В systemd это обычно одно из трёх мест:
переменная окружения
PGDATA(в env-файле или прямо в юните);параметр
-DвExecStart;дистрибутивные «обёртки» (например, Debian/Ubuntu часто управляют кластерами через
pg_ctlcluster).
Сначала выясняем, какой юнит запускает PostgreSQL и что в нём определено:
systemctl status postgresql
systemctl cat postgresql
systemctl show postgresql -p FragmentPath -p DropInPaths
Если юнит «версийный» (часто на RHEL-подобных), проверяйте его:
systemctl status postgresql-14
systemctl cat postgresql-14
Дальше я буду писать «сервис PostgreSQL». Подставляйте ваш юнит:
postgresql,postgresql-14,postgresql@14-mainи т.д.
Перенос datadir: быстрый pre-sync через rsync
Цель: заранее прогнать почти весь PGDATA на новый NVMe-том, пока PostgreSQL работает. Это не финальная копия (данные меняются), но она сильно уменьшит дельту в момент остановки.
Первый rsync на живой базе
Пример: текущий datadir /var/lib/postgresql/14/main, новый путь /pgdata/14/main.
sudo -u postgres mkdir -p /pgdata/14/main
sudo -u postgres chmod 700 /pgdata/14/main
sudo rsync -aHAX --numeric-ids --delete --info=progress2 /var/lib/postgresql/14/main/ /pgdata/14/main/
Пояснения по флагам:
-aсохраняет права/владельца/время (насколько возможно);-HAXпомогает сохранить hardlinks и расширенные атрибуты (если они реально используются в вашем окружении);--deleteделает приёмник зеркалом источника;слэши в конце путей важны: копируется содержимое каталога, а не сам каталог как вложенный.
Финальный rsync в окно простоя
В момент cutover вы останавливаете PostgreSQL, делаете ещё один rsync (обычно быстро, потому что основная масса уже скопирована), переключаете PGDATA и стартуете сервис.
Cutover: переключаем PGDATA и правим systemd
Ниже — общий подход. Конкретика зависит от того, как ваш дистрибутив задаёт datadir.
Остановка сервиса и финальная синхронизация
sudo systemctl stop postgresql
Убедитесь, что процессов не осталось:
ps aux | grep [p]ostgres
Финальный rsync:
sudo rsync -aHAX --numeric-ids --delete --info=progress2 /var/lib/postgresql/14/main/ /pgdata/14/main/
Переключение путей: вариант через systemd drop-in
Самый безопасный метод — не править пакетные юниты, а сделать drop-in. Создаём его:
sudo systemctl edit postgresql
Дальше содержимое зависит от исходного юнита. Два типовых варианта:
1) Если в юните учитывается PGDATA из окружения:
[Service]
Environment=PGDATA=/pgdata/14/main
2) Если в юните задан ExecStart с -D, нужно переопределить ExecStart полностью (сначала очистить):
[Service]
ExecStart=
ExecStart=/usr/lib/postgresql/14/bin/postgres -D /pgdata/14/main
Применяем изменения и пробуем старт:
sudo systemctl daemon-reload
sudo systemctl start postgresql
sudo systemctl status postgresql
Проверяем, что PostgreSQL действительно использует новый datadir:
sudo -u postgres psql -c "SHOW data_directory;"
Если вы на Debian/Ubuntu с кластерным управлением, нередко правильнее менять datadir средствами кластера/конфигов. Но итоговая проверка всегда одна:
SHOW data_directory.
Апгрейд версии через pg_upgrade: быстрый путь
Дальше — про сам pg_upgrade. Практичная схема с минимальным простоем выглядит так:
Ставим новую версию PostgreSQL (бинарники) заранее.
Инициализируем новый пустой кластер на NVMe (новый
PGDATA) заранее.Гоняем
pg_upgrade --checkзаранее, чтобы отловить несовместимости.В окно простоя: стоп старого сервиса,
pg_upgrade, старт нового сервиса.Post-upgrade: analyze/статистика, контроль логов, контроль расширений.
Подготовка каталогов старого и нового кластера
Удобно держать структуру на NVMe явной:
/pgdata/14/main— старый кластер (уже перенесён)./pgdata/16/main— новый кластер под апгрейд.
Создаём каталог для нового кластера:
sudo -u postgres mkdir -p /pgdata/16/main
sudo -u postgres chmod 700 /pgdata/16/main
Инициализируем новый кластер (пути к бинарникам зависят от дистрибутива/пакетов):
sudo -u postgres /usr/lib/postgresql/16/bin/initdb -D /pgdata/16/main
Проверка совместимости: pg_upgrade --check
Запустите проверку до окна работ. Если она падает на расширениях или библиотеках, у вас будет время поставить нужные пакеты под новую версию.
sudo -u postgres /usr/lib/postgresql/16/bin/pg_upgrade --check -d /pgdata/14/main -D /pgdata/16/main -b /usr/lib/postgresql/14/bin -B /usr/lib/postgresql/16/bin
Окно простоя и запуск pg_upgrade
Останавливаем старый кластер и запускаем апгрейд.
sudo systemctl stop postgresql
По умолчанию pg_upgrade старается использовать link-mode (жёсткие ссылки), это быстро, но требует, чтобы старый и новый datadir были на одной файловой системе. Если оба каталога на одном LVM-томе — отлично.
sudo -u postgres /usr/lib/postgresql/16/bin/pg_upgrade -d /pgdata/14/main -D /pgdata/16/main -b /usr/lib/postgresql/14/bin -B /usr/lib/postgresql/16/bin
Если вы хотите более прямолинейный откат ценой времени (копирование данных), используйте --copy:
sudo -u postgres /usr/lib/postgresql/16/bin/pg_upgrade --copy -d /pgdata/14/main -D /pgdata/16/main -b /usr/lib/postgresql/14/bin -B /usr/lib/postgresql/16/bin

Post-upgrade: статистика и «прогрев»
pg_upgrade обычно оставляет в рабочем каталоге скрипты наподобие analyze_new_cluster.sh и delete_old_cluster.sh. Прочитайте их перед выполнением и запускайте по месту.
Если отдельного скрипта нет или вы хотите контролируемый запуск — обновите статистику так:
sudo -u postgres /usr/lib/postgresql/16/bin/vacuumdb --all --analyze-in-stages
systemd: корректный запуск нового кластера и защита от ошибок
После апгрейда важно, чтобы systemd стартовал именно новую версию и новый PGDATA, а старый кластер не поднимался случайно.
Переключаем сервис на новый PGDATA
По сути, это повторение идеи с drop-in, но уже для новой версии/нового каталога. Минимальный набор проверок после переключения:
sudo systemctl daemon-reload
sudo systemctl start postgresql
sudo -u postgres psql -c "SHOW data_directory;"
sudo -u postgres psql -c "SELECT version();"
Журналы и быстрые проверки
Если что-то пошло не так, не гадайте — читайте journal:
journalctl -u postgresql -n 200 --no-pager
А ещё проверьте, что у каталога правильный владелец и режим:
sudo ls -ld /pgdata /pgdata/16 /pgdata/16/main
Как уменьшить downtime ещё сильнее
Pre-sync через rsync перед cutover (описан выше): фактически это главный рычаг уменьшения простоя при переносе.
pg_upgrade --checkзаранее: ловите несовместимые расширения до окна работ.Новый кластер (
initdb) заранее: не тратьте минуты на инициализацию в downtime.Упрощайте структуру datadir на время апгрейда: меньше «магии» с симлинками и нестандартными путями — меньше сюрпризов.
Реальный простой — это не только время
pg_upgrade, но и время прогрева кэшей, выполнения analyze и восстановления пула соединений приложения (например, PgBouncer/клиенты).
Если у вас большой парк приложений и соединения важнее всего, заранее продумайте пуллинг и поведение клиентов при рестарте: гайд по PgBouncer и пуллингу соединений помогает убрать часть «последствий» короткого downtime.
План отката: где «точка невозврата»
Rollback нужен всегда. Две практические заметки:
В link-mode (жёсткие ссылки) «новый» кластер разделяет файлы со «старым». Поэтому тестировать откат надо заранее в тестовой процедуре: простой «вернуть старый каталог» может быть уже небезопасен после записи в новый кластер.
Если вам важен максимально прямой откат, выбирайте
--copyи/или заранее делайте подтверждённый бэкап. LVM-снапшот иногда выручает как страховка, но на больших нагрузках может просадить I/O и требует грамотного расчёта размера.
Минимальный здравый вариант: до окна простоя иметь проверенный бэкап и возможность поднять старый сервис с прежним PGDATA. Старый каталог не удаляйте, пока не пройдёте пост-проверки и короткий период наблюдения.
Чек-лист после миграции
SHOW data_directory;возвращает новый путь на NVMe.SELECT version();показывает целевую версию.В journald нет ошибок старта и repeated crash-loop:
journalctl -u postgresql -n 200 --no-pager
Права на каталоги корректные, владелец
postgres, режимы не «шире», чем нужно.Все расширения грузятся и имеют ожидаемые версии (проверьте
pg_extension).Выполнен analyze (скрипт от
pg_upgradeилиvacuumdb).
Типовые ошибки и быстрые диагнозы
PostgreSQL не стартует после переноса PGDATA
Чаще всего причины:
systemd всё ещё смотрит в старый каталог (не обновили
PGDATAилиExecStart);не те права/владелец на новом каталоге;
том не примонтировался (ошибка в
/etc/fstab);SELinux/AppArmor запрещает доступ.
Быстрый путь: journalctl по юниту, затем проверка конфигурации юнита через systemctl cat, и только после успешного старта — контроль SHOW data_directory.
pg_upgrade ругается на несовместимые библиотеки
Почти всегда это расширения. Решение: установить пакеты расширений под новую версию до миграции и повторить pg_upgrade --check.
Итого
Комбинация «NVMe + LVM + pg_upgrade + предварительный rsync» — один из самых практичных способов обновить PostgreSQL и одновременно привести хранение данных в порядок. Ключ к минимальному downtime: максимум подготовительных действий до окна простоя, чёткое понимание, где задаётся datadir в systemd, и обязательный прогон pg_upgrade --check заранее.
Если вы строите хранилище «вдолгую», заранее продумайте, как будете масштабировать объём тома и бэкапы. Для наращивания диска на сервере пригодится инструкция: как увеличить диск на VDS и расширить раздел/ФС.


