Что будет интересного в pg_probackup 3

Сейчас, когда pg_probackup 3 активно готовится к релизу, но ещё не стал доступен для всех желающих, настал отличный момент обстоятельно поговорить о тех новшествах, которые будут нас ждать в релизе. Благо их более чем достаточно: начиная от полностью переосмысленной архитектуры самого приложения до долгожданных фич и возможности интеграции с другими приложениями.
Краткая историческая справка
Pg_probackup впервые увидел свет в 2016 году и с тех пор довольно активно развивается. Версия 2.5.15 доступна любому желающему на гитхабе, а вместе с нашими продуктами линейки Postgres Pro на текущий момент уже поставляется версия 2.8.6.

В таблице перечислены все главные релизы, их основные фичи и приведено сравнение с другими популярными инструментами. Если совсем галопом по Европам, то пробекап — это 14 поддерживаемых платформ, три типа инкрементов, возможность сливать их, делать catchup для быстрого рестора реплик, работа с S3, CFS и, конечно же, полная интеграция с продуктами Postgres Professional.
Кстати, у нас есть отдельный чатик в телеге, посвящённый пробекапу, где мы рады всем желающим проникнутся атмосферой резервного копирования постгреса.
По версиям pg_probackup, чтобы не путаться:
2.5.15 Community. Лежит на гитхабе. Вносятся только критические багфиксы. Исходный код открыт.
2.8.6 STD. Сильно переработанная, оптимизированная и более производительная версия, предназначенная в первую очередь для бекапа больших объёмов данных. Версия, идущая с Postgres Pro Standart. Исходный код закрыт.
2.8.6 ENT. Всё то же самое, что и в STD, но добавлена поддержка CFS и работа с S3. Исходный код также закрыт.
3 и выше. Всё по новой, ещё лучше, быстрее, выше, сильнее, чем раньше — конкретика ниже. Исходный код будет закрыт. Во всяком случае на ближайшие пару лет точно.
Новая архитектура pg_probackup 3
В процессе обсуждения плана работ над новой версией сложился довольно стандартный для проекта с историей пасьянс.
То, что накопилось в бэклоге:
много дельных предложений от клиентов, которые хочется реализовать;
ещё больше своих идей, которые хочется воплотить даже больше, чем из первого пункта
старая архитектура, которая тянется с 2016 года, где прямо сейчас содержится критическая масса не самых красивых костылей, и с каждым разом поддерживать это становится всё труднее и труднее;
понимание, что если ничего кардинально не изменить, хотелки из первых двух пунктов реализовать не представляется возможным.
Что не устраивало клиентов:
мы собирали pg_probackup отдельно под каждую версию Postgres. Нам-то несложно пару кнопок прожать, а вот если у клиента «зоопарк» из версий Postgres, то под каждую надо держать свою сборку пробекапа, и вот это уже проблема;
формат резервной копии унаследован от структуры данных PG или говорят простыми словами: банальный файловый бекап, то есть бекап — это дерево директорий, в котором хранится огромное количество файлов. Это очень неудобно (и дорого, кстати) при работе с S3 и лентами. И даже если это всё сложить в tar, легче не становится, так как он подразумевает последовательное размещение файлов внутри архива;
при использовании SSH необходимо выдавать отдельные права для авторизации;
два отдельных коннекта для копирования БД и WAL-файлов.
Поэтому, тщательно всё обдумав, было решено сжать силу в кулак, волю в узду и написать полностью новое решение. О чём мы теперь совершенно не жалеем. Самое важное отличие состоит в том, что pg_probackup — это больше не монолитное решение, а состоящее из трёх независимых частей. Если кратко:
Реализовано расширение ядра СУБД, которое отвечает за чтение файлов, отслеживание изменившихся данных для инкрементов и так далее.
Появилась новая сущность. Можно даже сказать, сердце всего pg_probackup — библиотека LibProbackup3, работающая связующим звеном между бекапным приложением и базой. То есть, при наличии желания (и возможности) с помощью этой библиотеки можно реализовать нечто своё или интегрироваться в общее для всей инфраструктуры решение. Поэтому можно смело говорить, что LibProbackup3 — это полноценный SDK, но об этом будет отдельно написано ниже.
Само бекапное приложение, которое отвечает за связь с СУБД и управляет записью данных резервной копии на диск.

Изначально план работ предполагался следующий:
разработать ядро и базовый функционал основе нового ядра;
повторить все фичи версии 2;
реализовать то, что нельзя было сделать в версии 2;
….
PROFIT!
Но начав планировать работы, мы поняли, что это не самый правильный подход. Так же как и в случае с архитектурой, накопился груз огромного числа доработок. Причём часть решений, на момент их создания, были важны и очень нужны, а сейчас, глядя на общую картину, кажутся более чем спорными. Поэтому, раз уж мы решили делать всё с нуля, был выбран другой подход.
Как всем понятно, главная потребность клиентов — не потерять данные, причём не в смысле успешно сделать бекап, но ещё и быстро восстановить его в случае аварии. Поэтому в изначальный план были внесены некоторые изменения:
первым делом реализуем ключевые моменты, обеспечивающие создание бекапа и оперативное восстановление. Сюда вошли новая архитектура, новое ядро и новый репликационный протокол, о котором будет чуть ниже;
далее переходим к анализу текущей функциональности и заново проектируем весь UX пользователя с учётом прежнего набора возможностей. Самоцель — обеспечение 100%-го повторения — мы себе не ставили, но договорились приблизиться к ней настолько, насколько это будет возможным.
В итоге получилось вот так:
конечно же, вы можете создавать резервные копии, восстанавливаться из них и спать значительно спокойнее;
совместимость с бекапами, сделанными в более ранних версиях, имеется, и им ничего не угрожает;
больше не требуется отдельное ssh-соединение. Ваш безопасник будет рад;
не нужно настраивать отдельный доступ к PDGATA. Всё работает под ролью postgres, а безопасник рад ещё больше;
максимально реализованы фичи pg_probackup 2. Не 1:1, но практически все;
реализована поддержка нескольких источников информации для создания инкрементальных бекапов;
свой формат хранения бекапов. Больше никакого слепого копирования структуры PGDATA, а взрослый принцип «один бекап — один файл бекапа» (и рядом файл с метаданными, если уж совсем точно). И да, сжать всё в один .tar не считается бекапом здорового человека в один файл;
доведённая до ума поддержка лент и S3, которую все так ждали. Резервная копия создаётся либо в виде одного файла, либо нарезается как набор файлов заданного размера;
новый репликационный протокол, в котором используется доработанный walsender. Благодаря этому теперь можно читать и записывать данные в несколько потоков;
поддержка разных вариантов Retention Policy;
поддержка ванильного Postgres будет. Как без неё?!
Чуть подробней о Libprobackup3
Как говорилось ранее, Libprobackup3 — это библиотека, отвечающая за общение с ядром Postgres Pro и позволяющая реализовать интеграцию со сторонними системами резервного копирования. Написана она на C++, но реализует интерфейс на C и позволяет легко интегрироваться хоть в скрипты на Python, хоть в приложения на Go и C++.
Взаимодействуя непосредственно с расширением в ядре СУБД, Libprobackup3 формирует потоки архивных файлов и метаданных. Также она реализует логику управления резервными копиями и мерж инкрементов. Но, в то же время, она ничего не знает о том, где хранятся бекапы, и не занимается чтением/записью файлов базы. Это всё остаётся на откуп бекапному приложению.
Теперь рассмотрим пример простейшей интеграции на Go:
~$ golang_sample backup -pgport $SOURCEPORT -pgdata $BASE -backup-id $FULL -backup-mode FULL -backup-source=pro -storage=fs
/* Импортируем нужный пакет, содержащий функции для работы с libpgpropbackup3 */
package pgpro
/* Парсим командную строку на параметры, необходимые для бекапа */
backupCmd Parse(os.Args[2:])
copt := pgpro.ConnectionOptions{PGDatabase: *pgdatabase_flag, PGHost: *pghost_flag, PGPort: *pgport_flag}
/*Инициализируем переменные*/
bopt := pgpro.BackupOptions{
NumThreads: *num_threads_flag,
PGData: *pgdata_flag,
BackupMode: backup_mode_flag,
BackupSource: source_flag,
Storage: storage_flag,
BackupId: *backup_id_flag,
ParentBackupId: *parent_backup_id_flag,
do_backup(copt, bopt)
/*Вызываем библиотечную функцию do_backup*/
func do_backup(copt pgpro.ConnectionOptions, bopt pgpro.BackupOptions, root string, tblspc string) {
enc := &DummyEncoder{}
enc.Root = root
enc.DataFiles = make(map[int]*os.File)
enc.NonDataFiles = make(map[int]*os.File)
enc.TablespaceMapping = build_tablespaces(tblspc)
enc.DataBlockSize = 0
enc.WalDataBlockSize = 0
res := pgpro.Backup(copt, bopt, enc)
if res != nil {
fmt.Println("Backup error: ", res)
}
}
Как будто бы тут ничего сложного, так что переходим к следующей теме.
Новый репликационный протокол в pg_probackup 3
В принципе, фундаментальных подходов, как сделать бекап базы Postgres, всего три: выгрузить всё SQL-скриптом, скопировать файлы из PGDATA абсолютно любым способом и репликация (физическая или логическая, не важно), она же копирование WAL-файлов.
Необходимость разработки своего протокола общения между базой и прочими компонентами пробекапа с выделением библиотеки возникла для того, чтобы мы не были ограничены привязкой к физическим файлам и директориям, а имели больше возможностей по оптимизации чтения, записи, передачи данных по сети. Благо революции в этой затее нет.
Свой протокол есть у pg_dump, использующего SQL под капотом, у pg_basebackup, который для физического бекапа использует команды репликации, зашитые в walsender и запускающие отдельный бэкенд, внутри которого уже происходит работа с файлами. И да, имеется в виду протокол не в том смысле, что мы тут свой сетевой протокол написали, а протокол в смысле алгоритма передачи данных.
Перед началом разработки, опять же, был составлен список целей, которых хотелось достичь:
одна версия утилиты для разных версий Postgres;
полный и инкрементальный бекап должны создаваться без прямого доступа приложения к данным внутри БД;
чтение и запись данных в несколько потоков;
прозрачная для приложения работа с CFS и прочими особенностями конкретных инсталляций;
шифрование данных и WAL;
простой и понятный механизм работы с инкрементальными бекапами.

Начиналось всё с такой схемы взаимодействия компонентов, на которой изображена проверенная временем классика работы pg_probackup 2. Бекапное приложение подключается к серверу с помощью библиотеки libpq (нужно два подключения: обычное и в режиме репликации), через вызов pg_start_backup запрашиваются метаданные на момент старта. Затем начинается передача дата-файлов, после увозятся WAL-файлы, метаданные на момент завершения бекапа, и в конце всё это куда-то пишется в виде набора отдельных файлов. Всё просто, как утюг, и работает как часы.
Но всегда обязательно возникает некое «но», которое побуждает тебя сделать ещё лучше. Когда база данных становится большой, а внутри PGDATA возникает слишком неприлично сложная структура каталогов и файлов, то скопировать её так просто уже не получается. Вернее, просто скопировать можно, но, если бекап пишется на S3, такой подход будет стоить вам неприлично много денег. А если увозить куда-то на локальный или сетевой диск, то отдельно писать данные, отдельно WAL становится накладно по времени. Ну и вишенка на торте — это последовательное копирование данных, когда может возникнуть ситуация, что данные мы увезли, а WAL для них в это время удалились. Очень не хватает снапшотов в таких случаях, но чем богаты.
За основу новой версии, из двух озвученных выше вариантов, был выбран путь pg_basebackup. Мы добавили свою команду репликации, и получилось примерно вот так:
PG_PROBACKUP LABEL label [( option ['value'] [, ...] )]
LABEL 'label'
PROGRESS [ boolean ]
CHECKPOINT { 'fast' | 'spread' }
WAL [ boolean ]
COMPRESS_ALG 'method'
COMPRESS_LVL number
START_LSN [ number ] - для инкрементов.
INCR_MODE [MODE]
VERIFY_CHECKSUMS [ boolean ]
В принципе, тут всё достаточно банально, если только можно остановиться на START_LSN и INCR_MODE. Если значение START_LSN равно нулю, значит нам нужен полный бекап. Если оно отлично от нуля, значит это инкремент начиная с конкретного LSN. А через INCR_MODE можно явно указать, какой именно инкрементальный режим мы хотим использовать: DELTA, PTRACK и так далее. Поэтому новая схема работы стала несколько сложнее.

От старой версии её отличает появление отдельного бэкенда на сервере и передача данных/WAL внутри одного подключения. Это позволило закрыть проблему их разбега во времени. И тут же появляется наша библиотека libprobackup, которая на лету кодирует данные на серверной стороне, а в бекапный файл они упаковываются на стороне приложения, которое пишет их на выбранный сторадж.
С точки зрения пользователя и его консоли, это выглядело примерно вот так:
BACKUP COMMAND PG_PROBACKUP(LABEL '0-full', WAL true, VERIFY_CHECKSUMS true, COMPRESS_ALG 'none', COMPRESS_LVL 1);
PG_PROBACKUP 33554472 tli=1
PG_PROBACKUP-STOP 33554744 tli=1 bytes written=61210881 bytes compressed=61210881
[2024-07-08 12:04:55.781912] [93397] [8620067840] [info] Backup time 710
INFO: Backup 0-full completed successfully.
BACKUP INSTANCE 'test'
Instance Version ID Start time Mode WAI Mode TLI Duration Data WAI Zaig Zratio Start LSN Stop LSN Status
-------------------------------------------------------------------------------------------------------------------------------------
test 17 0-full 2024-07-08 12:04:55 FULL STREAM 1 58MB - none 1.00 0/2000028 0/2000138 DONE
Как видим, явно отражена команда репликации pg_probackup, 0_full — это название бекапа без компрессии, но с проверкой контрольных сумм. Ну и в конце вывод об успехе.
Кажется, вот теперь всё хорошо, но нет. Это был кастом для Postgres Pro, а отказываться от поддержки ванильной версии в планы не входило. Также не получилось решить старую проблему: любое добавление новых команд или изменение логики старых требовало патча в ядро. И так уж получилось, что никто не любит лишний раз пересобирать ядро своей СУБД. Ну и настройка многопоточности в варианте одной команды требовала недюжих усилий. Поэтому было принято волевое решение идти дальше.
Для начала разбили одну команду на три части, о чём ещё поговорим. Следом был написан универсальный плагин, чтобы вместо PG_PROBACKUP LABEL [(option [«value»] [, …]) ]) можно было вызывать по схеме Plugin > Plugin name > Command PGPRO_CALL_PLUGIN
Схема всей конструкции стала ещё интереснее. Единый процесс обработки данных разделился на три части. Start_backup, чтобы получить метаданные на момент начала. Copy_files — непосредственно для отвоза данных в бекап, и таких процессов может быть много, да ещё и с разными параметрами. Это обеспечило ту самую управляемую многопоточность. И в конце stop_backup для получения метаданных при завершении процесса и финализации записи. А бэкенд pg_probackup 3 стал запускаться тем самым плагином.

Соответственно, одна команда разделилась на три. Было:
BACKUP COMMAND PG_PROBACKUP (LABEL '0-full', WAL true, VERIFY_CHECKSUMS true, COMPRESS_ALG 'none', COMPRESS_LVL 1);
Стало:
[info]START BACKUP COMMAND = PGPRO_CALL_PLUGIN pgpro_bindump start backup(LABEL '0-full');
[info]PG_PROBACKUP 0/200028 tli=1
[info]BACKUP COMMAND PGPRO_CALL_PLUGIN pgpro_bindump copy_files (VERIFY_CHECKSUMS true, COMPRESS_ALG 'none', COMPRESS_LVL 1);
[info]BACKUP COMMAND PGPRO_CALL_PLUGIN pgpro_bindump stop_backup (COMPRESS_ALG 'none', COMPRESS_LVL 1);
BACKUP INSTANCE 'test'
Instance Version ID Start time Mode NAL Mode TLI Duration Data Wal Zratio Status
=================================================================
test 17 0-full 2025-02-08 13:14:09+0000 1 full stream 58MB none 1 0/2000028 0/0 DONE
Кажется, что всё, вот оно работает, и можно релизить. Но снова нет! Просто передавать данные — это хорошо, но научиться делать catchup_data и отслеживать изменения данных для создания инкрементов тоже надо. Поэтому мы решили добавить ещё несколько команд.
Так что на текущий момент имеем такую картину с репликационными протоколами. DIRECT появился ещё во второй версии, и у него аж пять режимов. В третьем пробекапе появился вариант BASE на основе basebackup. И новый протокол, о котором вы прочитали, называется PRO. Само собой все три варианта поддерживаются в последней версии пробекапа.
PAGE | FULL | DELTA | PTRACK | WALSUM | |
DIRECT | 2016 | 2016 | 2018 | 2020 | 2025 |
BASE | - | 2023 | 2023 | - | - |
PRO | - | 2023 | 2023 | 2024 | 2025 |
Итого:
Direct аналогичен протоколу работы pg_probackup 2. Требует выделения прав на каталог PGDATA и занимается его копированием as is. Проверен временем и достиг пика своего развития;
PRO не требует прав для доступа к PGDATA, масштабируется согласно нагрузке. Новичок в мире бекапов PostgreSQL, ещё не раскрывший весь свой потенциал.
Так же, на всякий случай напомню, какой режим для чего нужен, а заодно соберём эту информацию в одном месте:
FULL — создать полную копию базы;
DELTA — инкрементальный режим. Реализован за счёт постраничного сравнения файлов;
PTRACK — инкрементальный режим. Отслеживает изменения и сохраняет соответствующий LSN прямо в момент события;
PAGE — инкрементальный режим. В момент резервного копирования создаётся карта изменений на основе LSN и чексумм.
Как это работает: немного практики
Чтобы выполнить бекап с использованием нового протокола, первым делом добавляем несколько строк в конфиг.
# ------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
# ------------------------------------------------------------------------------
# Add settings for extensions here
wal_level=replica
archive_mode=always
walsender_plugin_libraries = 'pgpro_bindump' #включаем плагин
shared_preload_libraries = 'pgpro_bindump'
На стороне приложения никаких особых новшеств нет. Вы просто вызываете пробекап с обычным набором ключей. От старого он отличается только появлением --backup-source=pro
Pg_probackup3 backup -B $BACKUPDIR3 --instance $INSTANCE -b FULL --backup-source=pro --backup-id=0-full
[info] Backup catalog '/Users/supauser/projects/pgpro/../backup3' successfully initialized
[info] Instance 'test' successfully initialized
[info] This PostgreSQL instance was initialized with data block checksums. Data block corruption will be detected
[info] START BACKUP COMMAND= PGPRO_CALL PLUGIN pgpro_bindump start_backup (LABEL '0-full');
[info] PG PROBACKUP 0/2000028 tli=1
[info] BACKUP COMMAND PGPRO_CALL_PLUGIN pgpro_bindump copy_files (VERIFY_CHECKSUMS true, COMPRESS_ALG 'none', COMPRESS_LVL 1);
[info] BACKUP COMMAND PGPRO_CALL_PLUGIN pgpro_bindump stop_backup (COMPRESS_ALG 'none', COMPRESS_LVL 1);
[info] PG_PROBACKUP-STOP 0/2000138 tli=1 bytes written=48885446 bytes compressed=48885446
[info] Backup time 817
[info] Backup 0-full completed successfully.
INFO: Backup 0-full completed successfully.
[info] Start validate 0-full...
[info] Validating backup 0-full
[info] Validate time 107
[info] Backup 0-full is valid
Как видим, в логах отображены те самые новые команды репликации.
Что нового с восстановлением у pg_probackup 3?
Одной из интересных задач, которую получилось решить, стало восстановление данных напрямую из бекапа, без необходимости разворачивать весь бекап. Мы назвали это FUSE.

Решаемая проблема стара как мир. Предположим, мы бекапили базу весьма приличных размеров, и вот настигло нас несчастье: прод лежит, и всё это буквально потому, что у нас вылетело несколько файлов из-за сбоя диска.
Вариант действий при старорежимном подходе. Меняем диски, стираем все файлы и с печалью во взгляде долго наблюдаем, как весь наш бекап на много терабайт скачивается и распаковывается. И хорошо, если это займет несколько часов, а не дней.
Вариант, когда реализовано восстановление напрямую из бекапа. Файл бекапа не распаковывается на диски, а через специальный драйвер монтируется через оперативную память. То есть это полная аналогия с тем, что делает команда mount с обычными дисками. Только в данном случае диском выступает файл с бекапом.
А дальше вспоминаем, что бекап в pg_probackup 3 — это не просто архив с мешаниной из файлов, а осмысленная структура блоков, описанная в метадате. То есть мы знаем, какие файлы внутри, по какому смещению что находится и так далее. А следовательно, через точку монтирования можем вычитывать их как обычные файлы и копировать куда надо. Таким образом, мы можем или вытащить нужные нам файлы, или даже запустить наш инстанс сразу.
pg_probackup fuse –B "/task_data/backups" --mnt-path /tmp/mntpoint --instance=node -i SQA7X7
pg_ctl -D /tmp/mntpoint start
Но только помним, что чудес не бывает и не стоит ожидать такой же производительности, как если бы всё уже было распаковано и лежало на дисках рядом. Это именно возможность аварийно запустить и дальше действовать чуть более спокойно, потому что самые критичные вещи для бизнеса медленно, но заработали.
Следующей фичой, которая многим виделась во снах, стало, конечно же, возможность восстановления отдельной БД. Опять же воздадим хвалу новому формату бекапов и возможности вычитывать только конкретные файлы. Что надо отметить:
восстановление происходит в отдельный инстанс;
копируются только данные нужной БД;
такая же история с WAL;
копируются служебные данные: таблицы с настройками, списки доступных баз и так далее;
запускается режим восстановления, в котором Postgres автоматически исправляет все расхождения в служебных файлах.
pg_probackup3 restore -B --instance -D new_pgdata_dir> -i --db-include=db1
И ещё стоит отметить возможность рестора с удалённой машины, когда нам не нужны права на сервер БД. Звучит как страшный сон безопасника, но на практике всё не так ужасно.
На удалённой машине запускается pg_probackup remote-restore -p
Тестирование производительности
Вопрос производительности нового решения, само собой, крайне важный, поэтому спешим поделиться результатами.
Мы провели полный цикл сравнительного тестирования версии 2.8.6 и последнего на тот момент стабильного билда pg_probackup 3.0.0. Тесты проводили на рандомно генерируемой базе размером в 1 TB с десятью таблицами внутри. Поставленную задачу можно описать как сравнение производительности при создании полной и инкрементальной резервных копий в режимах PRO и DIRECT. Все тесты проводились в многопоточном режиме, начиная с одного потока и до 96.
Для протокола и возможности повторить тесты у себя, вот команды:
#pg_probackup2.8
pg_probackup backup -B /probackup_repo --instance=demo -b full -j 96 --no-validate --compress-algorithm=lz4
pg_probackup backup -B /probackup_repo --instance=demo -b ptrack -j 96 --no-validate --compress-algorithm=lz4
#pg_probackup3.0.0
pg_probackup backup -B /probackup_repo --instance=demo -b FULL --backup-source=pro -j 96 --compress-algorithm=lz4 --no-validate --log-level-console=info
pg_probackup backup -B /probackup_repo --instance=demo -b PTRACK --backup-source=pro -j 96 --compress-algorithm=lz4 --no-validate --log-level-console=info --parent-backup-id 2025-01-22-16-18-05-247
pg_probackup backup -B /probackup_repo --instance=demo -b PTRACK --backup-source=direct -j 96 --compress-algorithm=lz4 --no-validate --log-level-console=info --parent-backup-id 2025-01-22-16-18-05-247
В результате имеем графики и выводы следующего вида.


При создании фула версия 2.8.6 в однопоточном режиме быстрее, чем 3.0.0. Но все понимают, что в один поток бекапы никто не делает, поэтому можно пренебречь.
Начиная с 8 потоков версия 3.0.0 становится производительнее.
С инкрементами ptrack в режиме pro ситуация повторяется, кроме интересной аномалии при большом количестве потоков. Там производительность версии 2.8 становится совсем грустной, и с этим надо отдельно разбираться.
Планы разработки
Глобально можно выделить четыре шага:
релиз pg_probackup 3;
подготовка open source версии;
оптимизация многопоточности;
реализация всего задуманного для pg_probackup 3.1.
Вот так всё просто. Но если есть предложения, вы обязательно предлагайте.