8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 259,
|
||
"slug": "editorial-2020-10-field-backup-recovery",
|
||
"title": "Резервная копия не равна восстановлению: как проверить backup без риска для источника",
|
||
"excerpt": "Архив можно увидеть и скачать, но это ещё не доказательство восстановления. Разбираем manifest, checksum, область данных, изолированную цель и проверку результата на учебном PostgreSQL-сценарии.",
|
||
"contentHtml": "<p>Симптом знакомый: в хранилище лежит свежий архив, команда видит его размер и дату, но никто не может уверенно сказать, что произойдёт после <code>restore</code>. Неясно, какие схемы попали в файл, совпадают ли его байты с теми, что указаны в журнале, куда пойдёт восстановление и чем считать результат успешным.</p>\n<p>Цена ошибки проявляется в аварии. Оператор выбирает архив по имени, запускает привычную команду и обнаруживает неполный набор таблиц. Или восстанавливает данные поверх источника. В этот момент резервная копия не помогает: она превращается в ещё один объект, которому нужно доверять без проверки.</p>\n<p>Тезис статьи простой: backup готов к применению только тогда, когда команда может проверить его scope, целостность, безопасность цели и смысл результата. Наличие файла доказывает наличие файла. Оно не доказывает, что приложение сможет продолжить работу. Учебный маршрут ниже показывает способ проверки, но не выдаёт учебный результат за production-drill.</p>\n<h2>Что именно нужно доказать</h2>\n<p>Восстановление состоит из нескольких независимых вопросов. Первый — что архив выбран правильно. Второй — что его содержимое не изменилось после создания. Третий — что в него попала нужная область базы. Четвёртый — что команда не затронет источник. Пятый — что после восстановления появились ожидаемые схемы, таблицы и данные.</p>\n<p>Эти вопросы связывает короткий manifest. В нём хранят идентификатор архива, формат, область данных, исключения, SHA-256 и проверки результата. Manifest не заменяет сам архив и не делает восстановление автоматическим. Он задаёт договор, с которым можно сравнить фактический файл и фактический кандидат.</p>\n<pre><code>{\n "manifestVersion": 1,\n "backupId": "training-catalog-2020-10-a",\n "artifact": {\n "file": "training-catalog.dump",\n "format": "pg_dump custom (-Fc)",\n "sha256": "<hash>"\n },\n "scope": {\n "includes": ["schema:catalog", "schema:reference"],\n "excludes": ["cluster roles", "tablespaces", "external files"]\n },\n "restoreChecks": {\n "relations": ["catalog.items", "reference.codes"],\n "rows": { "catalog.items": 3, "reference.codes": 2 }\n },\n "safety": "isolated training candidate only"\n}</code></pre>\n<p>Пример учебный. Имена, числа и значение хеша вымышлены. Поле <code>excludes</code> здесь не декоративно: логический dump одной базы не следует выдавать за копию всего кластера. Роли, tablespaces и внешние файлы требуют отдельного решения и отдельной проверки. <code>artifact.sha256</code> относится к файлу до восстановления, а <code>restoreChecks</code> — к базе после него; один флаг <code>complete: true</code> их не заменяет.</p>\n<figure><img src=\"/assets/editorial/2020/backup-recovery-diagnosis-2020.svg\" alt=\"Схема проверки резервного архива: scope и checksum ведут к безопасной цели, затем проверяются структура и данные\" loading=\"lazy\" /><figcaption>Restore проходит несколько границ: сначала договор и байты архива, затем цель, структура и данные кандидата.</figcaption></figure>\n<h2>Почему checksum не заменяет restore</h2>\n<p>SHA-256 помогает ответить на узкий вопрос: совпадают ли байты фактического файла с ожидаемым digest. Если значение изменилось, файл нельзя считать тем же артефактом. Причиной может быть неполная передача, повреждение, подмена или выбор другого файла под похожим именем.</p>\n<p>Но одинаковый digest не доказывает правильный scope и не подтверждает происхождение файла. Можно безошибочно сохранить неполный dump и получить идеальное совпадение checksum. Поэтому проверка идёт в два слоя: сначала bytes, потом содержимое архива и результат восстановления. Алгоритм хеширования подтверждает вычисленное значение, а не полноту данных.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика резервной копии и restore</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>SHA-256 не совпал</td><td>Файл изменился, повреждён или выбран не тот артефакт</td><td>Повторить digest и сверить путь с manifest</td><td>Остановить restore, получить архив из доверенного источника</td></tr><tr><td>Архив читается, нужной relation нет</td><td>Scope слишком узкий или выбран другой dump</td><td>Сравнить <code>pg_restore --list</code> с <code>scope.includes</code></td><td>Исправить путь создания или выпустить новую версию manifest</td></tr><tr><td>Роль отсутствует после restore</td><td>Cluster-wide объект не входил в dump базы</td><td>Проверить <code>scope.excludes</code> и список требуемых ролей</td><td>Восстановить роли отдельным согласованным шагом или изменить контракт</td></tr><tr><td>Restore завершился, таблица пуста</td><td>Dump неполный, выбран не тот объект или check не соответствует данным</td><td>Выполнить безопасный row count и сверить его с manifest</td><td>Остановить ввод данных, найти расхождение до переключения</td></tr><tr><td>Команда направлена в рабочую базу</td><td>Не доказана изоляция цели</td><td>Проверить адрес, имя и отдельные credentials кандидата</td><td>Не запускать restore; создать и явно подтвердить безопасную цель</td></tr></tbody></table></div>\n<p>Таблица задаёт отрицательный путь. Любое расхождение переводит операцию в остановку. Нельзя продолжать до следующего шага только потому, что архив «свежий» или команда раньше уже работала. Причина должна попасть в запись проверки вместе с действием, которое вернёт маршрут к доверенному состоянию.</p>\n<h2>Как получить и проверить учебный архив</h2>\n<p>Для custom archive сначала зафиксируйте исходную базу и scope, затем создайте артефакт и manifest. <code>pg_restore --list</code> читает оглавление non-plain archive и не подключается к базе, если не указан <code>--dbname</code>. Список нужно сравнить с manifest, а не с памятью оператора. Если ожидалась <code>reference.codes</code>, а её нет, повторный запуск restore не добавит объект.</p>\n<pre><code># Учебные имена. Команды этой статьёй не выполнялись.\npg_dump --format=custom --file=training-catalog.dump training_catalog\n# Linux: sha256sum; macOS: shasum -a 256.\nsha256sum training-catalog.dump\npg_restore --version\npg_restore --list training-catalog.dump</code></pre>\n<p>Команда <code>pg_dump</code> здесь создаёт dump базы, но не доказывает, что в scope вошли все зависимости приложения. После её выполнения digest и список объектов нужно записать в manifest. На macOS вместо <code>sha256sum</code> используйте <code>shasum -a 256</code>; смешивать вывод двух утилит с разными файлами нельзя.</p>\n<h2>Restore выполняйте в изолированном кандидате</h2>\n<p>Цель должна быть отдельной базой или отдельным кластером с понятным именем, доступом и запретом на перезапись источника. Новый кандидат удобно создать из <code>template0</code>, чтобы локальные добавления в <code>template1</code> не внесли неожиданные объекты. Перед последней командой проверьте подключение, адрес, credentials и отсутствие рабочих данных.</p>\n<pre><code># Только после проверки адреса и прав кандидата.\ncreatedb --template=template0 training_restore_candidate\npg_restore --exit-on-error --no-owner \\\n --dbname=training_restore_candidate training-catalog.dump\npsql --dbname=training_restore_candidate --command="SELECT to_regclass('catalog.items'), to_regclass('reference.codes');"\npsql --dbname=training_restore_candidate --command='SELECT count(*) FROM catalog.items;'</code></pre>\n<p>Флаг <code>--exit-on-error</code> делает ранний отказ заметнее, но не превращает код выхода в доказательство полноты. <code>--no-owner</code> полезен для учебного кандидата, где исходные роли не создавались; в рабочем сценарии владение объектами нужно согласовать отдельно. Сначала проверьте наличие relations, затем выполните row count и другие read-only checks. Если relation отсутствует, запрос к ней должен дать отрицательный результат, а не повод вручную добавить строки.</p>\n<h2>Порядок действий</h2>\n<ol><li>Сформулируйте scope одним предложением: какая база, схемы и зависимые объекты должны появиться после restore.</li><li>Перечислите исключения: роли, tablespaces, внешние файлы, секреты и другие ресурсы, которые не входят в этот артефакт.</li><li>Создайте архив выбранного формата и запишите имя, размер, версию инструмента и SHA-256 в manifest.</li><li>Прочитайте список архива и сравните его с ожидаемыми объектами. Несовпадение останавливает проверку.</li><li>Создайте изолированный кандидат из <code>template0</code>. Проверьте адрес, credentials и отсутствие маршрута записи в источник.</li><li>Выполните restore с сохранением stderr и кода выхода. Не трактуйте пустой stderr как полную проверку.</li><li>Запустите structural checks: relations, владельцы и версия схемы. Затем запустите безопасные semantic checks из manifest.</li><li>Запишите verdict и список непроверенных областей. Только после этого решайте, нужна ли отдельная проверка ролей, внешних файлов, retention или переключения.</li></ol>\n<h2>Отрицательный путь важнее зелёного</h2>\n<p>Положительный сценарий отвечает только на вопрос «маршрут работает». Для baseline важнее сохранить отказ там, где контроль должен сработать. Проверяйте четыре отказа: изменённый файл отклоняется по checksum; суженный scope — по сравнению manifest и списка объектов; источник — как недопустимая цель; пустая обязательная relation — как отрицательный verdict.</p>\n<p>Нельзя исправлять отрицательный результат подменой evidence. Не следует менять хеш, чтобы пройти gate, добавлять строки вручную после восстановления или переписывать expected count после обнаружения расхождения. Эти действия скрывают причину и делают следующий запуск менее надёжным.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Логический dump PostgreSQL покрывает не все способы восстановления. Копирование файлов кластера, continuous archiving и point-in-time recovery имеют другие предпосылки. Команда, которая проверила custom archive одной базы, не доказала восстановление всего кластера и не измерила готовность приложения.</p>\n<p>Учебный сценарий не проверяет сеть, права, секреты, размер production-данных, скорость передачи, репликацию, WAL, внешние object storage и работу зависимых сервисов. Он также не даёт RPO или RTO. Время учебной команды становится RTO только после измерения на разрешённой цели с описанными условиями. Дата архива сама по себе не является RPO. Версии PostgreSQL и клиентских утилит нужно записать рядом с результатом: доступные опции и поведение могут различаться между поддерживаемыми версиями.</p>\n<p>Критерий готовности проверяемый: для конкретного manifest другой оператор может найти нужный архив, подтвердить его digest, увидеть заявленные объекты, восстановить его только в изолированной цели и получить ожидаемые checks. При изменённом файле, неполном scope, неизвестной цели или расхождении результата маршрут останавливается с понятной причиной. Пока это не доказано повторяемым drill, в наличии есть файл, но нет подтверждённой резервной копии.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.postgresql.org/docs/current/backup.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL Documentation: Backup and Restore</a> — официально разделяет SQL dump, файловое резервирование и continuous archiving.</li><li><a href=\"https://www.postgresql.org/docs/current/app-pgdump.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL Documentation: pg_dump</a> — описывает формат custom и область одной базы; фактический scope нужно подтвердить отдельно.</li><li><a href=\"https://www.postgresql.org/docs/current/app-pgrestore.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL Documentation: pg_restore</a> — описывает <code>--list</code>, <code>--exit-on-error</code>, <code>--no-owner</code>, восстановление в новую базу и ограничения выборочного restore.</li><li><a href=\"https://csrc.nist.gov/pubs/fips/180-4/final\" target=\"_blank\" rel=\"noopener noreferrer\">NIST FIPS 180-4: Secure Hash Standard</a> — официальный стандарт семейства SHA-2; digest проверяет вычисленное значение, но не семантическую полноту восстановления.</li></ul>"
|
||
}
|