Топ-100

Как мы упростили обновление СУБД Pangolin: показываю бэкенд решения

25.04.2025

Меня зовут Николай Литковец, я инженер-разработчик в СберТехе, развиваю СУБД Pangolin — это реляционная СУБД, целевая в Сбере и не только. До недавнего времени у нас было два типа обновлений СУБД: минорное и мажорное. Минорное обновление — быстро и сравнительно просто, мажорное — долго, муторно, со значительными затратами ресурсов сервера. Мы стали думать, можем ли мы где-то обойтись без перехода на мажорные версии? Нашли сценарий, где это было возможным, и через некоторое время у нас появилось минорно-мажорное обновление, которое теперь экономит нам силы и время. На Хабре я рассказал про создание инструмента, который позволил нам устроить эту реформу, как обходили риски, и что нам это дало. Здесь приводится сокращённая версия текста, больше технических деталей — в полной версии статьи на Хабре.

В Platform V Pangolin DB изначально было предусмотрено два сценария обновления:

  • Обновление исполняемых данных. Или минорное обновление. Здесь достаточно заменить старые файлы (исполняемые, конфигурационные, файлы расширений) их обновлёнными версиями. Либо обновить версии утилит в составе дистрибутива. Формат внутреннего хранилища при таких модификациях не меняется. Минорное обновление мы проводим, например, при изменении конфигурационных файлов, используемых утилит и расширений. Или при доработке кода самой базы данных, когда в формат её внутреннего хранилища не вносятся изменения.
  • Обновление с переносом данных. Или мажорное обновление. Здесь требуется провести миграцию данных, так как в этом сценарии вносятся изменения в формат внутреннего хранилища БД.

Раньше мы проводили мажорные обновления в следующих случаях:

  • изменение мажорной версии базового PostgreSQL;
  • изменение логического или физического формата данных;
  • потеря обратной совместимости с предыдущими версиями функциональностей;
  • изменение системного каталога.

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

Как устроен системный каталог и его изменение

Системный каталог PostgreSQL — это центральный элемент базы данных. Он содержит метаданные обо всех её объектах: таблицах, индексах, пользователях и так далее. Его изменение может быть необходимо для повышения производительности, безопасности или добавления новых возможностей. 

Что именно понимается под изменением системного каталога:

  • изменение состава или структуры встроенных функций — pg_proc;
  • изменение списка встроенных типов данных — pg_type;
  • изменение состава или структуры встроенных представлений — pg_class, pg_type, pg_rewrite, а также ряда других таблиц.

Именно это зачастую требовало обновления с переносом данных. Наши изменения в составе и структуре вышеописанных системных каталогов включали в себя добавление новых объектов или модификацию (удаление) существующих.

Каждая функция, тип и представление имеют уникальный идентификатор OID (Object Identifier). Для перечисленных системных объектов он находится в диапазоне значений от 0 до 16 383 включительно. Внутри этого диапазона есть особенности:

  • OID 1–9999 зарезервированы для ручного присвоения;
  • OID 10 000–11 999 зарезервированы для автоматического присвоения с использованием скрипта genbki.pl;
  • OID 12 000–16 383 зарезервированы для неподключённых объектов, создаваемых в процессе инициализации базы данных командой initdb.

Системный каталог с описанными выше таблицами создаётся в каждой новой базе из шаблонов template0 и template1. Поэтому изменения каталогов требуется делать во всех БД.

При изменении системного каталога обновляется номер его версии, который хранится в файле global/pg_control.

Имена папок в табличных пространствах, создаваемых пользователями, совпадают с версиями системного каталога (PG_<POSTGRES_VERSION>_<CATALOG_VERSION_NO>).

При мажорном обновлении структура OID сохраняется благодаря созданию и инициализации новых баз данных с помощью команды initdb. Эта команда создаёт все объекты в соответствии с описанными правилами, включая назначение OID. При этом создаётся новый файл global/pg_control с актуальной версией системного каталога. Для переноса данных в новую базу переименовываются папки в пользовательских табличных пространствах. И создаются жёсткие символьные ссылки на эти пространства для новой БД. Это позволяет сохранить доступ к данным и обеспечить их целостность при обновлении системы.

Наша задача — сделать то же самое в уже существующей базе данных.

Реализация решения

Создаём сценарий минорно-мажорного обновления, называем его inplace upgrade. Он состоит из трёх основных частей:

  1. cкрипта обновления inplace_upgrade.sh;
  2. SQL-скриптов обновления системного каталога;
  3. утилиты обновления версии системного каталога.

Расскажу о них подробно.

Скрипт inplace_upgrade.sh содержит всю последовательность действий для обновления. Его задача — запустить Pangolin в режиме binary и применить SQL-скрипты обновления для всех баз данных. 

После чего СУБД запускается в режиме single: без pg_cronautovacuum, пользовательских бэкендов и других процессов, которые могут помешать работе inplace upgrade. Режим binary используется, чтобы задавать OID новых объектов с системными значениями.

Но как запустить Pangolin с новыми исполняемыми файлами, если версия системного каталога, которая им соответствует, отличается от текущей версии? Для решения этой проблемы мы разработали утилиту update_catalog_version. С её помощью мы меняем версию системного каталога в начале запуска скрипта. 

Cистемные каталоги есть во всех обновляемых базах данных. Нам нужно предварительно получить их список. В скрипте обновления подразумевается запрос всех присутствующих БД. Запускаем БД в обычном режиме, применяем запрос: 

SELECT datname FROM pg_database;

Получаем список баз данных. Базы данных или их объекты могут находиться в пользовательских табличных пространствах, завязанных на версию системного каталога. Перед запуском Pangolin нужно переименовать папки пользовательских табличных пространств.

Чтобы не возникло проблем при доступе к БД, нужно сделать это до первого запуска Platform V Pangolin DB при обновлении. Нам требуется получить список всех табличных пространств, переименовать их. И список нужен до обновления исполняемых файлов СУБД.

Мы решили выполнять всё это на работающей базе до обновления. Список пользовательских табличных пространств получаем SQL-запросом:

SELECT pg_tablespace_location(oid) FROM pg_tablespace WHERE spcname not in ('pg_default', 'pg_global')

Так у нас появился ещё один режим работы скрипта. Мы назвали его info. Добавили ещё ряд дополнительных проверок, опишу их ниже. 

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

Что может пойти не так?

Главная опасность — испортить системный каталог. Если он сломается, то база данных вообще не запустится. Или может перестать работать ранее заявленная функциональность. Мы решили собирать все SQL-скрипты в одну транзакцию. Если где-то произойдёт ошибка, то изменения системного каталога не применятся. Однако ввиду того, что обновлять нужно больше одной базы данных, проблема может возникнуть в любой из них. Тогда получится, что часть баз данных обновится, а часть — нет. Чтобы этого не произошло, мы применяем скрипты обновления ко всем базам данных с последующим откатом. Если в результате не возникло ошибок, то обновляем все БД нашими скриптами.

Вроде бы задача решена. Только как нам убедиться в том, что мы не испортили целостность системных каталогов во всех БД? Используем утилиту pg_dump. Формируем дамп системных каталогов во всех базах данных после обновления:

pg_dump -h $HOST -p $PORT -U $USER -d $dbname -n pg_catalog

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

Что делать, если мы всё-таки испортили системный каталог в какой-нибудь базе данных? Делаем резервную копию файлов системного каталога во всех БД. С помощью системной функции pg_relation_filepath узнаём имена файлов, соответствующих таблицам системного каталога, и сохраняем их копию. Если портится системный каталог, то восстанавливаем все его файлы из сохранённых копий.

Процедура восстановления системного каталога довольно сложная, поэтому она вынесена в отдельный режим работы скрипта reset.

Собственно, основной режим его работы мы назвали update. Он существует в двух вариантах:

  • на мастере;
  • на реплике.

Отличаются они только тем, что на реплике системный каталог дообновляется не с помощью скриптов обновления, а с помощью репликации.

Итак, у нас получилось три режима работы скрипта inplace_upgrade.sh:

  •  info;
  • update;
  • reset.

Далее расскажу, как они работают.

Утилита обновления версии системного каталога update_catalog_version

Эта утилита разработана для обновлении версии системного каталога в файле global/pg_control. Как ранее упоминалось, Pangolin не запустится, если версия системного каталога в его исполняемых файлах не соответствует версии системного каталога в файле global/pg_control. Для этого утилита при остановленной базе данных считывает файл global/pg_control, формирует его копию pg_control.bak, проверяет, что новая версия системного каталога превышает текущую, и устанавливает новую версию системного каталога. Также можно поменять версию системного каталога в обход проверок на случай ручного восстановления версии системного каталога, при исправлении неполадок обновления.

SQL-скрипты создаются для каждой версии СУБД Pangolin, в которой происходят изменения в составе системного каталога. 

Разберём сценарии работы скрипта inplace_upgrade.sh

Обновление системного каталога проходит в два этапа.

  • Разведка: оценка возможностей и необходимости обновления.
  • Обновление: выполнение всех необходимых изменений и модификаций системы.

Рассмотрим пример обновления с версии 6.3.1 на версию 6.3.4. В этом процессе изменения системного каталога происходят в версиях 6.3.3 и 6.3.4. Это условный сценарий, но он демонстрирует последовательность действий.

Разведка

Здесь оценивается, можно и нужно ли выполнять обновление. Важно учесть, что в этот период база данных продолжает функционировать, но пользователям ограничен доступ к ней до завершения обновления. Сценарий выполняется как на основном сервере (мастере), так и на репликах.

Сценарий состоит из следующих этапов.

Проверка необходимости обновления: анализ структуры SQL-скриптов, предназначенных для обновления.

Проверка возможности обновления версии системного каталога:

Проверка доступа к SQL-скриптам обновления на чтение:

Формирование списка табличных пространств:

При успешном прохождении всех этапов получаем соответствующее сообщение.

INFO mode finished with Success. Update required. ===================================================

Если все этапы проверок прошли без ошибок, переходим к обновлению.

Обновление

Есть два сценария обновления:

  • обновление мастера;
  • обновление реплики.

Выполняется на остановленной СУБД и состоит из следующих этапов.

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

Обновление версии системного каталога в файле global/pg_control: проводится с использованием утилиты update_catalog_version. Этот шаг крайне важен, поскольку без него СУБД не сможет запуститься.

Формирование общего SQL-скрипта обновления в соответствии с обновляемыми версиями.

Обновление названий папок в пользовательских табличных пространствах. Без этого шага после обновления нельзя получить доступ к пользовательским табличным пространствам.

Создание дампов системного каталога во всех обновляемых БД с помощью утилиты pg_dump. Эти дампы нужны для анализа внесенных изменений в системный каталог (если возникнет такая потребность) и проверки его целостности.

Создание резервной копии системных каталогов для всех обновляемых баз данных. Запускаем СУБД, формируем список файлов, соответствующих таблицам системного каталога для всех баз данных. И эти файлы сохраняются в резервную копию. Также создаётся копия файла global/pg_control с версией системного каталога до обновления, чтобы сохранить указатель на текущий чекпоинт.

Кроме того, мы сохраняем резервные копии папок pg_walpg_replslot и файла global/pg_control. Это нужно для полного отката изменений в случае возникновения проблем на дальнейших этапах.

Проверка обновления системных каталогов. СУБД запускается в режиме binary upgrade (аналогичном тому, что используется утилитой pg_upgrade для переноса объектов системного каталога с сохранением OID). Перед обновлением все необходимые скрипты применяются в тестовом режиме в одной транзакции с последующим откатом для всех баз данных.

Обновление системных каталогов. СУБД запускается в режиме binary upgrade. После чего применяется скрипт обновления к одной базе данных.

Затем СУБД останавливается и запускается в обычном режиме для создания дампа системного каталога после обновления одной базы данных. Этот дамп проверяет целостность системного каталога и если где-то что-то повреждено, выдаёт ошибку.

Эта процедура повторяется для всех обновляемых баз данных. После обновления системных каталогах во всех базах данных СУБД останавливается.

Окончание сценария обновления: после успешного обновления всех баз данных сценарий завершает работу.

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

Это значительно проще, чем обновление мастера. Само обновление системного каталога происходит в момент синхронизации с мастером после завершения сценария обновления.

Обновление выполняется на остановленной реплике и включает следующие этапы.

Проверка необходимости обновления: анализ структуры SQL-скриптов, предназначенных для обновления.

Обновление версии системного каталога в файле global/pg_control.

Формирование общего SQL-скрипта обновления в соответствии с обновляемыми версиями. Сам скрипт для обновления не требуется, но он проверяет целостность всего сценария обновления.

Обновление названий папок в пользовательских табличных пространствах.

Обновление мастера и реплики может происходить параллельно. В завершение обновления требуется восстановить синхронизацию между ними и убедиться в том, что она полностью завершилась. Это нужно, чтобы изменения системного каталога, сделанные на мастере, применились на реплике. Лишь после этого можно предоставить пользователям доступ к СУБД.

Откат изменений

Если при обновлении возникли конфликты, препятствующие запуску СУБД или нарушившие целостность системного каталога, то нужно откатить системный каталог к состоянию до обновления. Откат выполняется до предоставления пользователям доступа к СУБД.

Откат изменений подразумевает запуск сценария восстановления на остановленной СУБД и включает следующие шаги.

Восстановление из резервной копии:

  • файла global/pg_control;
  • WAL-файлов;
  • физических слотов репликации;
  • файлов системных таблиц для всех баз данных.

Восстановление структуры системного каталога: удаляются те директории, которые появились после обновления СУБД и отсутствовали ранее.

Запуск СУБД и создание дампов системного каталога для всех баз данных. Эти дампы проверяют корректность отката изменений системного каталога и его целостность.

После выполнения этих шагов восстановление считается успешным.

RESET mode finished with Success. ===================================================

Вместо заключения

Внедрение inplace upgrade значительно сократило время обновления Platform V Pangolin DB в сценарии с изменением данных системного каталога. Если мажорное обновление 100 ГБ базы занимает 65 минут, то обновление по новому сценарию выполняется за 23 минуты. К тому же он позволяет обновлять продукт проще и с меньшими рисками.

Собираемся развивать инструмент и адаптировать его для новых задач. Сейчас мы видим, что пользователи приходят с вопросами по содержанию логов инструмента — хочется сделать их более удобочитаемыми. И есть планы по небольшим доработкам скриптов обновления — о том, как здесь всё работает, писала моя коллега-DevOps в своей статье на Хабре. О развитии этого направления постараемся писать ещё статьи, как минимум будем рассказывать об этом в сообществе нашей команды (присоединяйтесь, будем рады!).