ESPN DeportesKawhi Leonard: ¿Cómo será recordado cuando finalice su carrera?Daily MaverickTHE GATHERING 2026: Fixing failing cities is in business’s own interest, leaders sayESPN'Gonna let them do them': Cunningham ignoring Kanter Freedom, Whiteוואלהחיל האוויר בגל תקיפות נגד יעדי טרור בדרום לבנוןRTP DesportoPortugal perde com Espanha e falha meias da Liga Europeia de futebol de praiaInquirerBojie Dy hails Pisa improvement: ‘It’s a welcome news’BlickVon Pfäffikon ZH über Siders VS bis Susch GR: Das sind die schlimmsten Busunglücke der Schweiz20 Minuten«Wir sind ein gutes Team»: Annemarie Carpendale lobt ihren WayneAntara NewsIndian envoy: BRICS agenda aligns with Indonesia bilateral focusBillboardFlavor Flav Shares Personal 9/11 Memory & Honors ‘Heroes Who Ran Toward Danger’ on 25th AnniversaryGlobal News13-year-old charged after firearm seized at Ajax hotel: Durham policeComplete Sports2026 US Open Final: Rybakina, Sabalenka Target Grand Slam Title
The Daily Newsstand · Free, Always
Friday, September 11, 2026

Бэкап, из которого ни разу не восстанавливались — это не бэкап

Translate

У резервного копирования есть свойство, которое отличает его от почти всего остального в инфраструктуре. Проверить, работает ли оно, можно ровно одним способом — восстановиться. Всё остальное проверяет что‑то другое.

Ситуация тем неприятнее, что узнаёте вы об этом в единственный день, когда копия понадобилась.

Поэтому в статье рассмотрим пять мест, где схема резервного копирования разваливается.

Ноль в конце формулы

Начну с главного, потому что остальные пять — его частные случаи.

Правило 3-2-1 знают все: три копии, два типа носителя, одна вне площадки. Придумано оно было в те времена, когда под аварией понимали пожар в серверной или смерть диска. Против аварии, у которой есть автор и мотив, оно держится хуже.

Поэтому появился расширенный вариант, 3-2-1-1-0. Добавленная единица — копия неизменяемая или физически отключённая. А ноль — это ноль ошибок при восстановлении, подтверждённых регулярными проверками.

Так вот, ноль в этой формуле стоит последним, а ломается первым. Восстановление никто не проверяет, потому что это долго, требует железа и всегда можно отложить до следующего квартала. В результате система живёт с копиями, о качестве которых не известно ничего.

Проверяется это одним вопросом: когда в последний раз восстанавливались из копии на настоящих данных. Ответ «в прошлом году» или «когда переезжали» означает, что с тех пор поменялись версии, схема и сама процедура, и текущее состояние не проверял никто.

Разумная частота — раз в квартал, с записью того, что восстанавливали, сколько это заняло и что пошло не так. Прогон можно собрать быстро:

#!/usr/bin/env bash
# restore-drill.sh — восстановление свежей копии на отдельный инстанс
set -Eeuo pipefail

DUMP=$(ls -t /backup/pg/*.dump | head -1)
CONT=restore-drill-$(date +%s)

trap 'docker rm -f "$CONT" >/dev/null 2>&1 || true' EXIT

docker run -d --name "$CONT" -e POSTGRES_PASSWORD=drill postgres:16 >/dev/null
until docker exec "$CONT" pg_isready -q; do sleep 1; done

START=$(date +%s)
docker exec -i "$CONT" pg_restore -U postgres -d postgres --no-owner < "$DUMP"
ELAPSED=$(( $(date +%s) - START ))

ROWS=$(docker exec "$CONT" psql -U postgres -tAc \
  "SELECT count(*) FROM orders WHERE created_at > now() - interval '7 days'")

echo "восстановление: ${ELAPSED}с, заказов за неделю: ${ROWS}"
[ "$ROWS" -gt 0 ] || { echo "данных нет, копия пустая"; exit 1; }

Последние две строки тут важнее всего остального.

Файл вполне может развернуться без единой ошибки и содержать пустые таблицы, так бывает, когда дамп снимали с реплики, которая давно отстала, или с базы, где у пользователя не было прав читать нужную схему.

Все копии в одном здании

Второе по частоте, и понять его проще всего от противного: спросите себя, какое одно событие способно уничтожить сразу и рабочие данные, и все их копии.

Если ответ находится — сервер, аккаунт, дата‑центр, вот она, эта ошибка, и дальше неважно, сколько копий вы на самом деле сделали.

Копия на соседнем диске того же сервера спасает от смерти одного диска и больше ни от чего: сервер сгорел — сгорело всё разом.

Копия на том же гипервизоре не переживёт отказ хоста.

Копия в том же облачном аккаунте не переживёт компрометацию этого аккаунта, а именно аккаунт в современных атаках и берут в первую очередь.

В правилах для этого есть отдельное название — общая судьба.

Не «что‑то могло сломаться», а конкретное событие, которое одним махом забирает и данные, и способ их вернуть.

Атаки последних лет строятся на этой логике.

Сначала атакующий тихо изучает окружение и находит все места, где лежат копии.

Потом, только убедившись, что восстанавливаться будет неоткуда, запускает шифрование рабочих систем.

Сколько именно времени уходит на разведку, я по открытым отчётам об инцидентах судить не берусь, цифры там сильно расходятся.

А вот порядок действий везде один и тот же:

  1. Сначала копии.

  2. Потом продуктив.

В общем, есть требования к неизменяемой копии. Неизменяемость означает, что данные записываются один раз и не могут быть изменены или удалены в течение заданного срока.

Включая ту учётную запись, которая их записала, это как раз главное.

aws s3api put-object-lock-configuration \
  --bucket backups-prod \
  --object-lock-configuration '{
    "ObjectLockEnabled": "Enabled",
    "Rule": {"DefaultRetention": {"Mode": "COMPLIANCE", "Days": 30}}
  }'

Режим COMPLIANCE отличается от GOVERNANCE тем, что снять блокировку не может никто, включая владельца корневого аккаунта.

Настроенная блокировка и работающая блокировка — разные утверждения, и проверить второе можно только попыткой удаления. Не с обычных прав, а с самых широких, какие вообще есть в аккаунте:

aws s3api delete-object --bucket backups-prod --key db-2026-08-20.dump
An error occurred (AccessDenied) when calling the DeleteObject operation:
User: arn:aws:iam::111111111111:root is not authorized to perform this operation

Если вместо этой ошибки объект удалился — конфигурация не применилась, приоритет забрала политика бакета, или блокировка стоит в режиме GOVERNANCE, который root как раз обходит. Р

Зелёный монитор ни о чём не говорит

Мониторинг обычно смотрит на код возврата задания. Отработало без ошибок — зелёный. При этом сломаться могло что угодно после этой проверки.

Посмотрим на три вещи, за которыми стоит следить отдельно.

  • Размер копии в сравнении со вчерашней. Резкое падение означает, что дамп снялся не полностью или база опустела. Резкий рост — что кто‑то залил мусор либо поменялась схема.

YESTERDAY=$(stat -c%s /backup/pg/db-$(date -d yesterday +%F).dump)
TODAY=$(stat -c%s /backup/pg/db-$(date +%F).dump)
DIFF=$(( (TODAY - YESTERDAY) * 100 / YESTERDAY ))

if [ "${DIFF#-}" -gt 20 ]; then
    echo "размер копии изменился на ${DIFF}% — проверить" >&2
fi
  • Возраст самой свежей копии. Задание может не падать, а просто не запускаться: планировщик отключили, таймер не включился после перезагрузки, узел вывели из кластера на обслуживание и забыли вернуть.

Проверить, запускался ли таймер systemd вообще, можно одной командой, и это первое, что стоит посмотреть, если подозрение уже есть:

systemctl list-timers pg-backup.timer
NEXT                        LEFT      LAST                         PASSED  UNIT
Sat 2026-08-21 03:00:00 UTC  4h 12min  n/a                          n/a     pg-backup.timer

Строка LAST n/a — таймер существует, но ни разу не срабатывал. Часто это следствие того, что юнит включили командой enable, забыв про --now, и он подхватится только при следующей перезагрузке узла, которая может не случиться месяцами.

Возраст самой свежей копии удобнее держать в метриках постоянно. Node exporter умеет читать значения из текстового файла, который вы обновляете сами после каждого прогона:

cat <<EOF > /var/lib/node_exporter/textfile_collector/backup.prom
# HELP backup_last_success_timestamp Unix-время последнего успешного бэкапа
# TYPE backup_last_success_timestamp gauge
backup_last_success_timestamp $(date +%s)
EOF

Дальше в Prometheus остаётся один запрос, который сравнивает это время с текущим и превращает молчание задания в честный алерт:

time() - backup_last_success_timestamp > 26 * 3600

Двадцать шесть часов вместо суток — запас на случай, если ночное окно один раз сдвинулось на час из‑за нагрузки. Алерт при этом реагирует не на ошибку задания, а на факт, что бэкапа давно не было, а это то, что нас интересует, независимо от причины.

  • И целостность архива. Дамп PostgreSQL проверяется чтением оглавления, а это заметно дешевле полного восстановления:

pg_restore --list /backup/pg/db-$(date +%F).dump > /dev/null \
    || echo "архив битый" >&2

Проверка читает только заголовок и содержание, так что её можно вешать сразу после снятия копии, не дожидаясь ночного окна.

Для копий, которые едут в объектное хранилище, чтения оглавления мало, файл мог побиться уже при передаче. Здесь спасает контрольная сумма, посчитанная сразу после создания дампа и сохранённая отдельно от самого файла:

sha256sum /backup/pg/db-$(date +%F).dump > /backup/pg/db-$(date +%F).sha256
aws s3 cp /backup/pg/db-$(date +%F).dump s3://backups-prod/
aws s3 cp /backup/pg/db-$(date +%F).sha256 s3://backups-prod/

А перед восстановлением сумму пересчитывают и сравнивают с сохранённой:

aws s3 cp s3://backups-prod/db-$(date +%F).dump .
sha256sum -c db-$(date +%F).sha256 || echo "файл повреждён при хранении или передаче" >&2

Если инструмент резервного копирования не самописный, а готовый — restic или borg, та же задача решена штатной командой, которая заодно проверяет дедупликацию и внутренние индексы:

restic check --read-data-subset=10%

Полная проверка --read-data без указания подмножества выглядит адекватнее, но перекачивает весь архив целиком, и на терабайтных репозиториях это отдельная по стоимости операция.

Час на восстановление, который оказался сутками

Про эти цифры обычно вспоминают на презентации проекта, а потом благополучно забывают до самой аварии.

Копия раз в сутки — значит, в худшем случае пропадёт рабочий день, и это все понимают заранее. А вот сколько времени займёт само восстановление, почти никто не считает заранее, хотя посчитать это можно ровно тем же квартальным прогоном, который мы уже разбирали.

Разрыв между обещанным сроком и тем, что выходит на деле, обнаруживается в двух местах.

Первое — скачивание из холодного хранилища.

Данные лежат в дешёвом классе, откуда их не выгрузишь мгновенно, а узнают об этом ровно тогда, когда счёт уже на минуты.

У Glacier восстановление файла — отдельная операция с тремя уровнями срочности, и у каждого своя цена и своё ожидание:

aws s3api restore-object \
  --bucket backups-cold \
  --key db-2025-01-15.dump \
  --restore-request '{"Days": 1, "GlacierJobParameters": {"Tier": "Expedited"}}'

Ускоренный уровень обещает единицы минут, стандартный — часы, объёмный — до полутора суток. Цена растёт в обратную сторону: ускоренный тариф на порядок дороже объёмного за тот же гигабайт.

Экономить на хранении холодных копий разумно, но если план на бумаге написан «выгрузим за пять минут», а настроен на самый дешёвый уровень — эти пять минут превратятся в полтора суток ожидания.

И это ещё без самой передачи. Объект вернулся из архива — теперь его нужно скачать.

Терабайтный архив по гигабитному каналу качается часа два, если канал весь ваш и ничем больше не занят. В аварии он редко бывает свободен.

Второе больное место — накат логического дампа на большую базу.

Дамп разворачивается медленно: строчка за строчкой, потом заново строятся индексы. Для базы в сотни гигабайт при паре часов на восстановление одна ночная копия попросту не успевает, и никакими настройками это не ускорить. Нужны физические копии и непрерывная архивация журналов:

pg_basebackup -h primary -D /backup/base -X stream -c fast -P

Потоковая передача журналов тут обязательная часть, без неё копия окажется несогласованной, если во время снятия шли записи.

База поднялась, а сервис не работает

Дамп развернулся, таблицы на месте, счётчик из первого скрипта показывает нужное число заказов. Восстановление прошло — можно закрывать тикет.

А приложение при первом же запуске падает с ошибкой, которую вы за месяц ни разу не видели.

FATAL: role "app_readonly" does not exist

Тако/й роли действительно нет.

+,92 И не будет, потому что pg_dump в обычном режиме сохраняет только содержимое одной базы — таблицы, данные, индексы, представления. Роли и права живут на уровне кластера СУБД, а не базы, и в дамп одной базы попросту не попадают.

pg_dump --dbname=orders --format=custom > orders.dump

pg_dumpall --globals-only > globals.sql

Второй командой почему‑то пользуются заметно реже первой, хотя без неё восстановленная база не узнаёт ни одного пользователя, кроме postgres.

При восстановлении порядок важен: сперва накатывают глобальные объекты, потом сам дамп.

psql -f globals.sql
pg_restore --dbname=orders orders.dump
  • Роли — только первая находка в этом разделе, и, пожалуй, самая безобидная: её хотя бы видно сразу по тексту ошибки. Дальше список идёт по нарастанию скрытности.

  • Секреты и сертификаты — если они не в базе, а в отдельном хранилище, копия базы их не затронет вообще.

  • Файлы, которые пользователи когда‑то загрузили и которые лежат на диске рядом с приложением, а не в объектном хранилище, — тоже мимо любого дампа СУБД.

  • Состояние очередей — сообщения, которые не успели разобрать на момент аварии, восстановленная база не помнит и не обязана.

Отдельная категория — вся инфраструктура вокруг самого приложения: правила фильтрации трафика, записи DNS, настройки балансировщика, переменные окружения в оркестраторе.

Если это описано кодом и лежит в репозитории рядом с приложением — восстанавливается тем же способом, что и всё остальное.

Если собиралось руками через консоль провайдера — восстанавливать придётся по памяти, а память эта сейчас, скорее всего, у человека в отпуске.

Собрать список того, что реально нужно для запуска, а не просто «кажется важным», проще всего явным манифестом — он же потом служит чек‑листом при разборе:

# restore-manifest.yml
required_for_boot:
  - name: postgres-data
    source: s3://backups-prod/orders.dump
    verify: pg_restore --list
  - name: postgres-roles
    source: s3://backups-prod/globals.sql
    verify: grep -q "CREATE ROLE app_readonly" globals.sql
  - name: uploaded-files
    source: s3://backups-prod/uploads/
    verify: aws s3 ls s3://backups-prod/uploads/ | wc -l
  - name: app-secrets
    source: vault-snapshot-2026-08-20.snap
    verify: vault operator raft snapshot inspect
  - name: dns-zone
    source: route53-export.json
    verify: jq '.ResourceRecordSets | length' route53-export.json

Дальше манифест прогоняется скриптом, который не восстанавливает данные сам, а честно говорит, чего не хватает:

#!/usr/bin/env bash
set -Eeuo pipefail

MISSING=0
while IFS= read -r name; do
    src=$(yq ".required_for_boot[] | select(.name == \"$name\") | .source" restore-manifest.yml)
    if [ -z "$src" ] || ! aws s3 ls "$src" >/dev/null 2>&1; then
        echo "нет источника для: $name" >&2
        MISSING=1
    fi
done < <(yq '.required_for_boot[].name' restore-manifest.yml)

[ "$MISSING" -eq 0 ] || { echo "чего-то не хватает, смотрите выше" >&2; exit 1; }

Отдельный прогон — сама проверка того, что из этого получается рабочий сервис, а не просто набор развёрнутых файлов.

Задача формулируется иначе, чем обычное восстановление: не «поднять базу», а «поднять сервис целиком в изолированном окружении, имея на руках только копии, без единого файла из живой системы».

docker compose -f drill-compose.yml up -d
sleep 5
curl -sf -X POST http://localhost:8080/login \
  -d '{"user":"drill@example.com","password":"drill"}' \
  || { echo "сервис поднялся, вход не работает" >&2; exit 1; }

Именно на этом шаге всплывает то, что в манифесте не всегда предусмотришь заранее: приложение при старте лезет за курсом валют во внешний сервис и без него не проходит проверку готовности, хотя к резервным копиям это отношения не имеет вовсе.

Чем раньше находится такая зависимость, тем дешевле она обходится — на плановом прогоне это пять минут разговора с разработчиками, посреди настоящей аварии — ещё один час простоя, который никто не планировал.

Напоследок

Если делать одну вещь из всего перечисленного, я бы сделал одно восстановление. Взять свежую копию, развернуть на отдельной машине, засечь время и проверить не факт запуска, а конкретные данные: есть ли вчерашние заказы, сходятся ли остатки, работает ли вход пользователя. То, что выяснится за эти два часа, обычно полезнее любого аудита схемы.

Дальше три вещи по убыванию отдачи.

  1. Неизменяемая копия — от неё зависит, останется ли у вас вообще что восстанавливать.

  2. Мониторинг возраста последней копии вместо кода возврата задания — молчащее задание опаснее падающего.

  3. И записанная процедура, проверенная человеком, который её не писал.

Цифры по частоте проверок и по срокам восстановления, которые я привёл, — отраслевые ориентиры из рекомендаций, а не измерения на конкретной инфраструктуре.

У вас они выйдут другими. Собственно, поэтому единственная надёжная проверка — своя, на своих данных и своём железе.

Даже хорошо настроенный бэкап не гарантирует восстановление системы, если заранее не проверить весь путь — от хранения копии до запуска рабочего сервиса.

Разобраться, как строить устойчивую инфраструктуру, искать слабые места и готовиться к сбоям, можно на открытых уроках:

  • 23 сентября, 20:00. «Узнайте, как использовать Patroni для управления высокодоступными кластерами PostgreSQL». Записаться

  • 21 сентября, 20:00. «Типовые задачи с RAID‑массивами: создание, эксплуатация, перенос данных и восстановление». Записаться

  • 14 октября, 20:00. «ИИ для мониторинга: что Prometheus и Grafana могут рассказать агенту». Записаться

Полный список бесплатных уроков сентября по инфраструктуре смотрите в дайджесте.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.