MAATRIX / Блог / Целостность без ZFS: как ловить порчу файлов на обычной ext4

Целостность без ZFS: как ловить порчу файлов на обычной ext4

MAATRIX

Дамп базы данных лежит на диске полгода, бэкап-скрипт каждую ночь честно отчитывается «OK», а когда доходит до реального восстановления — файл разворачивается наполовину, а pg_restore падает с ошибкой о повреждённых данных. Диск не выдал ни одной ошибки чтения, мониторинг молчал. Если сервер стоит на ext4 (самой распространённой файловой системе на Linux-серверах), у него просто нет встроенного механизма, который заметил бы, что несколько байт в файле однажды стали не теми, что были записаны. Разберём, как закрыть эту дыру самостоятельно — без миграции на другую файловую систему.

Почему это особенно больно для баз данных

Тихая порча данных (silent data corruption) бьёт по любым файлам, но дампы и архивы баз данных — едва ли не худший случай. Три особенности превращают редкую проблему в реальный риск:

  • Это «холодные» файлы. Дамп создаётся ночью, заливается в хранилище бэкапов и в норме не открывается месяцами — именно долгое нетронутое лежание даёт деградации носителя накопиться незамеченной.
  • Проверка «на глаз» не работает. Бинарный дамп PostgreSQL или сжатый mysqldump.gz с испорченным байтом в середине либо не распаковывается вообще, либо распаковывается частично и создаёт ложное чувство, что бэкап цел.
  • Обнаруживается в худший момент. Не при плановой проверке, а тогда, когда прод уже упал и восстановление нужно прямо сейчас, под давлением.

Если вы проверяете «работоспособность бэкапа» только по коду возврата pg_dump или mysqldump, вы проверяете, что дамп *создался*, а не что он *останется читаемым* через три месяца хранения. Разница между этими гарантиями — ровно то, о чём эта статья.

Что видит и чего не видит ext4

У классических файловых систем вроде ext4 или XFS есть контроль целостности, но он касается только метаданных — самой структуры файловой системы: журнала транзакций, таблиц inode, каталогов. Если после сбоя питания журнал не сходится, fsck это заметит и починит. Но у содержимого обычного файла — тех самых байт дампа базы — никакой контрольной суммы на уровне файловой системы нет. Когда диск возвращает сектор, ext4 верит ему на слово: раз чтение завершилось без ошибки ввода-вывода, значит, данные корректны.

Проблема в том, что диск может отдать не те данные, которые записывал, и не сообщить об ошибке. Источников расхождения несколько: постепенная деградация магнитного слоя на HDD или заряда ячеек NAND на SSD, единичный сбой прошивки контроллера, неисправность SATA/SAS-кабеля, редкий сбой бита в памяти без ECC-коррекции. Ни один из случаев не редкость сам по себе — вероятность растёт с объёмом данных и временем хранения, а не проявляется на разовом тесте диска.

Есть отдельный класс файловых систем, спроектированных иначе: они хранят контрольную сумму для каждого блока данных отдельно от самого блока (обычно в дереве метаданных родительского уровня) и пересчитывают её при каждом чтении. Если сумма не совпала — файловая система знает о повреждении в момент чтения, а не когда кто-то случайно наткнётся на битый файл. При наличии избыточности (зеркало, несколько копий) такие системы способны восстановить блок автоматически, без участия администратора — контроль встроен в путь чтения, а не пристроен снаружи. Сравнение таких систем с ext4 и XFS — в статье ext4, XFS, ZFS, btrfs: как выбрать. Здесь же речь о другом: что делать, если переезд сейчас не вариант — сервер уже развёрнут, миграция дорога, а данные всё равно нужно защитить.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Идея вручную: контрольная сумма, хранимая отдельно от файла

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

Ключевое слово здесь — «отдельно». Если хранить сумму в том же каталоге, на том же диске, что и файл, вы защищаетесь только от одного класса проблем (случайное изменение содержимого), но не от другого — деградации самого носителя, которая с равной вероятностью исказит и файл, и лежащую рядом сумму. Разумные варианты хранения:

  • отдельный диск или раздел на том же сервере (минимальная защита, но лучше, чем ничего);
  • отдельный сервер, куда льются сами бэкапы, но в отдельном пути;
  • объектное хранилище (S3-совместимое) — манифест весит килобайты, а не гигабайты, хранить его там дёшево;
  • git-репозиторий на отдельном сервере — заодно получаете историю: видно, когда именно изменилась сумма файла.

Команда для одного файла:

sha256sum /backup/pg/2026-08-30/full.dump.gz > /backup/pg/2026-08-30/full.dump.gz.sha256

Для целого каталога с бэкапами — один манифест на весь набор файлов за день:

find /backup/pg/2026-08-30 -type f -name '*.dump.gz' -print0 \
  | sort -z \
  | xargs -0 sha256sum > /var/lib/db-integrity/manifest-2026-08-30.sha256

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

scp /var/lib/db-integrity/manifest-2026-08-30.sha256 \
    integrity@backup-host:/srv/integrity-manifests/pg/

sha256sum — разумный выбор по умолчанию: устойчив к случайным искажениям, доступен из коробки почти на любом дистрибутиве и не требователен к CPU на объёмах, типичных для дампов базы. Более старый md5sum тоже технически справится с задачей обнаружения случайной порчи (речь не о защите от намеренной подделки, где MD5 слаб), но раз вы пишете инфраструктуру заново — нет причины закладываться на более слабый алгоритм.

Периодическая сверка и что делать с расхождением

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

cd /backup/pg/2026-08-30 && sha256sum -c /var/lib/db-integrity/manifest-2026-08-30.sha256

Команда построчно сверяет текущее содержимое каждого файла с сохранённым хешем и печатает OK или FAILED. Для автоматизации важен не сырой вывод, а отфильтрованный:

sha256sum -c /var/lib/db-integrity/manifest-2026-08-30.sha256 2>&1 | grep -v ': OK$'

Если строка вида full.dump.gz: FAILED появилась для файла, который заведомо не редактировался после создания (а бэкапы дампов обычно и не редактируются — это одноразовые снапшоты, а не живые файлы), это прямой сигнал: содержимое изменилось без вашего участия, то есть один из вариантов тихой порчи.

Когда расхождение найдено, порядок действий такой: не удалять файл сразу (даже частично битый дамп иногда можно распаковать до места повреждения и вытащить хотя бы часть данных); проверить, есть ли более старая исправная копия — предыдущий цикл бэкапа, реплика, снапшот на другом узле хранения; проверить SMART и логи диска (smartctl -a /dev/sdX, dmesg | grep -i -E 'ata|scsi') — единичная порча иногда предвестник начинающейся деградации носителя, стоит успеть снять свежую копию, пока диск ещё читается штатно; и, если сама база данных жива и цела, просто пересоздать бэкап заново — испорчен дамп на диске, а не источник данных.

Отдельно стоит различать два похожих по симптому, но разных по смыслу случая: файл, который *осознанно* перезаписывается (текущий лог, рабочая директория), и файл, который *не должен* меняться после создания (архивный дамп). Схема с фиксированной суммой годится именно для второго случая — для «холодных» данных. Живые, часто меняющиеся файлы такой подход будет считать «испорченными» при каждом легитимном изменении; для них нужна другая стратегия — версионированные снапшоты бэкапа, а не статичный манифест.

Инкрементальная проверка: не пересчитывать всё заново каждый раз

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

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

#!/bin/bash
# /usr/local/bin/db-integrity-rotate-check.sh
# Раз в сутки проверяет один "срез" по кругу -- весь архив проходит проверку за N дней

BASE=/backup/pg
MANIFEST_DIR=/var/lib/db-integrity
LOG=/var/log/db-integrity-check.log

DIRS=($(find "$BASE" -maxdepth 1 -mindepth 1 -type d | sort))
IDX=$(( $(date +%j) % ${#DIRS[@]} ))
TARGET_DIR="${DIRS[$IDX]}"
TARGET=$(basename "$TARGET_DIR")
MANIFEST="$MANIFEST_DIR/manifest-$TARGET.sha256"

[ -f "$MANIFEST" ] || { echo "$(date -Is) нет манифеста для $TARGET" >> "$LOG"; exit 0; }

RESULT=$(cd "$TARGET_DIR" && sha256sum -c "$MANIFEST" 2>&1 | grep -v ': OK$')
echo "$(date -Is) проверка среза $TARGET" >> "$LOG"
if [ -n "$RESULT" ]; then
  echo "$RESULT" >> "$LOG"
  echo "$RESULT" | mail -s "ALERT: db integrity FAILED for $TARGET" admin@example.com
fi

Каждый файл проверяется не ежедневно, а раз в цикл — но по кругу проходит весь архив, а не только свежая часть, и пиковой нагрузки от полной сверки не возникает.

Инструменты, которые автоматизируют это за вас

Связка find + sha256sum + cron рабочая для небольшого набора файлов, но у неё есть предсказуемые ограничения: нет истории изменений, нет удобного diff «что именно поменялось», нет инкрементальности из коробки. Если объём данных растёт, есть смысл посмотреть на готовые инструменты рядом:

ИнструментЧто делаетКогда уместен
sha256sum/shasum + find + cronБазовый расчёт и сверка суммНебольшой набор файлов, полный контроль над логикой
hashdeep / md5deepСчитает суммы рекурсивно, хранит базу, сравнивает два дерева каталогов и печатает только расхожденияСредние объёмы, нужен готовый diff без своего парсера
AIDEПолноценная база с суммами и метаданными файлов, изначально для контроля вторжений, но применима и к «холодным» даннымЗаодно нужны права доступа, владелец, время изменения
par2 (Parchive)Не просто обнаруживает повреждение, а создаёт избыточные данные для восстановления повреждённых участков без исходной копииАрхивы, которые критично уметь восстановить без копии в другом месте
rclone check / restic check / borg checkВстроенная проверка целостности, если бэкапы уже хранятся через эти инструментыОдин из них и так используется для бэкапов

Отдельно стоит сказать про par2: в отличие от остальных вариантов в таблице, он не просто *обнаруживает* порчу, а может *восстановить* повреждённые байты без обращения к другой копии файла — за счёт заранее посчитанной избыточности (похоже по идее на RAID, но на уровне отдельного файла), ценой места на диске под recovery-данные. Для по-настоящему критичных архивов, которые нельзя быстро перезалить из другого места, это разумное дополнение к обычным суммам, а не замена им.

Если бэкапы БД уже льются через специализированный инструмент, а не голым pg_dump в файл, — возможно, встроенная проверка целостности уже есть и городить отдельный слой не нужно. Общие принципы автоматизации самого процесса бэкапирования — в статье автоматизация резервного копирования баз данных.

Встроить проверку в конвейер бэкапа, а не держать отдельно

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

#!/bin/bash
# /usr/local/bin/pg-dump-with-checksum.sh
set -euo pipefail

DATE=$(date +%F)
DUMP_DIR="/backup/pg/$DATE"
MANIFEST="/var/lib/db-integrity/manifest-$DATE.sha256"

mkdir -p "$DUMP_DIR"
pg_dump -Fc mydb | gzip > "$DUMP_DIR/full.dump.gz"

# сумма считается сразу, пока файл гарантированно свежий и корректный
sha256sum "$DUMP_DIR/full.dump.gz" > "$MANIFEST"

# сразу копируем манифест туда, куда у этого сервера нет прав на запись
# задним числом -- это защищает от подмены суммы вместе с файлом
scp "$MANIFEST" integrity@backup-host:/srv/integrity-manifests/pg/

# пингуем внешний мониторинг задач -- узнать не только о провале суммы,
# но и о том, что сам скрипт вообще отработал
curl -fsS -m 10 --retry 3 https://hc-ping.com/YOUR-UUID-HERE >/dev/null || true

Здесь важны две детали. Во-первых, манифест копируется на другой сервер сразу же, а не позже отдельным шагом — иначе есть риск, что бэкап успеет испортиться до того, как сумма для него будет надёжно сохранена в стороннем месте. Во-вторых, ping во внешний сервис мониторинга задач в конце — это отдельная страховка: она отвечает не на вопрос «сумма совпала?», а на вопрос «скрипт вообще дошёл до конца?». Если cron перестал срабатывать — вы должны узнать об этом, а не выяснить постфактум, что бэкапов за неделю просто не было. Настройка такого мониторинга — в статье мониторинг cron-задач через healthchecks.io.

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

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Если сумма не совпала, это точно порча диска, а не что-то другое?

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

Можно ли хранить сумму в расширенном атрибуте (xattr) файла вместо отдельного манифеста?

Технически можно (setfattr -n user.checksum.sha256 -v "$(sha256sum file | cut -d' ' -f1)" file), но у этого есть слабое место именно для защиты от порчи диска: атрибут физически хранится на том же носителе, что и файл, — при серьёзной деградации сектора могут пострадать оба. Используйте xattr вместе с внешним манифестом, а не вместо него.

Насколько часто нужно пересчитывать суммы для архива бэкапов?

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

Стоит ли переходить на файловую систему со встроенными суммами вместо всей этой ручной возни?

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

par2 может заменить обычный бэкап в другом месте?

Нет. par2 защищает от порчи конкретных байт внутри файла, который физически остаётся на том же диске, но не от полной потери диска, удаления или шифровальщика. Это дополнение к географически разнесённым копиям, а не их замена.

Обсудить статью, задать вопрос или начать новую тему

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

Перейти в сообщество →