DarkRiDDeR15 мин

Контракт резервной копии: scope, manifest и проверяемый restore

ДанныеНадёжностьАрхитектура

Симптом сложнее, чем «архив отсутствует»: файл найден, checksum совпал, а восстановление всё равно останавливается на роли, зависимости или неожиданно пустой таблице. Цена ошибки в том, что команда принимает целостность одного файла за готовность всей системы. В момент сбоя это рождает ложный выбор: либо давить restore в неизвестной цели, либо вручную собирать недостающие части, не понимая, какая из них является источником истины.

Для октября 2020 года достаточно более приземлённой модели. Не строим SRE-платформу и не называем учебные отметки реальными RPO/RTO. Вместо этого формируем backup contract: одна заявленная область, один archive, один manifest, один безопасный путь restore и набор маленьких доказательств. Contract не отменяет потребность в политиках хранения, правах и резервировании, но он делает видимой границу между «команда выполнилась» и «мы проверили, что этот результат можно использовать по назначению».

Четыре утверждения, которые нельзя склеивать

Слово «копия готова» обычно скрывает сразу несколько разных утверждений. Первое: процесс прочитал согласованный срез данных. Второе: получившийся файл сохранился и не изменился. Третье: инструмент способен разобрать его формат. Четвёртое: после restore целевой набор объектов отвечает ожидаемому контракту. Ошибка возникает, когда успешная проверка второго шага автоматически засчитывается как четвёртый. Hash умеет сравнить байты; он не знает, что в архиве отсутствует роль, что выбранная схема зависит от другой схемы или что приложение ждёт внешний файл.

Разные доказательства в backup/restore contract
ДоказательствоЧто оно подтверждаетЧто не подтверждаетСледующее действие
Успешный pg_dumpинструмент завершил выбранный logical dumpполноту cluster-wide объектов и будущий restoreзаписать scope, stderr и формат в manifest
SHA-256 совпалпроверяемые байты archive не изменилисьсемантику данных и совместимость целипрочитать archive и подготовить isolated candidate
pg_restore --listarchive читается и содержит перечисленные элементычто все зависимости смогут работать после restoreсверить list с manifest, а не только с именем файла
Restore в кандидатевыбранный инструмент смог развернуть archive в отдельной целичто приложение готово обслуживать запросывыполнить структурные и безопасные смысловые checks
Checks после restoreзаявленный минимум objects и данных полученчто покрыт любой disaster-сценарийзафиксировать пробелы и спланировать следующий drill

Таблица полезна именно тем, что в каждой строке оставляет ограничение. Если check не умеет ответить на вопрос, его не нужно нагружать чужой ролью. Например, hash ценен, потому что он останавливает работу с изменённым archive до restore. А row count ценен, потому что ловит пустой или неожиданно урезанный учебный набор. Они не конкурируют; они стоят на разных границах и должны оставаться отдельными в runbook и в журнале проверки.

Scope начинается с того, что не попало в archive

PostgreSQL 12 прямо разделяет области. pg_dump делает dump одной базы, а cluster-wide объекты, такие как роли и tablespaces, требуют отдельного рассмотрения через pg_dumpall. Это не мелкая деталь командной строки: если restore нуждается в роли, а manifest её не упомянул, archive одной базы не становится «почти полным» от хорошего настроения. В учебном примере роли и tablespaces явно исключены, поэтому drill не может нечаянно зачесть их как восстановленные.

Выбор схемы тоже имеет цену. Параметр --schema удобен, когда цель действительно ограничена одной областью, но документация предупреждает, что зависимости выбранной схемы могут не попасть в чистую цель. Поэтому перед первым drill полезнее начать не с оптимизации маленького dump, а с вопроса: сможет ли target существовать без объектов вне списка? Если ответ не доказан, include-list должен расшириться либо manifest должен зафиксировать внешнюю предпосылку. «Мы всегда так делали» не является ни зависимостью, ни проверкой.

Вертикальная схема restore path: учебный scope становится logical archive, checksum сравнивает байты с manifest, оглавление archive сверяется с ожидаемыми relations, затем isolated candidate получает restore и проходит структурные checks; каждая граница может остановить маршрут
Restore не начинается с самой тяжёлой команды. Сначала contract исключает неверный scope и изменённые байты; после команды остаются отдельные проверки структуры и данных.

Manifest — это договор между созданием и восстановлением

Manifest не обязан быть большим каталогом всего хранилища. Для одной учебной копии достаточно стабильной формы: версия manifest, backup ID, file и format, размер и SHA-256, includes/excludes, список expected relations, безопасные checks и правило цели. Важно, чтобы поле называло наблюдаемое значение, а не эмоцию. Вместо "complete": true лучше хранить конкретное scope.includes и restoreChecks.rows. Тогда следующий читатель может повторить проверку или оспорить её границу, не пытаясь расшифровать одно булево поле.

{
  "manifestVersion": 1,
  "backupId": "training-backup-2020-10-a",
  "artifact": {
    "file": "training-catalog-2020-10.dump",
    "format": "pg_dump custom (-Fc)",
    "sha256": "здесь хранится вычисленный hash",
  },
  "scope": {
    "includes": ["schema:catalog", "schema:reference"],
    "excludes": ["cluster roles", "tablespaces", "external files"],
  },
  "restoreChecks": {
    "relations": ["catalog.items", "reference.codes"],
    "rows": { "catalog.items": 3, "reference.codes": 2 },
  },
  "safety": "restore only into an isolated training candidate"
}

Два свойства manifest особенно легко перепутать. artifact.sha256 относится к уже записанному набору байтов; если bytes изменились, restore не должен продолжаться. restoreChecks относятся к тому, что появилось после развертывания; они не вычисляются из hash и не могут быть подставлены до restore. Хранить их в одном документе удобно, но смысл у них разный. Первое — gate целостности, второе — ожидаемое наблюдение о результате. Разделение делает диагностику быстрее и не даёт чинить не ту причину.

Checksum не превращает файл в проверенный backup

SHA-256 нужен, когда причина симптома может находиться между созданием archive и его чтением: частично записанный файл, неверная передача, путаница имени или повреждение хранения. При несовпадении hash действие простое и безопасное — archive нельзя использовать, надо воспроизвести получение доверенного артефакта. Но checksum не видит SQL-содержимое как бизнес-правило. Два одинаково целых файла могут быть одинаково непригодны, если оба созданы с узким scope или оба не содержат нужной внешней части.

Поэтому fixture ниже намеренно проверяет две разные поломки. Изменение хотя бы одного байта меняет digest и запрещает попытку restore. Сужение includes с двух учебных схем до одной оставляет bytes нетронутыми, но нарушает contract и тоже останавливает маршрут. Это не эмуляция PostgreSQL и не тест backup-инфраструктуры. Это детерминированная модель, которая проверяет, что наш код и статья не называют «checksum совпал» полным ответом.

import { verifyFixture } from "./scripts/upgrade-2020-10.mjs";

const result = verifyFixture();
console.log(result.checks);
// Все проверки true только для исходных учебных bytes и полного scope.
// Фикстура не открывает файл, сеть или PostgreSQL.

Restore path: сначала остановки, затем доказательства

После manifest путь должен быть линейным. На входе есть archive и versioned contract. Первый gate сравнивает checksum. Второй читает содержание archive и сопоставляет его со scope. Третий создаёт изолированного кандидата. Четвёртый запускает restore выбранным инструментом. Пятый читает результат безопасными запросами. Сложность здесь не в количестве шагов, а в том, что каждый хранит свою причину остановки. Если archive не читается, не нужно обсуждать row count; если target не изолирован, не нужно запускать команду «чтобы посмотреть».

Для non-plain форматов PostgreSQL использует pg_restore; custom и directory archives позволяют просматривать и выбирать элементы. Это полезно как диагностический инструмент, но выборочная реконструкция не должна случайно стать новым scope. Если в manifest записано «две схемы вместе», а команда restore выбирает только одну relation ради скорости, результат уже не проверяет исходный contract. Оптимизация допустима только после того, как новый scope и его checks явно записаны отдельной версией manifest.

Смысловые checks должны быть безопасными

После restore нельзя ограничиваться тем, что процесс вернул код 0, но и не нужно включать опасную бизнес-операцию. Для учебной базы достаточно трёх слоёв: relation существует; схема имеет ожидаемую версию или маркер; маленький набор синтетических строк даёт ожидаемый счётчик. В рабочем проекте можно добавить проверку reference-данных или чтение агрегата без персональных значений. Ключевое условие: check не должен менять источник, писать во внешнюю систему и зависеть от ресурса, которого archive не заявлял.

Учебные checks после restore и их границы
ПроверкаСимптом, который ловитПричина, которую отделяетБезопасное действие
Relation существуетarchive развернулся, но нужной таблицы нетошибка scope или другой archiveсверить manifest и список archive
Версия схемытаблица есть, но форма не танесовместимый format или не тот релиз схемыостановить ввод данных и уточнить инструмент/версию
Синтетический row countобъекты созданы, но набор пуст или урезаннеполный dump либо неверный критерийсравнить с manifest, не с памятью оператора
Флаг isolated targetкоманда направлена не тудаошибка runbook до restoreне выполнять restore, создать отдельного кандидата

Нумерованный маршрут диагностики

  1. Прочитать manifest и назвать требуемый scope. Если он не содержит нужный объект или внешнюю предпосылку, не пытаться компенсировать это командой restore.
  2. Сравнить SHA-256 archive с manifest. Любое расхождение означает остановку до инструмента восстановления.
  3. Построить list archive и сверить ожидаемые relations и format. Несовпадение — повод разбирать creation path, а не менять target.
  4. Подтвердить isolated candidate и запрет перезаписи источника. Если цель не доказана, drill считается не начавшимся.
  5. Выполнить restore в кандидате выбранной версией инструмента и сохранить stderr как evidence, не трактуя его отсутствие как полный успех.
  6. Запустить безопасные structural and semantic checks, записать verdict и отдельно перечислить области вне scope: роли, tablespaces, файлы, сроки и настоящий disaster recovery.

Где заканчивается контракт

Backup contract не заменяет политику хранения, доступы к хранилищу, криптографическую защиту, мониторинг или план переключения. Он не даёт право называть практику устойчивой, пока нет конкретного разрешённого стенда и измерений. Но контракт полезен раньше этих зрелых слоёв: он делает видимыми inputs, ожидаемый output и границу доказательства. Для автора уровня М3 это честный следующий шаг — не «мы готовы к любой аварии», а «мы умеем проверить один путь и знаем, что ещё не включили».

Имена, данные, timestamps, размеры, команды и результаты ниже учебные. Здесь нет реальной базы, access key, production-окружения или отчёта о disaster recovery.

Проверяемые источники

  • PostgreSQL 12: Backup and Restore — разделяет SQL dump, копирование на уровне файловой системы и continuous archiving; у способов разные предпосылки и границы восстановления
  • PostgreSQL 12: pg_dump — документирует согласованный логический dump, форматы custom и directory, а также ограничение выборки схемы или таблицы без зависимостей
  • PostgreSQL 12: pg_restore — описывает восстановление non-plain archive, просмотр содержания и параметры, которые нужно сверять с версией выбранного инструмента
  • PostgreSQL 12: pg_dumpall — показывает отдельную область cluster-wide объектов, включая роли; один dump базы не следует выдавать за копию всего кластера
  • NIST FIPS 180-4: Secure Hash Standard — задаёт семейство SHA-2; hash сверяет байты артефакта, но сам по себе не доказывает пригодность результата к восстановлению