
Несколько лет назад я трудился в проекте, где основной биллинг работал на Oracle. Однажды коллега захотел поправить тестовые начисления в таблице abon_charges и выполнил такой запрос:
UPDATE abon_charges
SET amount = 0
WHERE service_id = 123 AND v_abon_id = v_abon_id;
На первый взгляд — ничего страшного. Но v_abon_id = v_abon_id истинно для любой строки. Oracle это не игнорирует. Условие становится:
WHERE service_id = 123 AND TRUE
Так запрос обнулил абсолютно все суммы для service_id=123 за десятки месяцев. В таблице было около 1,8 млн строк по этой услуге.
С такой неприятностью в Oracle может помочь механизм Oracle Flashback. Вкратце: находим проблемную транзакцию, в отдельной сессии включаем чтение таблицы на момент до обновления, снимаем копию в отдельную таблицу и отдаём её нашему виновнику для решения проблемы :).
Мы починили всё без простоя и полного восстановления всего кластера. С тех пор мне всегда хотелось иметь такой «точечный флэшбэк» и в PostgreSQL. Особенно в системах, где восстановление базы на несколько терабайтов может занимать часы. И вот недавно мне довелось организовать такое решение в нашем продукте Platform V CopyWala. Это инструмент для бэкапа от СберТеха, который работает с PostgreSQL. Покажу, как всё устроено.
Как работает FUSE и зачем он нужен
FUSE (Filesystem in Userspace) — механизм Linux, который позволяет реализовывать файловую систему в пользовательском пространстве. Программе передаются системные вызовы (open, read, readdir и т. д.) и она сама решает, что отдавать. Таким образом ОС кажется, что она работает с настоящим каталогом с данными.
Почему это интересно для PostgreSQL
FUSE-драйвер может смонтировать его как виртуальный PGDATA. Тогда можно запустить PostgreSQL так:
pg_ctl start -D <fuse-directory>
И PostgreSQL честно подумает, что работает на локальной файловой системе, хотя в реальности читает архив.
В нашем продукте для резервного копирования и восстановления данных Platform V CopyWala мы добавили драйвер CopyWala Fuse. Он умеет монтировать в произвольный каталог наш собственный формат архива и предоставлять ту самую виртуальную PGDATA, с которой может работать экземпляр PostgreSQL. Покажу, как воспроизвести псевдо-flashback в PostgreSQL с помощью postgres_fdw и Copywala Fuse.
Шаг 1. Поднимаем окружение
Нам понадобится два контейнера для эмуляции аварии. Один мастер (для примера используем контейнер, но вы можете использовать свой экземпляр) и одна реплика, которую мы поднимем с помощью Copywala Fuse. Структура проекта будет выглядеть следующим образом:
project/
├─ docker-compose.yml
└─ backups_dir/
├─ copywala // бинарь копивалы (не ниже v.1.2.0)
├─ config.yaml // конфиг для запуска копивалы
└─ test-agent/backups/<backup>.cwl // архив, сделанный на шаге 3
docker-compose.yml:
version: '3.9'
services:
master_ct:
image: postgres:16
container_name: master_ct
environment:
POSTGRES_USER: cwdbuser
POSTGRES_PASSWORD: cwdbpass
POSTGRES_DB: cwdb
ports:
- "5432:5432"
volumes:
- "./backups_dir:/backups_dir"
replica_ct:
image: postgres:16
container_name: replica_ct
environment:
POSTGRES_USER: cwdbuser
POSTGRES_PASSWORD: cwdbpass
POSTGRES_DB: cwdb
ports:
- "5433:5432"
volumes:
- "./backups_dir:/backups_dir"
Шаг 2. Наполняем мастер и портим данные для теста
docker compose up -d
docker exec -it master_ct pgbench -i -s 10 -U cwdbuser -d cwdb
pgbench — это замечательная утилита для нагрузочного тестирования и наполнения данных в БД. Она доступна «из коробки», так что проблем с ней быть не должно.
После генерации у вас появится следующая структура таблиц:
cwdb=# \dt
List of tables
Schema | Name | Type | Owner
--------+------------------+-------+----------
public | pgbench_accounts | table | cwdbuser
public | pgbench_branches | table | cwdbuser
public | pgbench_history | table | cwdbuser
public | pgbench_tellers | table | cwdbuser
(4 rows)
Снимаем эталонную checksum:
SELECT md5(string_agg(md5(t::text), ',' ORDER BY aid))
FROM pgbench_accounts t;
md5
----------------------------------
65061aa9af380081c3d77f665f90808b
(1 row)
Она нам понадобится в будущем.
Создаём резервную копию:
docker exec -it master_ct /backups_dir/copywala init test-agent -c \
/backups_dir/config.yaml
docker exec -it master_ct /backups_dir/copywala create-backup -c \
/backups_dir/config.yaml
Портим данные:
DELETE FROM pgbench_accounts
WHERE aid IN (
SELECT aid FROM pgbench_accounts TABLESAMPLE SYSTEM(1);
);
Шаг 3. Монтируем архив через FUSE и запускаем PostgreSQL без restore
В контейнере replica_ct:
docker exec -it replica_ct bash
su - postgres
mkdir -p /fuse_pg_data # директория в которую будем монтировать нашу резервную копию
pg_ctl stop
Монтируем архив, созданный на втором шаге, и запускаем экземпляр применительно к смонтированной директории:
/backups_dir/copywala fuse /fuse_pg_data /backups_dir/test-agent/backups/<backup>.cwl &
pg_ctl start -D /fuse_pg_data
После успешного запуска видим в журнале:
INF start fuse mounting mount_path=/fuse_pg_data cache_swap_size=268435456 cache_dir=/tmp
Здесь стоит рассказать немного про параметры запуска команды copywala fuse. cache_swap_size — это размер буфера, в который пишутся данные при работе со смонтированной директорией. При переполнении буфера данные будут сбрасываться на диск в директорию, указанную в cache_dir.
Экземпляр готов, подчеркну, без restore, напрямую из архива. Теперь в смонтированной директории мы видим следующее:

Что происходит внутри
Когда мы вызываем ls в корне смонтированной директории, СopyWala Fuse получает syscall = getdents(readdir), а обработчик идёт в мета-информацию архива, читая только корневую структуру директорий и файлов, и приводит их в необходимый формат текущей ОС для вывода.
Важно! Мы читаем не сами файлы, а только их мета-информацию, благодаря чему можем быстро пройтись по древовидной структуре и получить доступ к любому файлу почти мгновенно.
Разберём другой случай, когда нам нужно получить данные какого-либо файла.

При попытке чтения мы также получаем мета-информацию о местоположении файла в архиве и запрашиваем не весь файл, а только те части (срез offset + size), которые были запрошены на стороне клиента.
Шаг 4. «Флэшбэк» через postgres_fdw
Переходим на мастер и подключаемся к таблице, развёрнутой на нашей Fuse-реплике:
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE SERVER replica_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'replica_ct', port '5432', dbname 'cwdb');
CREATE USER MAPPING FOR cwdbuser
SERVER replica_server
OPTIONS (user 'cwdbuser', password 'cwdbpass');
CREATE SCHEMA IF NOT EXISTS replica_import; # добавляем таблицу во временную схему для исключения конфликта имён
IMPORT FOREIGN SCHEMA public
LIMIT TO (pgbench_accounts)
FROM SERVER replica_server
INTO replica_import;
ALTER FOREIGN TABLE replica_import.pgbench_accounts
RENAME TO pgbench_accounts_replica;
Сравниваем количество строк:
SELECT count(*) FROM pgbench_accounts; -- 999900
SELECT count(*) FROM pgbench_accounts_replica; -- 1000000
Восстанавливаем повреждённые строки:
INSERT INTO pgbench_accounts
SELECT r.*
FROM pgbench_accounts_replica r
LEFT JOIN pgbench_accounts m USING (aid)
WHERE m.aid IS NULL;
Проверяем checksum — должна совпасть с эталонной.
SELECT md5(string_agg(md5(t::text), ',' ORDER BY aid)) FROM pgbench_accounts t;
md5
----------------------------------
65061aa9af380081c3d77f665f90808b
(1 row)
Что ещё можно делать с FUSE в PostgreSQL?
Использование FUSE как «виртуального PGDATA» открывает массу возможностей.
-
Гранулярное восстановление через pg_dump. Можно вытащить одну таблицу:
pg_dump -t orders_replica > orders.sqlи вернуть её в боевую базу.
Экспресс-аудит любых исторических копий. Смонтировать архив → открыть psql → посмотреть состояние данных.
Исследование резервной копии. Проанализировать реальное состояние кластера на момент бэкапа, без restore.
Тестирование миграций на реальных данных. Поднимаем Postgres из архива → гоняем миграции → не портим боевой кластер.
Снижение RTO для локальных и dev-сред.Не нужно поднимать тяжёлые стенды.
Спасибо за внимание! Готов ответить на вопросы и послушать ваши мнения о том, где ещё может быть полезен Fuse. Больше о том, как мы развиваем инструмент, пишем в нашем сообществе про базы данных — приходите. И прошу в комментарии!
P. S. Выражаю особую благодарность своему тимлиду Михаилу Левицкому за консультацию при написании статьи.
Комментарии (8)

bigtrot
24.12.2025 07:46Насколько я понял из статьи, что архив создается после порчи данных. Если это так, то тогда не понятно, как получить данные до их порчи.

polkiloo
24.12.2025 07:46Спасибо за находку, действительно ошибка в тексте - архив создается до порчи данных.

bigtrot
24.12.2025 07:46В чем преимущество Вашего подхода по сравнению с восстановление на заданный момент времени штатным подходом и после этого восстановить данные?

polkiloo
24.12.2025 07:46Значительное преимущество по времени восстановления на конкретную точку будет при следующих условиях:
Размер базовой резервной копии > 1 Tb
Суммарный размер WAL сегментов для консистетного восстановления < 50 Gb - точка во времени близка к базовой копии.
В таком случае время проигрывания WAL незначительно отличается, а для дампа отдельной БД на 10Gb не нужно поднимать кластер более 1 Tb
Для таких операций можно выделить специальную recovery area с быстрыми nvme дисками малого размера.
Sleuthhound
Кажеться, что слишком много телодвижений + бэкап должен быть в таком формате чтобы максимально быстро можно было на нем запустить пг и тут возникает вопрос: А если за сутки у вас архивируется 100Tb wal файлов и инкрементальный бэкап пусть даже 1 раз в сутки, то скажем востановиться на 23:47:12 это нужно накатить примерно 50Tb валов до нужной точки - сколько Вы будите накатывать их? Как решается такая проблема?
polkiloo
Это отличный вопрос.
Действительно это серьезное ограничение для использования данной технологии в таких масштабах.
Однако, у нас есть несколько идей как можно сделать поднятие временного экземпляра существенно быстрее - в следующих статьях расскажем про наши исследования.
Sleuthhound
Кажеться, что отстающая реплика на N часов более интересное решение, хотя и более накладное с точки зрения потребления ресурсов.
polkiloo
Это отличное решение, проверенное временем.
Однако, используя экземпляр поднятый на резервной копии, можно заглянуть в прошлое сильно больше чем на час.
Заглянуть можно на столько, насколько позволяет глубина хранения РК - например можно вытащить данные по состоянию на несколько лет назад.
Кроме того, данная технология не ограничивается в сценариях использования только flashback восстановлением.