Симптом сложнее, чем «архив отсутствует»: файл найден, checksum совпал, а восстановление всё равно останавливается на роли, зависимости или неожиданно пустой таблице. Цена ошибки в том, что команда принимает целостность одного файла за готовность всей системы. В момент сбоя это рождает ложный выбор: либо давить restore в неизвестной цели, либо вручную собирать недостающие части, не понимая, какая из них является источником истины.
Для октября 2020 года достаточно более приземлённой модели. Не строим SRE-платформу и не называем учебные отметки реальными RPO/RTO. Вместо этого формируем backup contract: одна заявленная область, один archive, один manifest, один безопасный путь restore и набор маленьких доказательств. Contract не отменяет потребность в политиках хранения, правах и резервировании, но он делает видимой границу между «команда выполнилась» и «мы проверили, что этот результат можно использовать по назначению».
Четыре утверждения, которые нельзя склеивать
Слово «копия готова» обычно скрывает сразу несколько разных утверждений. Первое: процесс прочитал согласованный срез данных. Второе: получившийся файл сохранился и не изменился. Третье: инструмент способен разобрать его формат. Четвёртое: после restore целевой набор объектов отвечает ожидаемому контракту. Ошибка возникает, когда успешная проверка второго шага автоматически засчитывается как четвёртый. Hash умеет сравнить байты; он не знает, что в архиве отсутствует роль, что выбранная схема зависит от другой схемы или что приложение ждёт внешний файл.
| Доказательство | Что оно подтверждает | Что не подтверждает | Следующее действие |
|---|---|---|---|
Успешный pg_dump | инструмент завершил выбранный logical dump | полноту cluster-wide объектов и будущий restore | записать scope, stderr и формат в manifest |
| SHA-256 совпал | проверяемые байты archive не изменились | семантику данных и совместимость цели | прочитать archive и подготовить isolated candidate |
pg_restore --list | archive читается и содержит перечисленные элементы | что все зависимости смогут работать после 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 должен зафиксировать внешнюю предпосылку. «Мы всегда так делали» не является ни зависимостью, ни проверкой.
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 не заявлял.
| Проверка | Симптом, который ловит | Причина, которую отделяет | Безопасное действие |
|---|---|---|---|
| Relation существует | archive развернулся, но нужной таблицы нет | ошибка scope или другой archive | сверить manifest и список archive |
| Версия схемы | таблица есть, но форма не та | несовместимый format или не тот релиз схемы | остановить ввод данных и уточнить инструмент/версию |
| Синтетический row count | объекты созданы, но набор пуст или урезан | неполный dump либо неверный критерий | сравнить с manifest, не с памятью оператора |
| Флаг isolated target | команда направлена не туда | ошибка runbook до restore | не выполнять restore, создать отдельного кандидата |
Нумерованный маршрут диагностики
- Прочитать manifest и назвать требуемый scope. Если он не содержит нужный объект или внешнюю предпосылку, не пытаться компенсировать это командой restore.
- Сравнить SHA-256 archive с manifest. Любое расхождение означает остановку до инструмента восстановления.
- Построить list archive и сверить ожидаемые relations и format. Несовпадение — повод разбирать creation path, а не менять target.
- Подтвердить isolated candidate и запрет перезаписи источника. Если цель не доказана, drill считается не начавшимся.
- Выполнить restore в кандидате выбранной версией инструмента и сохранить stderr как evidence, не трактуя его отсутствие как полный успех.
- Запустить безопасные 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 сверяет байты артефакта, но сам по себе не доказывает пригодность результата к восстановлению