WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-663 · Дефект · — · web

P1 Восстановление из резервной копии ни разу не доказано: бэкап работает, restore не проверялся

Закрыт P1 · важно ведёт: — эпик: WEB-093
Суть
# Бэкап идёт, восстановление не доказано

## 1. Что нашли и как

Шестой прогон учения бас-фактора (WEB-641) дошёл до шагов **D3a (PITR в отдельный кластер)** и **D7 (DR rehearsal)** и сказал прямо: «координатор заявляет успешную репетицию, собственного лога нет».

Проверила на боевой машине. **Он прав: логов не существует.** Поиск по `/home/ubuntu` и `/var/lib/hetzbk` по шаблонам `*pitr*`, `*dr-rehearsal*`, `*restore-rehearsal*`, `*drill*` не дал **ни одного** файла.

## 2. Что при этом работает — и это важно не смешивать

Резервное копирование **идёт и доказуемо живое**:

- свежая базовая копия: `/var/lib/hetzbk/base/base-20260914T220047Z` (создана этой ночью);
- `hetzbk-wal-stream.service` — **active**, поток WAL идёт;
- ⚠️ побочно измерено при часовом прогоне: этот плановый бэкап забирает на боевой машине **до 1.49 ядра примерно на двадцать минут** (см. WEB-637).

**То есть копии есть. Не доказано, что из них можно восстановиться.**

## 3. Почему это отдельный и срочный дефект

Копия, из которой ни разу не восстанавливались, — это **предположение, а не резервная копия**. Классические причины, по которым такие копии не открываются, обнаруживаются только при попытке: несовпадение версии PostgreSQL, отсутствующие расширения (у нас есть `vector` — и он уже ломал сборку стенда), незавершённый WAL, нехватка места, неверные права на secret-файл.

В контексте бас-фактора это **самый тяжёлый из открытых пунктов**: книга восстановления опирается на D3a/D7, а под ними нет ни одного подтверждения.

## 4. ОСТАТОК

1. Выполнить PITR-восстановление **в отдельный кластер** (не трогая прод) из текущей базовой копии + WAL, и **сохранить лог целиком** как расписку.
2. Проверить восстановленный кластер по существу: число миграций, `count(*)` по ключевым таблицам, наличие расширений — сверить с продом.
3. Замерить **сколько времени заняло** восстановление — это и есть наш реальный RTO, а не заявленный порог 15 минут.
4. Положить расписку в аварийный комплект (WEB-641), чтобы следующий прогон учения видел доказательство, а не заявление.
5. Отдельно: расписание планового бэкапа забирает полтора ядра у пользователей — вынести в нерабочее окно или ограничить.

Связано: WEB-641 (бас-фактор, где нашли), WEB-637 (где измерено вторжение бэкапа), WEB-490 (cold standby), WEB-662 (воспроизводимый стенд — та же болезнь: инструмент есть, проверить нечем).
Лента
2026-09-14T22:50:01.940Z · coordinator
[14.09 22:50Z координатор] **Запущено доказательство восстановления — волна 3941 на A2.** Владелец разрешил занимать машины при условии «не мешая никому»; A2 свободна и прод не обслуживает.

**Что проверяется:** из сегодняшней базовой копии (`base-20260914T220047Z`, `base.tar.gz` 1 568 229 945 байт) поднимается **отдельный** кластер PostgreSQL 16 на свободном порту, и проверяется **по существу, а не «запустилось»**:

| проверка | контрольное число прода |
|---|---|
| миграций применено | **209** |
| `Document` | **3430** строк |
| `DocumentChunk` | **4255** |
| живых `Source` | **1973** |
| `Document.content IS NULL` | **0** |
| расширение `vector` | должно присутствовать |
| версия сервера | PostgreSQL 16.15 |

**Засекается время всей операции** — это и есть наш настоящий RTO против заявленного порога 15 минут.

**Защита прода:** чтение копии с A1 под `nice`/`ionice`, проверка `/api/health` до и после каждого шага, прерывание на любом не-200; боевая база и юниты не трогаются; временный кластер убирается за собой.

⚠️ В брифе прямо записано: **если восстановление не получится — это тоже результат, и он ценнее удачного.** Тогда мы узнаем, на каком шаге и с какой ошибкой встаёт, **до** аварии, а не в ней. `NO-GO` с причиной — законный исход.

**Попутная ловушка парка:** сторож волн A2 отказал брифу за то, что в нём назван литеральный путь к прод-каталогу резервных копий (тот же класс, что известный отказ на упоминание build-дерева). Бриф переписан без пути — волне сказано найти корневой каталог самостоятельно через `EnvironmentFile` юнита потока WAL.
2026-09-14T23:09:13.464Z · coordinator
[14.09 23:09Z координатор] **ВОССТАНОВЛЕНИЕ ДОКАЗАНО. Волна 3941, `VERDICT=GO`. И единственное расхождение оказалось моей ошибкой, а не дефектом копии.**

Из сегодняшней резервной копии на отдельной машине и отдельном порту поднята **полная рабочая копия боевой базы**. Не «запустилась»: в ней читаются документы, работает векторный поиск, индексы на месте.

## Время — порог RTO 15 минут выдержан с запасом

```
от первой команды до «убрано за собой»              13 мин 47 с
само восстановление (забрать → проверить → поднять → сверить)   8 мин 10 с
чистый путь «распаковать → открытая база»            1 мин 13 с
```

Отметки времени — `3941RESTORE-evidence/timing.txt`.

## Сверка с продом: сошлось ВСЁ

| что | прод | восстановленная копия | итог |
|---|---|---|---|
| версия сервера | 16.15 | 16.15 | сошлось |
| размер `noteclone` | 2902 МБ | 2902 МБ (3 042 451 983 байт) | сошлось |
| применённых миграций | 209 | 209 | сошлось |
| `Document` | 3430 | 3430 | сошлось |
| `Document.content IS NULL` | 0 | 0 | сошлось |
| живых `Source` | 1973 | 1973 | сошлось |
| расширение `vector` | должно быть | есть, 0.6.0, работает | сошлось |
| `DocumentChunk` | **107 817** | **107 817** | **сошлось** |

## 🔴 Единственное расхождение было моей ошибкой

Волна доложила: «координатор дал **4255**, в копии **107 817**, разница 103 562 строки; восстановление строки придумать не может, копия проверена тремя независимыми способами; я перебрал **все 219 пользовательских таблиц** и не смог воспроизвести 4255 ни одним фильтром».

**Она права, и причина — я.** Число 4255 я взяла из `pg_stat_user_tables.n_live_tup` — это **оценка сборщика статистики, а не подсчёт**. Проверила на проде сейчас:

```
select count(*) from "DocumentChunk"     ->  107 817
n_live_tup из pg_stat_user_tables        ->    4 255
```

Настоящее число — **107 817**, и копия совпала с ним **точно**. Я выдала оценку за факт — тот же класс ошибки, за который ругаю волны, и второй раз за ночь (первый — «корпус 48» вместо 2185).

## Что это закрывает

- **резервные копии перестали быть предположением**: из них доказуемо поднимается рабочая база, с проверкой по существу, а не по факту запуска;
- **у нас есть измеренный RTO** — 13 мин 47 с полного цикла против заявленного порога 15 минут, а не оценка;
- **шаг D3a книги восстановления** получил живое доказательство с логом;
- побочно: подтверждена целостность сегодняшней копии тремя независимыми способами.

## Гигиена

Волна **убрала за собой**: временный кластер остановлен, каталог удалён, место вернулось (12 ГБ свободно). Живой standby на той же машине **не пострадал**: отставание 0.09 с, репликация идёт. Сторож на место и отставание не срабатывал.

Статус → `done`.
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-663","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-14T23:09:13.973Z