Бэкапы, обновление, разбор падения
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Стек выложен на домен и работает. Дальше начинается то, чего в учебниках по фреймворкам не бывает: копия базы, которую никто ни разу не поднимал, обновление версии, после которого данные остаются на месте не сами собой, и падение, которое приходится разбирать по журналу и списку контейнеров, потому что отладчика на сервере нет. Все три занятия выглядят скучно рядом с доменной логикой, и все три решают, сколько проживёт сервис. Разберём, что такое дамп и чего в нём нет, почему единственная проверка копии — это восстановление, что переживает выкат и в каком порядке задают вопросы, когда прод уже лежит.
Данные базы лежат в томе, и том переживает пересоздание контейнера. Отсюда легко сделать вывод, что о сохранности можно больше не думать, — и вывод неверный. Том не спасает ни от команды, которая его стирает, ни от DELETE без WHERE, ни от диска, который кончился, ни от VPS, выключенного хостингом за неоплату. Во всех этих случаях данные пропадают вместе с томом или внутри него. Бэкап — это отдельная копия, которая лежит не там же.
У PostgreSQL копия бывает двух родов. Логическая — текст SQL, который восстанавливает схему и строки обычными командами: её снимает pg_dump, после сжатия она невелика, читается глазами и переносится между мажорными версиями. Физическая — побайтная копия каталога данных вместе с журналом упреждающей записи; она умеет «на минуту до аварии», но требует того же мажора сервера и места под журналы. Одному VPS с одной базой хватает первой, и весь раздел про неё.
Первая ловушка ждёт там, где её не ищут, — в версии клиента. Сервер у нас 18-й, а pg_dump на хосте приехал с каким-то другим пакетом и оказался 17-м. Прогноз: что напечатает каждый из двух клиентов?
# Один и тот же сервер, два клиента pg_dump разных мажорных версий.
# Каталоги клиентов приходят в CLIENTS: на хосте это тот postgresql-client,
# который поставил пакетный менеджер, в контейнере — клиент самого образа.
set -eu
export PGUSER=${POSTGRES_USER:-app}
DB=${POSTGRES_DB:-app_db}
echo "сервер: $(psql -tAc 'show server_version' -d "$DB")"
for DIR in $CLIENTS; do
echo "--- клиент pg_dump $("$DIR/pg_dump" --version | cut -d' ' -f3)"
if "$DIR/pg_dump" -d "$DB" > dump.sql 2> err.txt; then
echo "дамп снят, строк SQL $(wc -l < dump.sql | tr -d ' ')"
else
echo "код выхода $?"
cat err.txt
fi
done
rm -f dump.sql err.txtсервер: 18.4 (Homebrew) --- клиент pg_dump 17.5 код выхода 1 pg_dump: error: aborting because of server version mismatch pg_dump: detail: server version: 18.4 (Homebrew); pg_dump version: 17.5 --- клиент pg_dump 18.4 дамп снят, строк SQL 259
Клиент 17-й версии не знает каталогов 18-й и отказывается сам, кодом 1, не сняв ни строки. Отказ громкий и честный: половина дампа была бы хуже. Отсюда правило боевого сценария — pg_dump запускают внутри контейнера базы, а не на хосте: там клиент и сервер приехали одним образом и разъехаться не могут.
Строка set -euo pipefail несёт больше, чем кажется. Код выхода трубы — это код последней команды, а последняя тут gzip: упавший pg_dump отдал бы ему пустой поток, тот записал бы 20-байтовый архив и вернул ноль. Файл есть, дата свежая, сценарий доволен. Спасает pipefail — с ним сценарий обрывается до ротации и не успевает удалить рабочий дамп в пользу пустого.
Но проверять всё равно надо не «файл появился», а сам файл, и делать это должен не человек. Минимальный монитор — две проверки: свежесть и размер против того, каким этот дамп обычно бывает. Прогноз: сколько файлов из пяти он признает годными и что скажет про последний?
Свежесть у последнего файла в порядке, а размер — нет, и это самый частый вид мёртвого бэкапа: снимается каждую ночь, файл появляется, дата новая. Порог «не меньше половины медианы» груб, зато не требует знать, сколько весит база, растёт вместе с ней и ловит ровно тот случай, когда дамп не снялся вовсе.
Проверка, которая кажется разумной и не работает: сравнить два дампа побайтно. У PostgreSQL 18 в начале и конце файла стоит \restrict со случайным ключом, и два дампа одних и тех же данных не совпадают. Сравнивают содержимое базы, а не файлы.
Теперь о том, чего в дампе базы нет. Роли и их пароли — объекты кластера, а не базы, и pg_dump их не берёт; при этом строки вида ALTER TABLE … OWNER TO app в дампе есть, то есть роль app обязана существовать раньше восстановления. Фотографии лежат не в базе. Файла с секретами в базе нет вовсе.
| Что | Чем спасается | Как это устроено в стеке учебника |
|---|---|---|
| Схема и строки базы | pg_dump внутри контейнера базы | ночной deploy/backup.sh, дампы в deploy/backups/, последние четырнадцать |
| Роли и пароли кластера | pg_dumpall --globals-only | не снимается: роль заводит сам образ по переменным стека |
| Фотографии | зеркало объектного хранилища | одноразовый контейнер с mc mirror, туда же в backups/ |
| Файл с секретами | копия вне сервера | .env.prod живёт на VPS и в вашем хранилище паролей |
| Образы | пересборка из репозитория | ни в дампе, ни в томе их нет — и не нужно |
А вот что в дампе есть, и об этом забывают: журнал миграций. Отметки о применённых шагах живут в самой базе, в отдельной схеме, и уезжают в дамп вместе с данными. База, поднятая из копии, для раннера миграций выглядит как база, где все шаги уже применены, — и это верно, схема в ней именно такая.
Лишние дампы убирает ls -1t — список по времени изменения, свежие первыми, — а tail -n +15 оставляет четырнадцать. Сортировка по имени дала бы то же самое при одном условии: дата в имени записана от крупного к мелкому, 2026-09-28. С другим форматом порядок имён перестаёт быть порядком дат, и ротация начинает удалять самое свежее. Это первая задача статьи.
И второе: четырнадцать дампов на том же VPS переживут ошибку в приложении, но не смерть сервера. Копию периодически забирают к себе — тем же scp, которым увозят зеркало фотографий.
Дамп, который ни разу не поднимали, — это не копия, а файл. Между «pg_dump вышел с нулём» и «из этого файла встаёт работающая база» лежит целый класс поломок: прав не хватило на часть объектов, дамп снимали с фильтром по схеме, роль владельца в новой базе называется иначе, дамп снимали не с той базы. Каждая из них видна только при восстановлении.
Проверка — три шага: снять дамп, поднять его в чистой базе, сверить содержимое с тем, откуда снимали. Третий обычно и пропускают, ограничившись «команда не отказала». Прогноз: что напечатает сверка и какие таблицы попадут в её список?
# Проверка бэкапа: снять дамп, поднять его в ЧИСТОЙ базе и сверить содержимое.
# На боевой машине это идёт внутри контейнера с базой — клиент там заведомо
# той же мажорной версии, что и сервер:
# docker compose exec -T postgres sh /backup/restore-check.sh
# Хост и порт libpq берёт из окружения; внутри контейнера это локальный сокет.
set -eu
DB=${POSTGRES_DB:-app_db}
FRESH=${DB}_check
export PGUSER=${POSTGRES_USER:-app} PGDATABASE=$DB
echo "сервер: $(psql -tAc 'show server_version'), pg_dump: $(pg_dump --version | cut -d' ' -f3)"
mkdir -p backups
DUMP=backups/${DB}_$(date +%F_%H-%M).sql.gz
# дамп собирается файлом, а не трубой в gzip: у трубы код выхода берётся от
# последней команды, и падение pg_dump в нём теряется
pg_dump > "$DUMP.plain"
gzip -f "$DUMP.plain" && mv "$DUMP.plain.gz" "$DUMP"
echo "дамп: $DUMP, строк SQL $(gunzip -c "$DUMP" | wc -l | tr -d ' ')"
PGOPTIONS='-c client_min_messages=warning' dropdb --if-exists "$FRESH"
createdb "$FRESH"
# ON_ERROR_STOP=1: восстановление обязано остановиться на первой же ошибке
if gunzip -c "$DUMP" | psql -q -o /dev/null -v ON_ERROR_STOP=1 -d "$FRESH"; then
echo "восстановление в чистую базу $FRESH: без ошибок"
else
echo "восстановление в чистую базу $FRESH: psql отказал"
exit 1
fi
psql -tAF' ' -d "$DB" -f check.sql > before.txt
psql -tAF' ' -d "$FRESH" -f check.sql > after.txt
cat after.txt
if diff -q before.txt after.txt > /dev/null; then
echo "сверка: содержимое совпало"
else
echo "сверка: РАСХОЖДЕНИЕ"
diff before.txt after.txt
fi
dropdb "$FRESH"
rm -f before.txt after.txtсервер: 18.4 (Homebrew), pg_dump: 18.4 дамп: backups/app_db_<дата>.sql.gz, строк SQL 259 восстановление в чистую базу app_db_check: без ошибок drizzle.__drizzle_migrations 1 2312873a52d92ca62794a1e90ee21023 public.notes 28 cc261ab230be28604ba2a5d3cd54089a public.users 3 1d7010d87bc37d3c659b05d3678fb578 сверка: содержимое совпало
Таблиц в списке три, а не две: рядом с notes и users стоит журнал миграций из отдельной схемы. Сверка идёт не по числу строк, а по контрольной сумме содержимого — число строк совпадает и у таблицы, в которой перепутаны значения. Порядок внутри суммы задан явно: иначе она зависела бы от того, в каком порядке база вернула строки, а этого база не обещает.
Слово «чистая» во втором шаге — не про аккуратность. Что psql после ошибки дочитывает файл до конца и выходит с нулём, вы уже видели на трёх вставках; здесь та же механика стоит дороже. Прогноз: тот же дамп поднимают в базу, где схема уже есть и строки тоже, — сколько удалённых строк вернётся на место?
# Восстановление дампа ПОВЕРХ базы, в которой уже есть схема и строки, —
# так делают, когда «надо срочно вернуть удалённое».
# Хост и порт libpq берёт из окружения; PGUSER и имя базы — как в compose.
set -eu
export PGUSER=${POSTGRES_USER:-app}
DB=${POSTGRES_DB:-app_db}
LIVE=${DB}_live
# Дамп «за прошлую ночь»
pg_dump -d "$DB" | gzip > before.sql.gz
# Сцена: та же база, из которой кто-то удалил черновики. Это и есть инцидент,
# после которого хочется вернуть строки из ночного дампа
PGOPTIONS='-c client_min_messages=warning' dropdb --if-exists "$LIVE"
createdb "$LIVE"
gunzip -c before.sql.gz | psql -q -o /dev/null -v ON_ERROR_STOP=1 -d "$LIVE"
psql -q -d "$LIVE" -c "DELETE FROM notes WHERE status = 'draft'"
echo "после инцидента: users $(psql -tAc 'SELECT count(*) FROM users' -d "$LIVE"), notes $(psql -tAc 'SELECT count(*) FROM notes' -d "$LIVE")"
# Восстановление так, как его запускают в спешке: psql без ON_ERROR_STOP
set +e
gunzip -c before.sql.gz | psql -q -o /dev/null -d "$LIVE" 2> errors.txt
CODE=$?
set -e
echo "восстановление поверх: код psql $CODE, строк с ERROR $(grep -c '^ERROR' errors.txt | tr -d ' ')"
echo "первая ошибка: $(head -1 errors.txt)"
echo "после восстановления: users $(psql -tAc 'SELECT count(*) FROM users' -d "$LIVE"), notes $(psql -tAc 'SELECT count(*) FROM notes' -d "$LIVE")"
# То же самое, но psql остановлен на первой ошибке
set +e
gunzip -c before.sql.gz | psql -q -o /dev/null -v ON_ERROR_STOP=1 -d "$LIVE" 2> /dev/null
echo "то же с ON_ERROR_STOP=1: код psql $?"
set -e
dropdb "$LIVE"
rm -f errors.txt before.sql.gzпосле инцидента: users 3, notes 8 восстановление поверх: код psql 0, строк с ERROR 17 первая ошибка: ERROR: schema "drizzle" already exists после восстановления: users 3, notes 8 то же с ON_ERROR_STOP=1: код psql 3
Ни одной: заметок было восемь, восемь и осталось. Семнадцать строк с ERROR в потоке ошибок — и ноль в коде выхода, то есть единственное, о чём этот ноль сообщает, — что файл дочитан. А вернуть удалённое дамп и не мог: COPY в нём — одна команда на всю таблицу, и она падает целиком на первом же конфликте ключа, не вставив ни строки. Тот же запуск с ON_ERROR_STOP=1 кончается кодом 3.
Восстановление поверх живой базы не годится ещё и потому, что дамп ничего не удаляет: строки, появившиеся после него, останутся, а удалённые не вернутся. Выйдет смесь двух состояний, которой не было никогда.
Из этого складывается регламент восстановления — короткий документ, написанный заранее: где лежит копия, кто её поднимает, в какую базу, чем сверяет и сколько это занимает по времени. Последнее важнее, чем кажется: восстановление гигабайтного дампа — минуты, а не секунды, и знать их лучше до аварии, а не во время.
Текстовый дамп поднимается psql. Свой формат -Fc и каталог -Fd поднимает pg_restore: он умеет выборочно — только схему, только данные, одну таблицу — и в несколько потоков. Перепутать не получится: pg_restore, которому дали текстовый дамп, отвечает «input file appears to be a text format dump. Please use psql» и выходит кодом 1 (проверено на 18.4).
Код выхода psql тоже стоит знать заранее: 2 — не удалось соединиться, 3 — ошибка в сценарии при ON_ERROR_STOP. Ноль означает «файл дочитан», и только сверка отличает его от «база восстановлена».
Обновление версии на сервере — это три команды, и вся работа выката прячется в двух последних: первая только привозит код.
Сборка неслучайно отдельная команда: up -d --build собирал бы всё, что разошлось, разом, а на двух ядрах VPS три параллельные сборки уже роняли прод — поэтому сценарий выката автора собирает по одному образу и только потом поднимает стек. Сколько займёт вторая команда, решает при этом не мощность сервера, а ключ шага: правка обработчика не должна сбивать установку зависимостей. Отдельной команды для схемы в этом списке нет намеренно: миграция — сервис стека, который приложение дожидается по условию успешного выхода, потому что применять миграцию стартом приложения нельзя — копий приложения бывает больше одной. Выкат без правок схемы от этого не дорожает: джоба выходит с нулём, не сделав ничего.
Перед всеми тремя стоит нулевая: снять дамп. Не потому, что миграция обязательно сломается, а потому, что шаг, который её ломает, чаще всего не отказывает, а тихо портит данные, — и без копии «до» это не лечится.
Дальше начинается то, из-за чего выкат вообще может стоить данных. Контейнер — вещь одноразовая, и up -d после пересборки образа заменяет его целиком.
| Что | Переживает пересоздание контейнера | Почему |
|---|---|---|
| Строки базы в именованном томе | да | том живёт отдельно от контейнера и подключается к новому |
| Файлы, записанные внутрь контейнера | нет | файловая система контейнера удаляется вместе с ним |
| Журнал контейнера | нет | журнал принадлежит контейнеру; новый начинает с пустого |
| Каталог данных при смене мажора образа базы | нет | каталог помечен мажором, который его создал |
Последняя строка — самая дорогая. В каталоге данных лежит файл с номером мажора, и сервер другой версии этот каталог своим не считает. У образов с 18-й версии он вложен в том подкаталогом с номером мажора, так что образ нового мажора в этот каталог даже не заглядывает — зато находит его рядом со своим и вместо тихой пустой базы печатает объяснение и выходит с кодом 1. Итог один: одна правка тега образа с 18 на 19 превращает выкат в остановку базы, а лечится она не откатом тега, а переносом (pg_upgrade, которому нужны обе версии сразу) или дампом, снятым до обновления.
Остаётся окно, в котором сайт мигает: прокси пересоздание пережил и продолжает работать, а приложения за ним несколько секунд нет, и читатель получает от прокси отказ. На одном VPS с одной копией приложения это окно не убрать — его можно только сделать коротким и знать, когда оно кончилось. Для второго и нужна ручка готовности вместе с пробой в описании стека: «контейнер запущен» и «сервис отвечает» — разные события, и выкат заканчивается на втором.
Старые образы копятся и занимают заметное место, но чистка — не действие момента аварии. При пересборке новый образ получает то же имя, и прежний остаётся без тега, то есть для команды чистки он мусор: после неё откатываться уже не на что.
«Откатимся» — самое частое предложение в первые минуты аварии, и почти всегда оно означает «вернуть прежний образ». С образом это и правда просто: он неизменяемый, лежит под своим тегом, и выкат прежнего тега — обычный выкат. Проблема в том, что образ — единственная часть стека, которая так умеет.
Схема назад не ходит. Миграция уже применена, и если она была расширяющей — добавила колонку, добавила таблицу, — прежний код на новой схеме работает: он про новое просто не знает. Если она что-то убрала или переименовала, прежний код на ней падает, и откат образа делает только хуже. Поэтому шаги схемы и разбиваются на выпуски: не ради красоты, а чтобы у отката оставалась хоть одна рабочая пара «код плюс схема».
Копия базы — тоже не откат, а возврат в прошлое: всё, что произошло после дампа, исчезает. Восстановление уместно там, где данные испорчены, и неуместно там, где просто выкатили плохой код: во втором случае дешевле выпуск вперёд.
А есть и то, чего восстановление не возвращает вовсе, потому что оно не в базе, — состояние клиентов. Прогноз: сервер синхронизации поднят из ночного дампа, в котором были только две первые операции. Телефон помнит курсор, взятый до аварии. Что он заберёт при следующем обращении?
На живой базе дыра между откатившимся счётчиком и курсором телефона — не одна запись: сколько номеров человек сжёг после того, как дамп был снят, столько новых правок и пропадёт молча.
Сдвиг закрывает половину задачи: телефоны снова начинают получать новые правки. Вторую половину закрыть нельзя. Операции, которые копия не сохранила, телефоны уже убрали из своей очереди — они получили по ним вердикт и считают их отправленными. Для человека это выглядит как несколько пропавших записей за последний день, и единственное честное действие — сказать об этом, а не искать их в базе.
Отсюда правило: в аварии откат — это выпуск вперёд. Прежний тег образа годится как способ выиграть время, но починка почти всегда оказывается новым выпуском, а не возвратом.
Когда прод лежит, соблазн один: открыть журнал и читать его сверху вниз. Это худший способ потратить первые десять минут. Вопросов всего три, и они идут по порядку: что именно сломано, с какого момента, что менялось до этого момента. Первый отвечает состояние контейнеров, второй и третий — журнал.
Собирать картину по сквозному номеру запроса вы уже умеете: сводка по запросам даёт число отказов, причину и маршруты, а одна причина на двух маршрутах значит, что сломалась общая зависимость. Здесь нужен следующий шаг — момент начала. И на нём легко обмануться, потому что самая заметная ошибка редко бывает первой.
Прогноз: в журнале десять записей от двух контейнеров, и в файле они не по времени. Какая ошибка окажется самой частой, какая — первой и что стояло в журнале перед ней?
Самая частая ошибка — «база не ответила», пять раз из шести, и починка началась бы с неё. Первая по времени другая: свободные соединения в пуле кончились раньше, чем база стала отказывать в новых. Порядок здесь и есть подсказка: сначала уже открытые соединения перестали отвечать и заняли весь пул, и только потом попытки открыть новое начали получать отказ. Смотреть надо на первую строку — она ближе всего к событию, которого в журнале приложения может не быть вовсе.
Вторая половина ответа — строка перед первой ошибкой. Последний запуск приложения случился за двадцать девять секунд до неё, и версия в нём новая. Это не доказательство вины выката, но это первое, что проверяют: совпадение по времени с выкатом сужает поиск до одного набора изменений. Если же ближайший выкат был три дня назад, искать надо не в коде, а в том, что менялось снаружи, — в объёме данных, в диске, в чужом сервисе.
Сортировка по строковому времени работает, только если время в журнале записано в UTC и с ведущими нулями. Смешанные часовые пояса от двух машин превращают «первую ошибку» в лотерею.
Отчёт нужен не начальству, а вам через полгода, поэтому он короткий: окно (с какой по какую минуту), цена (сколько запросов отказало и скольких людей задело), первая ошибка, причина, что сделали, что изменили, чтобы это не повторилось. Всё, кроме последних двух строк, считается из журнала — этим занята вторая задача статьи.
Строка «что изменили» — единственная, которая делает отчёт полезным. Если в ней написано «перезапустили», инцидент вернётся.
Первый вопрос — «что именно сломано» — отвечает список контейнеров. Отвечает он, впрочем, ровно на то, о чём спросили: по умолчанию список показывает работающие контейнеры — в том числе те, что крутятся в перезапуске, — а вышедшую одноразовую работу только с флагом -a. Ровно на этом и строится самая частая ошибка разбора.
Прогноз: пять сервисов, у одного код выхода 1, у другого 137 и шесть перезапусков. Кого модель назовёт первым подозреваемым в списке без -a и кого — в полном?
В коротком списке виноват api: он не может стартовать, его перезапускают, он снова падает, а код 137 уводит разбор в память машины. В полном списке первым оказывается migrate: одноразовая работа вышла с кодом 1, то есть миграция не применилась и схема осталась прежней. Её журнал читают первым: строк там немного, и они отвечают, доехал ли выкат до конца. Память и шесть перезапусков api — вопрос второй, и пока не прочитан первый, чинить начинают не то.
| Что видно | Что это значит | Что спросить дальше |
|---|---|---|
exited и код 0 у одноразовой работы | она отработала и вышла — так и задумано | ничего; это нормальное состояние джобы |
exited и код не 0 | работа отказала и выкат идти дальше не должен | её журнал целиком: строк там немного |
restarting, перезапусков много | процесс не доживает до готовности | журнал за последнюю попытку, а не за всё время |
| код 137 | процесс убит сигналом 9 — обычно кончилась память | сколько памяти на машине и что там ещё собиралось |
running, но unhealthy | процесс жив, а проба не проходит | что именно спрашивает проба и чем отвечает сервис |
running и healthy у всех | сломано не то, что вы смотрите | прокси и то, что снаружи: домен, сертификат, DNS |
Дальше — журнал, и его берут не целиком. Окно задаётся флагом «с какого момента» (--since 20m или отметка времени), последние строки — флагом числа строк, а отметки времени включаются отдельно: контейнер печатает свои, а не время получения. Полезное упражнение — выписать окно инцидента из предыдущего раздела и взять журнал ровно по нему: всё остальное только мешает.
Код 137 — это 128 плюс 9. Так же читаются и остальные: 143 — это сигнал 15, то есть штатная остановка, а вовсе не авария.
Журнал принадлежит контейнеру, а не сервису: пересоздание уносит его целиком. Это значит, что после up -d с пересобранным образом строки предыдущей версии недоступны — а именно они чаще всего и нужны, когда авария началась до выката. Отсюда и правило про поток вывода: строки, которые нужны после пересоздания, обязан складывать наружу кто-то снаружи контейнера — сборщик логов, читающий этот поток.
У контейнера в цикле перезапусков обратная беда: строк много, и почти все они от прежних попыток. Помогает флаг числа строк — взять последние несколько десятков, посмотреть, чем кончается попытка, и только потом решать, нужна ли остальная история.
- Том — не бэкап: он переживает пересоздание контейнера и не переживает ни команды, которая его стирает, ни
DELETEбезWHERE, ни смерти сервера. Бэкап базы — это дамп, снятыйpg_dumpвнутри контейнера базы: клиент младшей версии со старшим сервером работать отказывается вовсе. - В дампе базы нет ролей и паролей кластера, файлов и секретов, зато есть журнал миграций — база из копии для раннера уже применённая. Поэтому «бэкап» — это всегда несколько разных копий, а не один файл.
- Файл появился — ещё не дамп: труба отдаёт код последней команды, и упавший
pg_dumpоставляет 20-байтовый архив при нуле на выходе. Лечитсяpipefailв сценарии и монитором, который смотрит свежесть и размер. - Копию проверяет только восстановление, и только в чистую базу со сверкой содержимого по контрольной сумме. Поверх живой базы тот же дамп даёт семнадцать ошибок, ноль в коде выхода и ни одной вернувшейся строки:
psqlбезON_ERROR_STOPсообщает лишь о том, что дочитал файл. - Откатывается назад только образ. Схема назад не ходит, поэтому её меняют выпусками; восстановление базы отбрасывает всё, что было после дампа, а состояние клиентов не возвращает вовсе: счётчик синхронизации после восстановления сдвигают за любой выданный номер, иначе всё, что попадёт в дыру между новой головой и курсором телефона, тот не увидит — без ошибки и без отказа.
- Разбор падения идёт по порядку: что сломано (список контейнеров, обязательно с вышедшими работами), с какого момента (первая ошибка по времени, а не самая частая) и что менялось до этого момента. Журнал берут окном по этому моменту, а не целиком, — тем более что пересоздание контейнера уносит журнал прежней версии.
1/4Почему pg_dump в боевом сценарии запускают внутри контейнера базы?
2/4Дамп подняли в базу, где схема и строки уже есть. psql вышел с нулём. Что это означает?
3/4Сервер синхронизации поднят из дампа, в котором не хватает последних операций. Что увидит телефон с прежним курсором?
4/4В журнале шесть ошибок: пять «база не ответила» и одна «нет свободного соединения», и она первая по времени. С чего начинать?
Ротация, которая удаляет самое свежее
Сценарий оставляет три самых свежих дампа, остальные отдаёт под удаление. Имена приходят от старого сценария: дата в них записана как день.месяц.год. Сейчас функция сравнивает имена как строки — и под удаление уходят как раз свежие файлы. Почините её так, чтобы удалялись два самых старых.
Строковое сравнение работает только при формате от крупного к мелкому. Значит, из имени надо достать дату и переставить её части:
Теперь под удаление уходят 29.09.2026 и 30.09.2026. Вывод из этого не про регулярные выражения: дату в имени файла пишут %F — тогда порядок имён и порядок дат совпадают сами, и сортировать можно чем угодно. Сценарий автора идёт другим путём и сортирует по времени изменения файла, но у имён, приехавших из объектного хранилища, времени может и не быть.
Цена инцидента для отчёта
В отчёт нужны две цифры: сколько запросов отказало и скольких людей это задело. Сейчас считаются строки журнала, а у одного запроса строк с ошибкой бывает больше одной — и оба числа выходят завышенными. Посчитайте запросы и людей.
Считать надо разные значения полей, а не строки:
Выходит шесть запросов и три человека против восьми и восьми. Разница почти втрое по людям — это и есть причина, по которой в отчёт идут именно эти два числа: восемь отказов у трёх человек и восемь отказов у восьми — разные инциденты, и разговаривать с пользователями после них надо по-разному.
Сверка, которая не заметила расхождения
Сверка после восстановления сравнивает только числа строк и только у таблиц, которые есть справа. На этих данных она печатает «содержимое совпало», хотя расхождений два: у одной таблицы не сходится контрольная сумма, а одной таблицы справа нет вовсе. Сделайте так, чтобы она назвала оба.
Обходить надо объединение имён, а сравнивать — и число строк, и сумму:
Печатает два расхождения: drizzle.__drizzle_migrations (справа нет) и public.notes (сумма другая). Первое — тот самый журнал миграций, который легко потерять, сняв дамп с фильтром по схеме; второе — случай, ради которого сверка и считает сумму, а не строки.