Снимки или дельты: цена восстановления состояния на вчера
Рано или поздно в любой системе, где данные меняются, кто-то задаёт вопрос: «а можно посмотреть, как это выглядело вчера в 15:00?» Иногда это бухгалтер, которому нужен баланс на конец квартала, иногда служба поддержки, которой нужно понять, что видел клиент до правки менеджера, иногда — сам разработчик, который откатывает баг. Ответ на этот вопрос зависит не от того, насколько система «продвинутая», а от одного архитектурного решения, принятого заранее: как именно вы храните историю состояния — целиком или по кусочкам. Разберём оба подхода без иллюзий: у каждого есть цена, и платить её придётся либо местом на диске, либо временем и сложностью восстановления.
Содержание
Что значит «восстановить состояние на вчера»
Задача точнее, чем кажется на первый взгляд. Есть разница между тремя разными вещами, которые часто путают:
- Текущее состояние — то, что лежит в таблицах прямо сейчас. Его хранить не нужно отдельно, оно и так есть.
- Полная история изменений — список всех событий, которые когда-либо происходили с записью: кто, что и когда поменял.
- Состояние на конкретный момент времени (point-in-time state) — реконструированная картина того, как выглядела запись (или вся база) в 15:00 вчера, а не список изменений.
Третий пункт — это то, что обычно нужно на практике: не «покажи мне все события», а «покажи мне, как это было». И вот тут выбор способа хранения истории начинает напрямую определять, сколько это будет стоить в дисках и сколько — в CPU и времени на реконструкцию. Есть два базовых подхода, и почти все реальные системы — от pg_dump до event sourcing — это вариации одного из них или их комбинация.
Снимки: цена простоты
Снимок (snapshot) — это полная копия состояния на момент времени T. Простейший пример: раз в сутки в 03:00 вы копируете всю таблицу orders в orders_snapshot_20260827 или сохраняете дамп базы целиком.
Плюсы понятны сразу:
- Восстановление конкретной точки — это чтение одного объекта. Никакой логики применения, никакой цепочки.
- Скорость чтения предсказуема и не зависит от того, сколько времени прошло с последнего снимка.
- Модель проста для понимания даже не-инженерами: «вот файл на вчера, вот файл на позавчера».
Минус тоже один, но он растёт быстрее, чем кажется на старте. Каждый снимок хранит всё, даже те 99% данных, которые не изменились с прошлого раза. Если у вас таблица на 50 ГБ и вы снимаете полный снимок каждый час, за сутки это 1,2 ТБ — притом что реально за час меняется, условно, несколько мегабайт.
Практический пример на PostgreSQL — наивная схема снимков через pg_dump:
# Каждый час, cron
0 * * * * pg_dump -Fc -d orders_db -f /snapshots/orders_$(date +\%Y\%m\%d_\%H).dump
Через месяц в каталоге /snapshots будет 720 файлов, и если каждый весит хотя бы гигабайт — это уже 720 ГБ ради базы, которая физически занимает 1 ГБ. Именно об этой экономике подробно с разбором IOPS и стоимости хранения написано в статье про то, сколько снапшотов держать и во что обходится каждый — механика та же самая что для файловых снимков ZFS/LVM, что для прикладных дампов состояния: цена растёт линейно с частотой и временем хранения.
Снимки хорошо работают, когда: точек восстановления нужно немного (раз в сутки, раз в неделю), данные не гигантские, а восстановление должно быть максимально надёжным и простым — в 3 часа ночи, когда всё горит, никто не хочет разбираться в цепочке из сорока файлов.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверДельты: цена компактности
Дельта (delta, diff) — это не полное состояние, а только то, что изменилось с прошлого раза. Практически это выглядит как журнал событий:
CREATE TABLE orders_history (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL,
changed_at timestamptz NOT NULL DEFAULT now(),
operation text NOT NULL CHECK (operation IN ('insert', 'update', 'delete')),
changed_fields jsonb NOT NULL -- только то, что реально изменилось
);
При каждом UPDATE orders SET status = 'shipped' WHERE id = 42 триггер или приложение пишет одну строку: {"status": {"old": "paid", "new": "shipped"}}. Если менялось только одно поле — весит эта запись десятки байт, а не килобайты полной копии строки.
Плюс очевиден: место. Если за час в таблице реально меняется 0,1% строк, дельта за час весит в тысячу раз меньше полного снимка. Именно этот принцип — «копируем не всё, а только изменения» — лежит в основе инкрементальных бэкапов, и если хочется увидеть разницу в конкретных гигабайтах на числовом примере, это подробно разобрано в статье про разницу между полным, инкрементальным и дифференциальным бэкапом.
Минус — в цене восстановления. Чтобы узнать, как выглядела запись order_id = 42 вчера в 15:00, недостаточно прочитать одну дельту. Нужно:
- Найти последнее известное полное состояние (либо самая первая запись
insert, либо какой-то опорный снимок). - Последовательно применить все дельты от этой точки до момента 15:00 вчера, в правильном порядке.
- Только после этого получить итоговое состояние.
Если история копится годами и снимков вообще нет, это означает применение тысяч записей ради одного SELECT. И это не только вопрос скорости — это вопрос устойчивости: если хотя бы одна дельта в цепочке повреждена, потеряна или применена не в том порядке, всё состояние после неё оказывается недостоверным. Ровно тот же класс проблем, что и с инкрементальными бэкапами — длинная цепочка зависимостей означает, что слабое звено ломает всё, что после него.
Снимки против дельт: сравнение в одной таблице
| Критерий | Снимки (full state) | Дельты (diff/changelog) |
|---|---|---|
| Место на диске | Растёт линейно с частотой снимков × размер базы | Растёт с объёмом реальных изменений, обычно на порядки меньше |
| Восстановление точки | O(1) — читаем один объект | O(N) — применяем N дельт от опорной точки |
| Скорость восстановления недавнего состояния | Постоянная | Быстрая, если дельт немного |
| Скорость восстановления далёкого состояния | Такая же, как для недавнего | Деградирует линейно с числом дельт |
| Устойчивость к повреждению одного элемента | Теряется только этот снимок | Теряется вся цепочка после повреждённого звена |
| Сложность реализации | Низкая (дамп/копия) | Выше (нужен консистентный журнал и логика воспроизведения) |
| Годится для | Редких точек, критичных к простоте восстановления | Частых точек, где место дороже времени восстановления |
Ни один из подходов не «лучше» в вакууме — это классический компромисс место/время, знакомый по любой структуре данных с индексами и без. Вопрос в том, какую цену ваша система может себе позволить платить чаще: место на диске каждый час или CPU-время при восстановлении раз в квартал.
Гибрид: снимки плюс дельты между ними
Большинство реальных систем не выбирают один подход, а комбинируют оба: периодический полный снимок как опорная точка плюс дельты между снимками. Это снижает и объём хранения, и длину цепочки восстановления одновременно.
Схема простая: раз в сутки — полный снимок, всё остальное время — только дельты. Чтобы восстановить состояние на любой момент внутри суток, берём ближайший предыдущий снимок и докатываем дельты только до нужной точки — цепочка не длиннее суток, а не месяцев.
Три реальных примера этой схемы, которые вы уже, скорее всего, используете, даже не называя их так:
PostgreSQL: базовый бэкап + WAL. pg_basebackup снимает полную копию кластера (снимок), а после этого PostgreSQL непрерывно пишет журнал предзаписи (WAL) — по сути, поток дельт на уровне физических страниц. Point-in-time recovery — это restore базового снимка плюс проигрывание WAL-файлов до нужной секунды через recovery_target_time. Почему база вообще ведёт этот журнал и как он устроен изнутри — отдельная механика, разобранная в статье о том, что такое WAL и зачем писать всё дважды. С точки зрения темы этой статьи важно одно: WAL — это дельты, pg_basebackup — это снимок, а PITR — это гибрид в чистом виде.
MongoDB: снимок + oplog. Тот же принцип: mongodump даёт полное состояние на момент запуска, а oplog (операционный журнал репликации) — поток дельт, которые можно проиграть поверх снимка до нужной точки.
Event sourcing на уровне приложения. Если вы храните не текущее состояние сущности, а поток событий (OrderCreated, StatusChanged, ItemAdded), классическая проблема — агрегат с историей в десятки тысяч событий пересобирается на каждый запрос слишком долго. Решение то же самое: периодически материализовать снимок агрегата (например, каждые 100 событий или раз в сутки) и при чтении брать последний снимок плюс события после него:
def rebuild_state(order_id, as_of):
snapshot = get_latest_snapshot(order_id, before=as_of)
events = get_events(order_id, after=snapshot.event_id, before=as_of)
state = snapshot.state
for event in events:
state = apply(state, event)
return state
Это ровно та же арифметика, что и в примере с PostgreSQL, просто на уровне прикладного кода, а не движка СУБД.
Как выбрать интервал снимков на практике
Универсальной цифры нет — она зависит от того, сколько у вас реально меняется данных и как часто нужна точность восстановления. Но есть рабочая эвристика, от которой можно оттолкнуться.
Прикинуть баланс можно по простой модели: пусть S — размер полного снимка, d — средний размер одной дельты, n — число дельт между снимками. Тогда объём хранения на один период — S + n·d, а длина цепочки восстановления худшего случая — n применений. Задача — выбрать интервал снимков так, чтобы n·d не превышало сам снимок в несколько раз (иначе смысл дельт теряется) и чтобы n не превышало число операций, которое ваша система применяет за разумное время (это уже зависит от вашей реализации — у кого-то это сотни в секунду, у кого-то тысячи, тут нужно измерять на своих данных, а не полагаться на чужие цифры).
Практические ориентиры, от которых можно отталкиваться и корректировать под свою нагрузку:
- Если данные меняются медленно (справочники, конфигурация, редко редактируемые карточки) — снимок раз в неделю, дельты покрывают всё между ними.
- Если данные меняются активно (заказы, транзакции, статусы) — снимок раз в сутки или даже чаще, чтобы цепочка дельт не разрасталась.
- Если нужна точность до минуты для аудита или комплаенса — снимок как «дешёвая перезагрузка» раз в сутки плюс полный журнал событий (дельт) без сокращения, потому что здесь ценность в полноте, а не в скорости чтения.
- Если восстановление состояния — редкая операция (раз в квартал, для отчётности) — экономьте на снимках, храните в основном дельты, время восстановления в этом случае не критично.
Отдельный практический момент: снимок сам по себе не отменяет необходимости в обычном резервном копировании — это разные задачи. Снимок состояния данных отвечает на вопрос «как это выглядело тогда», а бэкап — на вопрос «что делать, если сервер и диск исчезли физически». Как восстанавливать базу из бэкапа на практике, с реальными командами и типичными граблями, разобрано в статье про восстановление базы данных из бэкапа — если у вас пока нет ни того, ни другого, начинать стоит именно оттуда, а историю состояния добавлять поверх.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли вообще не делать снимки и хранить только дельты с самого начала?
Технически можно, но каждое восстановление тогда требует применения всей истории с нуля, и со временем это становится всё медленнее. Это работает, только пока объём данных и история малы; как только счёт идёт на месяцы работы системы, отсутствие опорных снимков превращается в постоянно растущую цену восстановления.
Что дешевле — снимки или дельты — если считать в деньгах за хранение?
Дельты почти всегда компактнее в моменте, но чистое сравнение места без учёта частоты снимков некорректно: снимок раз в год может занимать меньше места, чем дельты, пишущиеся каждую секунду годами без единого опорного снимка. Сравнивайте не сами подходы, а конкретную схему с конкретными интервалами.
Как понять, что дельт накопилось слишком много и пора делать новый снимок?
Практический сигнал — когда время восстановления состояния (не абстрактное, а измеренное на реальном прогоне) начинает превышать приемлемый порог для вашего сценария использования. Если этот порог не задан заранее, легко не заметить момент, когда цепочка уже неприемлемо длинная.
Нужен ли гибридный подход для маленького проекта, где данные меняются редко?
Часто нет — если правок в сутки единицы, полных снимков раз в день вполне достаточно, а сложность дельт не окупается. Гибрид оправдан, когда объём изменений уже заметен на счётчиках дискового пространства или в скорости отчётов.
Что делать, если дельта в середине цепочки повредилась?
Всё состояние после неё становится недостоверным, поэтому для дельт критична проверка целостности при записи (контрольные суммы, транзакционность записи в журнал) и хранение снимков достаточно часто, чтобы повреждённый участок не откатывал восстановление на недели назад.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →