Копия базы 1С оборвалась, а gzip -t и psql вернули 0

Ежедневную копию баз 1С на тестовом стенде я поставил 2 октября: cron, pg_dump каждой базы через gzip в файл и строчка в журнал. На следующий день одна копия оборвалась. Привычные проверки этого не увидели.
Журнал кончался строкой “End backup DB2”. gzip -t по файлу вернул 0. Я залил файл в пустую базу - снова 0, без единой ошибки. Только индексов в базе не оказалось совсем, а строк набралось примерно 42 % от исходной.
Если ваша ночная копия собрана из cron, pg_dump и gzip, посмотрите на последний файл. Хватит одной строки - zcat копия.sql.gz | tail -n 5 | grep -c ‘dump complete’ показывает, дописал ли pg_dump файл до конца.
У целой копии выходит 1, у оборванной 0. А gzip -t доволен обеими.
Аварии у клиента здесь нет. Стенд наш, и PostgreSQL посреди копии перезапустили мы сами, соседним тестом.
Как было устроено
Стенд - Debian 13.7 и PostgreSQL 18.4 в сборке для 1С. Копия стартует в 17:00 по часам сервера, это МСК+2.
Скрипт обходит базы циклом: “Start backup” в журнал, pg_dump в gzip, “End backup” в журнал. Вторая строка пишется при любом исходе. pipefail не включён. Двадцать с лишним лет в ИТ не помешали мне поставить на свой стенд копию, которая не смотрит ни на код pg_dump, ни на готовый файл.
3 октября, 17:23:05
В 17:22:36 очередь дошла до DB2, она в списке десятая и последняя. Это копия рабочих данных 1С.
Через 29 секунд соседний тест настройки shared_buffers перезапустил PostgreSQL. В журнале sudo осталась команда systemctl restart postgresql@18-main. Строку “End backup DB2” скрипт записал в ту же секунду, 17:23:05.
Связь я вывожу только из этого совпадения. Текста ошибки нет нигде, код pg_dump скрипт не сохранил.

На кадре: журнал копии, размеры копий DB2 за 2 и 3 октября, gzip -t, проверка конца и строка cron. Имя базы в кадре заменено на DB2.
Нижняя строка кадра - из журнала cron: “No MTA installed, discarding output”. Почты на сервере нет, и всё, что задание печатало, ушло в никуда. Было ли там сообщение pg_dump, я могу судить только по времени. Та же строка в журнале cron вашего сервера - повод выяснить, куда деваются сообщения ночной копии.
Вчерашняя копия той же базы снималась три минуты и весила 1 120 951 158 байт. Оборванная - 29 секунд и 95 043 271 байт, почти в 12 раз меньше.
Я по образованию финансист, и для меня такой файл - скрытый долг: лежит рядом с целыми, с записью “End backup”, а платить по нему пришлось бы в день восстановления.
Восстановление из оборванной копии
Файл я залил в пустую базу той же командой, что и базы стенда: zcat в psql без ключей. Через 25 секунд код 0, ошибок ни одной.
В новой базе оказались все 4 687 таблиц, но индексов ноль, а в исходной DB2 их 10 362.
Строк 3 393 049 из 8 060 005, отсюда и 42 %. Число приблизительное: исходную базу я считал уже после копии, а за день число строк в ней меняется на десятки.
Строгая заливка - одной транзакцией, с остановкой на первой ошибке - тоже вернула 0, база вышла такой же недолитой.
Разгадка нашлась в конце файла: последний блок данных закрыт целиком, дальше пустая строка, и psql споткнуться было не обо что.
Убитый pg_dump: код 0 и чистый gzip -t
Файлы мы ломали и руками, на DB11 - синтетической базе теста Гилева. pg_dump убивали сигналом KILL спустя 0,3 секунды после старта. OOM, kill или перезагрузка обрывают копию так же.

Прогон на кадре отдельный, поэтому размеры файлов в нём не те, что ниже, а gilev - настоящее имя DB11. Убитый pg_dump в кадре выдают только команда с timeout -s KILL 0.3 и размер файла.
Без pipefail конвейер вернул 0: его код здесь - код gzip. С pipefail вышло 137, код убитого pg_dump.
gzip -t оба раза ответил 0, потому что gzip получил конец потока и закрыл файл штатно. Файл, у которого 1 октября мы отрезали половину сжатых байтов, gzip -t зато отбраковал с кодом 1, хотя заливка без ключей вернула 0 и на нём.
Файл из прогона с pipefail весил 240 189 байт при 3 987 740 у целого дампа, и проверка конца на нём дала 0. Залитый в пустую базу, он дал код 0, ноль индексов и 15 007 строк из 135 121.
Как стало: фрагмент копии
Одной проверки на все обрывы я не нашёл, поэтому во фрагменте их две: код конвейера и конец файла. Фрагмент я собрал после случая и прогнал 3 октября на маленькой базе. Это не скрипт копии по расписанию.
#!/bin/bash
set -o pipefail
db=$1; dir=$2; log=$dir/backup.log
f=$dir/$db-$(date +%H%M%S).sql.gz
if pg_dump -d "$db" | gzip > "$f" \
&& zcat "$f" | tail -n 5 | grep -q 'dump complete'; then
echo "$(date '+%F %T') OK $db $(stat -c%s "$f")" >> "$log"
else
rc=$?
echo "$(date '+%F %T') FAIL $db код=$rc" >> "$log"
fi
Между двумя OK по часам сервера, в 18:46:14 и 18:46:18 3 октября, стоит FAIL код=137 в 18:46:15. Это второй запуск на DB11: pg_dump в нём прожил 0,3 секунды, его убил таймер, которого в листинге нет.
Первая версия фрагмента записала в журнал “FAIL DB11 код=0”. Подстановка с date в той же строке echo выполняется раньше и затирает $?, поэтому теперь первая команда ветки сохраняет код в rc.
Обрыв 3 октября pipefail поймал бы только при ненулевом коде pg_dump, а этот код не сохранился.
Фрагмент, как и старый скрипт, отдаёт сообщения pg_dump на откуп cron. Что cron с ними делает, видно на первом кадре.
Проверка при заливке
Строгая заливка текстового дампа - это psql с ключом -1 и переменной ON_ERROR_STOP=1.
Наткнувшись на ошибку, psql по умолчанию просто едет дальше. С ON_ERROR_STOP он на ошибке прервётся и вернёт 3, но база всё равно будет недолитой. Глава “SQL Dump” документации PostgreSQL 18 так и пишет: “Either way, you will only have a partially restored database”.
Против недолитой базы глава советует ключ -1: дамп идёт одной транзакцией, и при ошибке ничего не остаётся на полпути - “either fully completed or fully rolled back”. Цена - время: мелкая ошибка, как предупреждает та же глава, может откатить многочасовое восстановление. По-моему, это дешевле недолитой базы, принятой за целую.
Страница psql привязывает -1 к ключам -c и -f, а откат при ошибке обещает только с ON_ERROR_STOP. На файле, обрезанном посередине, строгая заливка дала код 3 и пустую базу и с -f, и через стандартный ввод.
Забыть ON_ERROR_STOP опасно. Файл, оборванный посреди строки данных, одной транзакцией дал пустую базу и код 0 - тоже обоими способами. Скрипт, который смотрит только на код, сочтёт такую базу восстановленной.
Почему база пустая, страница psql не объясняет - гадать не буду.
Custom-архив можно проверить без базы, прогнав pg_restore -f /dev/null вхолостую и посмотрев код возврата.
На половине архива это чтение дало 1 и “не удалось прочитать входной файл: конец файла”, восстановление в базу - тоже 1. А pg_restore -l такую же половину пропустил: код 0, оглавление как у целого. Отрезанный хвост в 64 КБ это чтение не поймало, но и восстановление без него ничего не потеряло.
Чего я не знаю
Почему pg_dump оборвался ровно после целого блока данных, не установлено. Случай один, база одна, в 1С залитую базу я не открывал. Строку конца я искал только в текстовых дампах pg_dump 18.4 и 15.6, а формат directory не проверял ни так, ни через pg_restore. Документация PostgreSQL 18 написана для ванильной СУБД, а стенд работает на сборке под 1С.
Надеюсь, пригодится. Если проверяете custom-архивы больших баз, напишите в комментариях, чем: мой архив весил всего 5,3 МБ, а что будет на сотнях гигабайт, не знаю.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.