Postgres 16 → 17: чек-лист апгрейда без сюрпризов
Апгрейд мажорной версии Postgres — отдельный жанр работы. Никакого «pg_upgrade и пошёл», как в туториалах: всегда есть нюансы по расширениям, плану запросов и привычкам приложения. Я в этом году протащила два кластера с 16 на 17, и хочу собрать чек-лист, по которому это делать спокойно.
Контекст: managed Postgres у меня нет, всё руками на VPS — самый интересный режим, потому что нет никого, кто бы прибрался за тебя. Если у вас RDS, Yandex Managed или Selectel Cloud Database, часть пунктов отпадает: кнопкой апгрейд там делается за 10 минут с короткой паузой. Но даже там стоит пробежаться по совместимости.
Что новое в 17 важно для типичного приложения
Длинного списка не будет, рассказываю, что задело лично нас.
- Логическая репликация умеет резервировать seq при failover. Если ты используешь логику для подписки между шардами или на staging — стало ощутимо удобнее.
- VACUUM поведение и memory limit отдельным
maintenance_io_concurrency. На больших таблицах с частыми обновлениями автоваккум стал ровнее. - Новые JSON-функции. Появились
JSON_TABLEи компания. Если у тебя много jsonb и раньше парсил руками — есть смысл переписать. - SLRU теперь в shared buffers. Знакомая многим стена «multixact members SLRU» больше не упирается в захардкоженные мегабайты, а параметризуется. Снимает старый источник латентности на high-concurrency базах.
Из неприятного: некоторые extension-ы выпускают совместимые версии не сразу. Перед апгрейдом я ходила и проверяла, что нужные мне расширения уже собраны под 17.
Шаг 0: бэкап и план отката
Никакой апгрейд не начинается без свежего полного бэкапа базы и проверенного плана отката. У меня план простой: pg_dumpall --globals-only + pg_dump -Fc по каждой базе, складываем в S3 (Selectel Object Storage). Если апгрейд через pg_upgrade упал и оставил битые datadirs — мы за час поднимаем всё с нуля на чистом 17 из dump.
pg_dumpall --globals-only -U postgres > /var/backups/pg/globals-$(date +%F).sql
for db in $(psql -U postgres -tAc "select datname from pg_database where datistemplate = false"); do
pg_dump -U postgres -Fc "$db" -f "/var/backups/pg/${db}-$(date +%F).dump"
doneОбязательно проверь, что dump читается: pg_restore -l /var/backups/pg/-...dump | head. Молча испорченный backup — самая обидная штука.
Шаг 1: проверка расширений
Самая частая причина зависнуть на апгрейде. На 16 у нас стояло: pg_stat_statements, pgcrypto, pgvector, pg_trgm, postgis, citus (на одном кластере). С первыми четырьмя проблем нет — они идут с дистрибутивом. postgis и citus — отдельная история: их версия должна быть собрана под 17.
select * from pg_available_extensions where installed_version is not null;Сравни с pgxn.org и репозиторием PGDG, есть ли пакет {extension}-postgresql-17. Если пакета нет — апгрейд откладывается, иначе после него половина приложения отвалится.
Шаг 2: pg_upgrade --check
Это сухой прогон: он не трогает данные, но честно сообщит, что не нравится.
sudo -u postgres /usr/lib/postgresql/17/bin/pg_upgrade \
--old-datadir /var/lib/postgresql/16/main \
--new-datadir /var/lib/postgresql/17/main \
--old-bindir /usr/lib/postgresql/16/bin \
--new-bindir /usr/lib/postgresql/17/bin \
--checkЕсли упало — читай ~postgres/loadable_libraries.txt: там список extension-ов, которые pg_upgrade считает неподходящими. Часто достаточно проставить нужные пакеты PGDG.
Шаг 3: реальный апгрейд
Перед командой остановись и проверь:
- Приложение либо отключено, либо переключено в read-only.
- Все коннекты от воркеров и cron закрыты. Ловятся
pg_stat_activity. - На диске есть свободное место для второго datadir. С опцией
--linkместа нужно мало (hard link), без неё — практически второй полный размер базы.
Команда без --check, но с --link — самое быстрое:
sudo -u postgres /usr/lib/postgresql/17/bin/pg_upgrade \
--old-datadir /var/lib/postgresql/16/main \
--new-datadir /var/lib/postgresql/17/main \
--old-bindir /usr/lib/postgresql/16/bin \
--new-bindir /usr/lib/postgresql/17/bin \
--link \
--jobs=4--link переиспользует datafiles через жёсткие ссылки. Это значит: после апгрейда ты не сможешь откатиться обратно на 16 на тех же данных. Поэтому бэкап выше — обязательный шаг.
Шаг 4: пост-апгрейд
Сразу после успешного pg_upgrade запускаем analyze_new_cluster.sh, который скрипт оставит в текущей директории. Это нужно, чтобы план запросов знал актуальные статистики.
sudo -u postgres ./analyze_new_cluster.shНа большом кластере имеет смысл запустить vacuumdb --all --analyze-in-stages -j 4 — он считает статистику в три прохода, от грубой к точной, и приложение быстрее получает адекватные планы.
Шаг 5: postgresql.conf
Конфиг в новой версии часто отличается параметрами. Я делаю так: оставляю старый postgresql.conf и беру шаблон от 17, потом руками переношу свои значения. Параметры, на которые стоит обратить внимание в 17:
maintenance_io_concurrency— теперь имеет смысл задрать на SSD-сторадж.multixact_offsets_buffers,multixact_members_buffersи компания — это те самые SLRU из shared buffers. Если на 16 ты упирался в их размер — ставь явно, не оставляй default.autovacuum_naptimeиautovacuum_vacuum_scale_factor— мейнтейнс на 17 чуть быстрее, можно начать с тех же значений и потом подкручивать.
Шаг 6: совместимость драйверов
Это про клиентскую сторону. Драйверы pg для Node, psycopg2/psycopg3 для Python, JDBC — все работают с 17 без отдельных обновлений, протокол тот же. А вот пулеры на старых версиях иногда удивляют:
- PgBouncer 1.18 и старше может не поддерживать новые типы протокола. Проверь, что у тебя версия от 1.21 или выше.
- Patroni — поднимай до текущей stable, ранние версии не понимают новый
pg_basebackup. - Connection-pooler от Yandex Managed и других managed-сервисов обновляется автоматически, тут можно не волноваться.
Шаг 7: smoke-тест
После старта 17 я запускаю проверочный набор:
SELECT version();— что вообще работает.- Несколько типичных запросов из приложения, чтобы убедиться, что планы остались адекватные.
EXPLAIN ANALYZEпо двум-трём тяжёлым запросам. Если план сильно изменился к худшему — добавляем недостающую статистику или индексы.- Прогон
pg_stat_statementsчерез сутки и сравнение топ-20 запросов с тем, что было на 16. Иногда после апгрейда неожиданно выстреливает другой запрос как самый частый — повод оптимизировать.
Чего я в этот раз не сделала и пожалела
На одном из кластеров я не обновила PgBouncer заодно с Postgres. Постгрес встал, всё ходило, но через два дня всплыл странный таймаут на длинных prepared statements. Версия PgBouncer была от 2022, и на новый протокол она реагировала непредсказуемо. Полчаса потерянного времени плюс рестарт пулера в неудобное время — пустяк, но лучше делать обновление парой.
Что запомнить
- Снимаем dump перед всем.
- Проверяем расширения на наличие сборок под 17.
- Гоняем
pg_upgrade --check, потом сам апгрейд с--linkради скорости и места. - Сразу запускаем analyze.
- Перетряхиваем конфиг с оглядкой на новые параметры SLRU и autovacuum.
- Проверяем версию пулера и драйверов.
На спокойном кластере с базой ~100 ГБ всё это занимает у меня окно в 30–40 минут реального простоя плюс пара часов на проверку планов. Главное — не торопиться и не пропускать пункт с бэкапом, остальные шаги легко доразоблачить, если что-то пойдёт не так.