Перейти к содержимому
ADMIN
FUTURE
Главная страница » В 14:37 удалили данные 1С: как вернуться в прошлое и не уничтожить настоящее
FIELD NOTES / 04.11.2026

В 14:37 удалили данные 1С: как вернуться в прошлое и не уничтожить настоящее

Максим Шаповалов19 мин чтения
Восстановление PostgreSQL к моменту времени: цепь WAL от базовой копии до изолированной копии данных 1С

Вчера в 14:37 база записала неправильный результат

Привет, коллеги. Я Максим, системный администратор. Вчера в 14:37 оператор завершила в 1С обработку, которую запускать было не нужно: часть документов списалась дважды. Она сразу позвонила и сказала честно: «Я нажала не то». Паники не было, база работала дальше, коллеги продолжали вводить документы. На сервере PostgreSQL ничего не упало и не зависло.

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

Разберём учебный случай: сервер srv-pg-01, кластер PostgreSQL с базой 1С trade_1c, фирма example. Имена вымышленные, это не отчёт о клиентской аварии. Ночью делалась файловая копия, WAL архивировались на отдельный сервер. Будем восстанавливать данные к моменту перед ошибкой — в изолированной копии, не трогая работающий production. В конце получим проверяемый результат: восстановленное состояние на 14:36, из которого владелец конфигурации извлечёт нужные документы и вернёт их в рабочую базу контролируемым способом.

Команды рассчитаны на PostgreSQL с включённой архивацией WAL. Если архивация не настроена заранее, честный ответ будет в разделе про границы метода: вернуться в 14:37 без цепи WAL нельзя, и я не буду делать вид, что можно.

Почему откат всей базы — проигрышный ход

Смотрю на часы. Ошибка случилась в 14:37. Ночная копия сделана в 02:00. Между ними — двенадцать с половиной часов работы десятков людей. Если я разверну базу из копии в production, вместе с ошибкой исчезнут и их документы. Утром выяснится, что мы обменяли одну аварию на другую, и вторая будет больше первой.

Есть и второй довод, технический. Ночная копия у нас файловая, сделанная на уровне каталога кластера. А если бы вместо неё был pg_dump? Логическая выгрузка полезна для миграций и переноса между серверами, но она не участвует в восстановлении по журналу. Документация PostgreSQL прямо предупреждает: pg_dump и pg_dumpall не создают файловой копии и не могут быть частью решения с проигрышем WAL. Восстановление по журналу стартует с файловой копии кластера, а не с SQL-дампа.

Значит, ищем способ вернуть только утраченное. Такой способ существует, но он опирается на то, что сервер уже ведёт журнал опережающей записи, — а ведёт он его всегда, независимо от наших желаний. Осталось понять модель.

Каждый шаг сужает неопределённость: от границы ошибки до приёмки владельцем конфигурации. Ни один шаг не трогает работающий production.
Каждый шаг сужает неопределённость: от границы ошибки до приёмки владельцем конфигурации. Ни один шаг не трогает работающий production.

WAL уже содержит и ошибку, и правильную работу

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

Для нас важно другое свойство: в WAL попадает всё подряд. И ошибочная обработка оператора в 14:37, и правильные документы коллег до и после неё. Журнал ничего не знает о наших намерениях — он фиксирует факты.

Отсюда главный вывод метода: проигрыш журнала без точки остановки воспроизведёт в копии базы и ошибку, и всё остальное. Мы не «отменяем» обработку оператора — мы умеем остановить повтор истории за секунду до неё. Именно поэтому задача звучит не «откатить», а «восстановить к моменту времени» — point-in-time recovery, PITR.

Замечу и про место хранения. Незаполненные журнальные файлы живут в каталоге pg_wal внутри кластера. Если там кончится место, сервер остановится аварийно: документация описывает это как PANIC shutdown — подтверждённые транзакции не теряются, но база не работает, пока место не освободят. Сегодня нам важно, что заполненные сегменты уходят в архив, и дальше решает уже цепь.

Модель: базовая копия плюс непрерывная цепь WAL

Механизм PITR стоит на двух ногах. Первая — базовая копия: снимок файлов кластера, сделанный штатно, на работающем сервере, через pg_basebackup. Вторая — непрерывная цепь архивных сегментов WAL, начинающаяся не позже начала этой копии.

Почему цепь должна быть непрерывной? При восстановлении сервер берёт файлы копии и переписывает поверх них журнал, начиная с той позиции, на которой копия стартовала. Каждый пропущенный сегмент — это дыра в истории. Сервер дойдёт до разрыва и остановится, потому что следующее изменение уже не восстановить. Документация формулирует условие жёстко: для успешного восстановления нужна непрерывная последовательность архивных WAL, уходящая назад как минимум до начала базовой копии.

Важная деталь, которую легко пропустить: pg_basebackup сам по себе не гарантирует архивацию WAL, сгенерированных во время копирования. Гарантию даёт связка: архиватор настроен и работает, а сервер ждёт архивации последнего нужного сегмента перед завершением копии. Процесс копирования создаёт backup history file — маленький файл с меткой, начальным и конечным сегментами. Он попадает в архив и говорит нам, откуда начинать проверку цепи.

Пока сервер жив и архивация работает, цепь растёт сама. Наша работа — убедиться, что она растёт без разрывов.

Production продолжает работать, пока копия отматывается по цепи WAL к 14:36:55. Откат production здесь не нужен — он и есть то, от чего мы уходим.
Production продолжает работать, пока копия отматывается по цепи WAL к 14:36:55. Откат production здесь не нужен — он и есть то, от чего мы уходим.

Проверка архивации до того, как она понадобится

Сначала читаем состояние, ничего не меняя. PostgreSQL ведёт статистику архиватора в представлении pg_stat_archiver. Она отвечает на три вопроса: когда последний сегмент успешно ушёл в архив, сколько попыток завершилось неудачей и не застрял ли архиватор на одном файле.

-- Read-only. On the primary, any database.
SELECT archived_count, failed_count,
       last_archived_wal, last_archived_time,
       last_failed_wal, last_failed_time,
       stats_reset
FROM pg_stat_archiver;
-- archived_count is a lifetime counter: look at fresh last_archived_time
-- and a failed_count that is NOT growing. A stuck last_archived_wal
-- with a growing failed_count means the chain is not being extended.

Что здесь важно интерпретировать правильно. Поле archived_count, растущий день ото дня, — хороший знак, но сам по себе не доказательство: он считает успешные архивации за всю жизнь статистики. Важнее связка: last_archived_time свежий, failed_count не растёт. Если failed_count увеличивается, сервер долбится в архив и откладывает — и каждый такой день цепь не дополняется, а pg_wal накапливает необработанные сегменты.

Дальше — проверка целевого каталога. Файлы архива лежат там, куда их кладёт archive_command. Имена сегментов несут позицию в абстрактной последовательности WAL, и по имени видно, что за чем следует. Переходим к самой цепи.

Целая цепь решает, один файл — нет

Восстанавливаемость — свойство цепи, а не отдельного файла. Утилита покажет, что ночная копия на месте, и это ничего не скажет о том, восстановимся ли мы к 14:37.

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

#!/usr/bin/env bash
# pitr-chain-check.sh — read-only WAL archive continuity check.
# Usage: ./pitr-chain-check.sh /srv/wal-archive
# Exits 0 if no gaps found, 1 on the first missing interval.
set -euo pipefail
ARCHIVE="${1:-}"
if [[ -z "$ARCHIVE" || ! -d "$ARCHIVE" ]]; then
  echo "Usage: $0 /path/to/wal/archive" >&2
  exit 2
fi
segments=$(find "$ARCHIVE" -maxdepth 1 -type f -regextype posix-extended \
  -regex '.*/[0-9A-F]{24}' | sort)
if [[ -z "$segments" ]]; then
  echo "No WAL segment files found in $ARCHIVE" >&2
  exit 2
fi
prev=""
count=0
while IFS= read -r seg; do
  name=$(basename "$seg")
  count=$((count + 1))
  if [[ -n "$prev" ]]; then
    # Sequential segment names differ by 1 in the last 8 hex digits.
    prev_tail=$((16#${prev:16}))
    cur_tail=$((16#${name:16}))
    if (( cur_tail - prev_tail != 1 )); then
      echo "GAP: after $prev the next file is $name (expected +1)" >&2
      echo "Check from $prev forward: the chain is broken before $name" >&2
      exit 1
    fi
  fi
  prev="$name"
done <<< "$segments"
echo "OK: $count WAL segments, no numbering gaps."
echo "Note: this checks segment NAMES, not their content."
echo "Start boundary: the first segment must not be newer than the base backup start"
echo "(see the newest *.backup history file in the same archive)."

Три честные оговорки. Во-первых, скрипт проверяет непрерывность имён, а не содержимое: битый файл с правильным именем он не заметит — для этого у сервера есть собственная проверка при replay. Во-вторых, граница проверки — начало базовой копии: всё, что старше ближайшего backup history file, для восстановления из этой копии не нужно. В-третьих, проверку надо делать регулярно, а не в день аварии: разрыв, найденный в 14:40 вчерашнего дня, лечится настройкой архивации; разрыв, найденный сегодня, может оказаться фатальным для задачи.

# Read-only. Where does the WAL chain start for the newest base backup?
# Archive layout (example): /srv/wal-archive/<segment-name>
ls -1 /srv/wal-archive/*.backup
# Newest history file points at the first WAL segment of that backup:
#   START WAL LOCATION: 0/4000028 (file 000000010000000000000040)
# Everything from that segment forward must be present and unbroken.

Цепь цела, архивация жива. Теперь решаем, где остановить повтор истории.

Точка остановки: время — не граница транзакции

Цель восстановления задаётся параметром recovery_target_time — меткой времени, до которой проигрывается журнал. Формат тот же, что у типа timestamp with time zone, и документация советует писать смещение от UTC или полное имя зоны: сокращения вроде MSK сервер может не понять.

Тонкость в том, что 14:37:00 — это время, когда мы заметили проблему, а не граница транзакции. Обработка оператора стартовала в 14:35:52 и фиксировала изменения пачками. Если поставить цель 14:36:59, в копию попадёт половина её транзакций — состояние будет хуже понятного: половина документов списана, половина нет. Параметр recovery_target_inclusive управляет границей: остановиться сразу после указанной цели или сразу перед ней.

Что делать администратору, если точная граница неизвестна? Искать её в журнале сервера: время начала обработки видно по записям о подключениях и запросам. Ставлю цель на пару секунд раньше старта обработки — включительности хватит, чтобы не захватить ни одной её транзакций. Другие типы целей — по номеру транзакции, LSN или имени — сведём в таблицу ниже, они решают те же задачи точнее, когда известен идентификатор.

Параметр целиЧто задаётКогда выбиратьОговорка
recovery_target_timeМетку времени остановкиГраница известна по времени (журнал сервера)14:37 — время заметки, не граница транзакции; нужен запас
recovery_target_xidНомер транзакцииИзвестен XID ошибочной транзакцииXID выдаются при старте, фиксируются в другом порядке
recovery_target_lsnПозицию LSN в журналеПозиция видна из метрик/журналовТребует чтения LSN, не время
recovery_target_nameИменованную точкуТочка создана заранее через pg_create_restore_pointТочная граница по построению; нужна привычка ставить точки
recovery_target_inclusiveОстановиться после/перед цельюУточнение границы на одну записьРаботает с time/xid/lsn
recovery_target_actionДействие на цели: pause / promote / shutdownpause — для проверки до решенияshutdown: recovery.signal останется, старт снова остановится
# postgresql.conf of the ISOLATED copy. Adjust the target, do not copy blindly.
restore_command = 'cp /srv/wal-archive/%f %p'
recovery_target_time = '2026-11-03 14:36:55+03'
recovery_target_inclusive = false
recovery_target_action = 'pause'
# pause: stop AT the target, allow read-only inspection, then decide.
# Wrong target? Shut down, change the value, start again:
# a new timeline branch is created, the archive is not harmed.

Восемь строк конфигурации, которые мы ещё применим на изолированной копии. Пока это план, не действие.

Предохранитель до ошибки: именованные точки восстановления

Лучшая точка восстановления — та, что создана до ошибки, а не угадана после неё. PostgreSQL умеет фиксировать именованные метки в журнале: pg_create_restore_point. Перед рискованной обработкой — свёрткой, групповым изменением, массовым перепроведением — точка ставится одной командой и попадает в WAL вместе с остальной историей.

-- Before a risky batch job. Requires rights for pg_create_restore_point.
SELECT pg_create_restore_point('before_bulk_recalc_20261103') AS restore_point_lsn;
-- The mark lands in WAL. It is a label, not a snapshot:
-- useful only together with working WAL archiving.
-- After the job: if it went wrong, recovery_target_name='before_bulk_recalc_20261103'

Имя говорит само за себя: before_bulk_recalc_20261103. Если обработка пойдёт не так, цель восстановления recovery_target_name ссылается на эту метку, и граница транзакций получается точной по построению — не нужно вычислять её из журнала задним числом.

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

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

Timelines: эксперименты не должны затирать историю

Восстановление к прошлому создаёт парадокс, который документация PostgreSQL честно сравнивает с путешествиями во времени. Мы развернули копию на 14:36 и дали людям с ней работать. Копия генерирует свои журнальные записи. Если эти записи пойдут в тот же архив, они перезапишут сегменты, ведущие к другим вариантам истории, — и вернуться в исходную ветку станет невозможно.

Решение встроено в механизм: timelines. Каждое завершённое восстановление порождает новую ветвь истории, а номер timeline входит в имена файлов WAL — первые восемь шестнадцатеричных символов. Ветки не затирают друг друга. Не угадали точку остановки с первого раза? Восстанавливаемся заново в другую ветвь, не боясь испортить архив — файл истории с расширением .history фиксирует, откуда ветвь отделилась.

Практическое следствие для нашей аварии: изолированная копия, которую мы сейчас поднимем, после проверки может стать новым primary, а может быть выброшена. В обоих случаях архив остаётся цел — благодаря ветвлению, а не благодаря нашей аккуратности. Ещё следствие: файл .history мал, и документация советует хранить его в архиве вместе с сегментами бессрочно; комментарий в нём с причиной ветки окупится при следующем расследовании.

Возврат в прошлое порождает новую ветвь timeline. Повторные попытки не затирают архив и не портят исходную историю кластера.
Возврат в прошлое порождает новую ветвь timeline. Повторные попытки не затирают архив и не портят исходную историю кластера.

Изолированная площадка для возврата

Восстановление делаем не на production. Причины простые: копия кластера будет носить то же имя базы, те же порты, и она не должна отвечать пользователям, пока мы её не проверили. Кроме того, место: на время восстановления нужно дисковое пространство под полный кластер и архив WAL, которые будут перечитаны.

Площадка у нас есть: тестовый хост restore-01 в том же сегменте, с доступом к архиву по NFS. Требования к ней скромные: тот же major-версии PostgreSQL, достаточно места, закрытый сетевой доступ к портам базы до завершения проверки.

Две вещи, которые я делаю до первого запуска. Первая: pg_hba.conf в копии закрыт для всех, кроме локального административного подключения — даже если площадка изолирована, привычка дешёвая. Вторая: архив WAL доступен на чтение, и ничто в моих командах не пишет в архив. Каталог кластера копируется, а не переносится: оригиналы архива и резервная копия остаются нетронутыми, чтобы при ошибке начать заново без потерь.

Сервер production за это время продолжает работать и накапливать новые WAL — цепь пополняется, и это нормально: наша точка остановки в прошлом, новые сегменты её не портят.

Первый прогон восстановления

Порядок действий на площадке воспроизводим по документации: разложить файлы базовой копии, очистить каталог pg_wal от того, что приехало внутри копии, записать restore_command, создать файл recovery.signal и запустить сервер. Сервер войдёт в режим восстановления и начнёт читать архив.

# On the isolated host (restore-01), as the postgres OS user. Example paths.
set -euo pipefail
# 1) Clean target directory; nothing from previous attempts must remain.
rm -rf /var/lib/pgsql/pitr-copy
mkdir -p /var/lib/pgsql/pitr-copy
# 2) Copy (not move!) the base backup. Originals stay untouched.
cp -a /srv/basebackup/latest/. /var/lib/pgsql/pitr-copy/
# 3) Segments inside the backup are stale: the archive is authoritative.
rm -rf /var/lib/pgsql/pitr-copy/pg_wal
mkdir -p /var/lib/pgsql/pitr-copy/pg_wal
# 4) Recovery configuration (see the recovery target block above).
cp /var/lib/pgsql/pitr-copy/postgresql.conf /var/lib/pgsql/pitr-copy/postgresql.conf.orig
vi /var/lib/pgsql/pitr-copy/postgresql.conf
# 5) Marker: while it exists, the server is in archive recovery.
touch /var/lib/pgsql/pitr-copy/recovery.signal
# 6) Start and watch the log: replay position, pause at the target.
pg_ctl -D /var/lib/pgsql/pitr-copy -l /var/lib/pgsql/pitr-copy.log start
tail -f /var/lib/pgsql/pitr-copy.log

Три места, где я не экономлю внимание. Во-первых, на шаге 2 файлы копируются, а не переносятся — после неудачного прогона не нужно просить резервную копию заново. Во-вторых, pg_wal очищается: сегменты из копии устарели, а свежие сегменты сервер возьмёт из архива; если я оставлю старые, получу путаницу версий. В-третьих, recovery.signal — это маркер: пока он существует, сервер считает себя восстанавливающимся; целевой каталог должен быть пуст от прошлых запусков.

recovery_target_action=pause остановит повтор на цели и оставит сервер в состоянии, когда можно подключиться и смотреть данные, но нельзя писать. Это именно то, что нужно для проверки: смотреть до того, как решать судьбу копии.

Результат проверяем до вывода в мир

Копия стоит на паузе. Подключаюсь локально и проверяю состояние.

-- On the isolated copy, local connection, after the pause.
SELECT pg_is_in_recovery() AS in_recovery,
       pg_is_wal_replay_paused() AS replay_paused;
SELECT pg_last_wal_replay_lsn() AS replay_lsn,
       pg_last_wal_receive_lsn() AS receive_lsn;
-- Sampled business counters (teaching example: 1C document tables).
-- Compare with the production numbers reported BEFORE the incident.
SELECT count(*) AS posted_docs_before_target
FROM doc_journal WHERE posted_at < '2026-11-03 14:36:55+03';
SELECT count(*) AS bad_batch_traces
FROM doc_journal WHERE batch_id = 'bulk_recalc_20261103';
-- Expected: pause at target = true; bad_batch_traces = 0.
-- Mismatch = stop, refine the target, replay again into a new branch.

Что здесь доказывает каждый результат. Первая строка: сервер на паузе именно там, где мы его остановили — recovery дошла до цели. Вторая: текущая позиция повтора сравнима с журналом сервера, по которому мы вычисляли границу. Третья и четвёртая: счётчики документов на выборку — операции до 14:36 на месте, признаков обработки оператора нет.

Если числа не сошлись — не трагедия, а ветвь. Останавливаю сервер, уточняю цель по журналу, запускаю снова. Timeline разведёт новую ветку, архив не пострадает. Так выглядит итерация поиска точки: дёшево, обратимо, с доказательством на каждом шаге.

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

Бизнес-результат извлекаем, а не вставляем

Дальше работа делится на две разные задачи, и смешивать их опасно. Первая — инженерная: у нас есть копия кластера в правильном состоянии. Вторая — прикладная: вернуть утраченные данные в рабочую базу так, чтобы 1С считала их своими.

Прямой вставки строк в физические таблицы здесь не будет. Таблицы 1С связаны логикой конфигурации: документ, его движения по регистрам, счета, проводки. Физически корректный INSERT способен дать данные, которые платформа не признает или признает с повреждением ссылочной целостности. Правильный инструмент — штатные средства платформы: обработка выгрузки/загрузки данных, перепроведение документов, свёртка по правилам владельца конфигурации. Администратор базы обеспечивает копию; прикладное извлечение делает тот, кто отвечает за конфигурацию.

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

Оговорка про масштаб: если ошибка тронула сотни тысяч объектов, ручная выгрузка нереалистична — тогда решение о частичном переносе принимает владелец конфигурации, вплоть до варианта, когда копия становится новой рабочей базой, а накопленные за вечер операции переносят в неё. Это бизнес-решение, не административное.

Границы метода

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

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

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

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

Конфигурационные файлы в WAL не попадают: postgresql.conf, pg_hba.conf и pg_ident.conf правятся вручную, и их изменения архив не помнит. Копия конфигурации — отдельная забота.

wal_level ниже replica делает PITR невозможным: минимальный уровень не пишет строки для постоянных отношений, создаваемых или переписываемых транзакцией, и документация предупреждает, что смена уровня на minimal делает прежние базовые копии непригодными для PITR. Плюс целый класс ограничений: CREATE TABLESPACE пишет в WAL абсолютный путь и воспроизведётся с тем же путём; изменение шаблонных баз во время копии может протечь в восстановление.

Из всего списка самая частая реальная граница — первая: метод возвращает кластер, а не документ. Отсюда и архитектура статьи: копия несёт состояние, бизнес-результат из неё извлекают средствами платформы.

Когда PITR не поможет

Честность метода — знать, когда он бессилен. Три ситуации из практики.

Архивация не настроена заранее. WAL существует, но заполненные сегменты перезаписываются в pg_wal и не уходят наружу. К 14:37 вчерашнего дня нужные сегменты уже переработаны — вернуться в 14:36 физически не к чему. Спасёт только то, что лежит в обычных копиях: ночной снимок или логический дамп. Вывод не «PITR плох», а «архивацию надо было включить до аварии». После чтения этой статьи — включить до аварии.

Архив или диск с ним потеряны. Цепь цела, но лежит на том же хранилище, что и база, и хранилище умерло вместе с сервером. Отдельный носитель для архива — не паранойя, а условие самого метода. Если архив уезжает на другой сервер, проверяйте доступность этого сервера в сценарии «основной мёртв».

Повреждение затронуло и архив, и текущий каталог. Тогда разговор уже не про 14:37, а про последнюю проверенную копию и про standby, если он есть. Это сценарий другой статьи и другой проверки.

Ситуацияpg_dump (логическая копия)Файловая копия без WALBase backup + цепь WAL (PITR)
Нужно перенести базу на другой серверПодходит: переносит схему и данныеИзбыточен и рискован без выключенной базыИзбыточен
База разрушена, нужна вся база на вчерашнее утроВосстановит на момент выгрузкиВосстановит на момент снимкаВосстановит на любой момент внутри цепи
Ошибочная обработка в середине дня, production живёт дальшеНе поможет: правильные операции после дампа потеряютсяНе поможет: та же граница ночного снимкаВосстанавливает кластер к моменту перед ошибкой в изолированную копию
Зона честной применимостиМиграции, выгрузка по фильтрамАварийный снимок целикомВозврат истории без остановки production

Сравнение инструментов выше — не украшение, а карта «что спасёт в какой ситуации». Каждая колонка — честный инструмент со своей зоной покрытия.

Приёмка после чтения и профилактика

Итог учебного расследования: production не тронут, цепь WAL цела, изолированная копия поднята на 14:36:55 и подтверждена выборкой, владелец конфигурации извлёк из неё документы и вернул их в рабочую базу штатной обработкой. Оператор продолжила работу. Авария стоила нам вечера, а не половины рабочего дня чужих документов.

Полезное действие после чтения одно: проверить архивацию WAL на своём сервере сегодня, пока она не нужна. Две команды из раздела про pg_stat_archiver и скрипт цепи из комплекта ответят на вопрос «а мы вообще сможем вернуться в 14:37» честным да или честным нет.

Комплект к статье: скачиваемый архив pitr-kit.zip — скрипт проверки цепи WAL, критерии приёмки восстановления RU/EN и список первоисточников.

Профилактика, которая делает следующую аварию скучной: именованная точка восстановления перед каждой рискованной обработкой; регулярная проверка цепи по расписанию; тренировочное восстановление в изолированную копию хотя бы раз в квартал — успешный backup не подтверждает восстановимость; архив WAL на отдельном носителе; и фиксация фактических RPO и RTO после тренировки, а не до.

КритерийКак проверяемЧто означает успех
Архивация работаетpg_stat_archiver + свежий last_archived_time, failed_count не растётЦепь пополняется сейчас, не «вчера была настроена»
Цепь непрерывнаСкрипт pitr-chain-check.sh по каталогу архива от начала последнего base backupРазрывов нет; первый разрыв назван явно
Цель восстановления обоснованаГраница взята из журнала сервера (старт обработки), а не из памятиЦель раньше первой транзакции ошибочной обработки
Изоляция восстановленияКопия поднята на отдельном хосте, pg_hba закрыт, в архив не пишетсяProduction не затронут ни чтением пользователей, ни записью
Состояние подтверждено выборкойpg_is_wal_replay_paused, replay LSN, контрольные счётчики документовКопия несёт ровно искомое состояние
Бизнес-приёмкаВладелец конфигурации выгрузил документы и сверил отчёты в productionДанные возвращены штатными средствами платформы
Фактические RPO/RTO записаныЗамеры тренировочного восстановления зафиксированыЦифры измерены, а не оценены

Вопросы коллегам, на которые я бы хотел знать ответы заранее: кто в вашей команде умеет читать backup history file и найти начало цепи? Сколько часов займёт восстановление кластера на тестовый хост — меряли? Что произойдёт с архивом WAL, если основной сервер проработает без ребутов ещё год — место на диске архива не кончится?

МШ

Максим Шаповалов

Системный администратор. 17+ лет работы с инфраструктурой.

Обсудить задачу ↗

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх