Базу восстановили, а картинки не открываются: рассинхрон ссылок
Авария случилась, вы её пережили: база данных поднята из бэкапа, приложение стартовало, таблицы на месте. А потом первый же пользователь открывает карточку товара или профиль — и вместо картинки видит битую иконку. Проверяете вторую, третью — та же история. База восстановилась честно, но она хранит только ссылки на файлы, а не сами файлы, и эти ссылки теперь указывают в пустоту.
Содержание
- Почему база «восстановилась», а файлы — нет
- Причина №1: база и хранилище бэкапились в разное время
- Причина №2: файлы восстановились не туда, где их ждёт база
- Как обнаружить рассинхрон: выборка ссылок и сверка с диском
- Скрипт массовой проверки: что реально лежит на диске
- План действий при обнаружении расхождений
- Как не наступить на это снова
Почему база «восстановилась», а файлы — нет
База данных и файловое хранилище — это два независимых источника правды, которые согласованы между собой только пока их никто не трогает порознь. В таблицах лежат пути или URL вида /uploads/2026/08/photo.jpg или https://cdn.example.com/files/abc123.pdf, а сами байты изображения лежат отдельно — на диске, в объектном хранилище, на CDN. Восстановление базы из дампа возвращает на место только первую часть: строки, указатели, метаданные. Если вторая часть — сами файлы — не была восстановлена той же операцией и из того же момента времени, у вас на руках согласованная база, ссылающаяся на несогласованное файловое пространство.
Это принципиально не то же самое, что просто «отвалившийся диск» — с точки зрения СУБД всё в порядке, SELECT * FROM products WHERE id = 42 возвращает корректную строку, ошибок в логе базы нет. Разрыв невидим на уровне базы данных и проявляется только на уровне приложения, когда оно пытается открыть файл по пути из этой строки. Если у вас уже была практика восстановления самой базы из дампа — процесс pg_restore/mysql < dump.sql и первичная проверка целостности таблиц подробно разобраны в статье про восстановление базы данных из бэкапа — здесь мы говорим о следующем шаге, который часто выпадает из процедуры.
Причина №1: база и хранилище бэкапились в разное время
Самая частая причина — банальная рассинхронизация расписаний. База дампится каждую ночь в 3:00, а файлы синхронизируются в хранилище раз в неделю по воскресеньям, потому что «файлов много, а меняются они редко». В момент аварии в четверг вы поднимаете свежий дамп базы (данные за среду) и файлы из последнего воскресного бэкапа (данные за прошлую неделю). Итог предсказуем: в базе появились записи о файлах, загруженных пользователями с воскресенья по среду, а самих файлов в бэкапе нет — они физически не успели туда попасть.
Работает и обратная ситуация: файловое хранилище бэкапится чаще и полнее, а база — реже, или наоборот, в файлохранилище идёт ротация с более коротким сроком хранения, чем у бэкапов базы. Тогда после восстановления в базе может не быть записи о файле, который физически есть на диске (не страшно — просто "осиротевший" файл), либо наоборот, ссылка ведёт на файл, который уже удалила ротация хранилища.
Отдельный частый случай — когда бэкапят только базу, считая, что «в файлах ничего важного, это просто загрузки пользователей». Этот подход и его цена разобраны в статье про миф, что бэкапить нужно только базу данных — рассинхрон путей после восстановления оттуда практически неизбежен, если бэкап файлов вообще не велся или велся нерегулярно.
Проверить признаки этой причины просто: сравните временные метки. Возьмите дату/время снятия дампа базы и дату/время последнего успешного бэкапа хранилища файлов. Если между ними разница больше, чем интервал между двумя последовательными загрузками пользователей (для активного сервиса это может быть минуты, для тихого — дни), рассинхрон почти гарантирован, и его масштаб примерно равен этому интервалу.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверПричина №2: файлы восстановились не туда, где их ждёт база
Вторая частая ситуация — файлы физически на месте и целы, но не там, где их ищет приложение. Это происходит, когда:
- база и файлы восстанавливались раздельными операциями на новый сервер, и файловое хранилище развернули по другому пути (например, было
/var/www/app/storage/uploads, стало/srv/app/uploads); - восстановление шло на другой хост (staging вместо прежнего продакшена, новый VPS взамен упавшего), а в базе пути абсолютные и жёстко зашиты вместе с доменом или IP старого сервера;
- файлы лежали в объектном хранилище (S3-совместимом) под одним bucket/endpoint, а после аварии подняли новый bucket с другим именем или регионом, и URL в базе ссылаются на старый;
- часть путей в базе относительная, а часть — абсолютная (типичная ситуация после накопленных миграций CMS), и при переносе на новый сервер относительные пути резолвятся иначе, чем ожидалось.
Здесь файлы физически существуют и не потеряны — проблема чисто в путях. Это отличает ситуацию от причины №1 (где файлов действительно нет в нужной версии) и меняет стратегию починки: вместо восстановления файлов из другого источника нужно просто исправить ссылки в базе.
Как обнаружить рассинхрон: выборка ссылок и сверка с диском
Не проверяйте по одной ссылке руками, кликая по карточкам в интерфейсе — на сколько-нибудь живом проекте это займёт часы и не даст полной картины. Задача решается выгрузкой всех путей к файлам из базы и сверкой каждого с файловой системой.
Сначала получите список путей. Структура зависит от схемы, но подход общий — выбрать все столбцы, где хранятся пути или URL:
-- PostgreSQL / MySQL, пример для таблицы медиафайлов
SELECT id, file_path FROM media_files;
-- если пути раскиданы по разным таблицам (типично для CMS)
SELECT id, image_url AS path FROM products WHERE image_url IS NOT NULL
UNION ALL
SELECT id, avatar_path AS path FROM users WHERE avatar_path IS NOT NULL
UNION ALL
SELECT id, attachment_path AS path FROM tickets WHERE attachment_path IS NOT NULL;
Выгрузите результат в CSV или просто в текстовый файл — этого достаточно для следующего шага.
psql -U app -d appdb -A -F ',' -c "SELECT file_path FROM media_files WHERE file_path IS NOT NULL;" \
-t > /tmp/db_paths.csv
Дальше — не открывать вручную, а прогнать массовую проверку.
Скрипт массовой проверки: что реально лежит на диске
Небольшой Python-скрипт читает список путей из базы и проверяет каждый на существование, учитывая, что часть путей в базе может быть относительной (тогда её нужно домножить на корень хранилища), а часть — полным URL (тогда для локальной проверки достаточно вырезать домен и оставить путь):
#!/usr/bin/env python3
import csv
from pathlib import Path
from urllib.parse import urlparse
STORAGE_ROOT = Path("/srv/app/uploads") # актуальный корень хранилища после восстановления
DB_PATHS_FILE = "/tmp/db_paths.csv"
REPORT_FILE = "/tmp/broken_links_report.csv"
def resolve_local_path(raw_path: str) -> Path:
if raw_path.startswith("http://") or raw_path.startswith("https://"):
raw_path = urlparse(raw_path).path
raw_path = raw_path.lstrip("/")
return STORAGE_ROOT / raw_path
total = 0
missing = 0
with open(DB_PATHS_FILE, newline="") as src, open(REPORT_FILE, "w", newline="") as out:
writer = csv.writer(out)
writer.writerow(["db_path", "resolved_path", "status"])
for line in src:
raw = line.strip()
if not raw:
continue
total += 1
local_path = resolve_local_path(raw)
if local_path.exists():
status = "ok"
else:
status = "missing"
missing += 1
writer.writerow([raw, str(local_path), status])
print(f"Проверено: {total}, не найдено: {missing} ({missing / total:.1%})")
Запуск и результат:
python3 check_media_links.py
# Проверено: 48213, не найдено: 6104 (12.7%)
Дальше открываете broken_links_report.csv и смотрите на характер расхождений — это ключевой диагностический шаг:
- если пропавшие файлы сгруппированы по дате (например, всё, что загружено после определённого числа, отсутствует, а до — на месте) — это причина №1, разрыв во времени между бэкапами базы и хранилища;
- если отсутствуют абсолютно все файлы, но при этом вы точно знаете, что бэкап хранилища восстановился и данные там есть — это причина №2, путь в базе не совпадает с фактическим расположением, и
resolve_local_pathв скрипте выше просто ищет не там; - если расхождение частичное и без явной закономерности — стоит проверить оба варианта, а заодно исключить банальную опечатку в
STORAGE_ROOTили в правах доступа (не забудьте, чтоPath.exists()вернётFalseи в случае, если у скрипта просто нет прав на чтение директории — запускайте от того же пользователя, что и приложение).
Для второй ситуации диагностика ещё проще — возьмите десяток путей из отчёта и найдите по имени файла на диске:
find /srv/app/uploads -iname "photo_abc123*"
Если файл находится, но по другому относительному пути — значит, дело действительно в структуре директорий, и проблема решается не поиском пропавших файлов, а исправлением ссылок.
План действий при обнаружении расхождений
Порядок действий зависит от того, какая причина подтвердилась.
Если пути просто указывают не туда (причина №2) — чинить нужно базу, а не искать несуществующие файлы:
-- пример: замена префикса пути после переноса хранилища
UPDATE media_files
SET file_path = REPLACE(file_path, '/var/www/app/storage/uploads', '/srv/app/uploads')
WHERE file_path LIKE '/var/www/app/storage/uploads%';
Перед массовым UPDATE обязательно снимите свежий бэкап текущего состояния базы и прогоните запрос сначала как SELECT, чтобы увидеть, сколько строк затронет замена — правило то же, что и для любой массовой миграции данных. Если сервис несёт файлы через CDN или объектное хранилище с другим доменом или именем bucket, апдейт аналогичен, но затрагивает домен/host в URL, а не локальный путь.
Если файлов физически нет (причина №1) — вариантов немного:
- Поискать более свежий или более ранний бэкап хранилища, где нужные файлы есть — иногда помогает предыдущая ротация, если она ещё не удалена.
- Если файлы были также доступны через CDN с длинным сроком кэширования — проверить, не отдаёт ли CDN их до сих пор по старому URL, и перекачать оттуда.
- Если файлов нет нигде — честно пометить записи в базе как «файл недоступен» (заглушка вместо разбитой иконки) и уведомить пользователей, чьи загрузки затронуты, вместо того чтобы оставлять битые ссылки без объяснения.
- Задокументировать инцидент и окно потерянных данных — это тот случай, когда для будущего разбора полезно точно знать, за какой период файлы утрачены безвозвратно.
Если хранилище физически переехало на новый сервер большого объёма (например, с истекающего VPS на выделенный сервер под терабайты данных), стоит заранее спланировать саму миграцию, а не только последующую сверку путей — процесс и типичные грабли разобраны в статье про миграцию файлового хранилища на терабайты.
После любого из вариантов повторно прогоните скрипт массовой проверки — он же служит финальной приёмкой: пока missing не близко к нулю (ноль недостижим, если часть файлов решили считать окончательно утраченными), считать восстановление завершённым нельзя.
Как не наступить на это снова
Рассинхрон путей — почти всегда следствие того, что база и файлы бэкапятся и восстанавливаются как два независимых процесса. Несколько практических мер, которые снижают риск:
| Мера | Что даёт |
|---|---|
| Единое расписание бэкапа базы и хранилища | Снимок файлов и снимок базы соответствуют одному и тому же моменту времени |
| Хранение путей относительными, а не абсолютными | Перенос на новый сервер не ломает ссылки автоматически |
| Регулярное тестовое восстановление обоих компонентов вместе | Рассинхрон ловится на тесте, а не в проде — см. сценарий в статье про проверку восстановления бэкапа раз в месяц |
| Скрипт сверки путей как часть регламента после любого восстановления | Расхождение видно сразу, а не через жалобы пользователей |
| Отдельные метки времени (снапшот/версия) на бэкапах базы и хранилища | Можно осознанно подобрать пару «база + хранилище» из одного момента вместо «что было под рукой» |
Ни одна из этих мер не требует сложной инфраструктуры — это в первую очередь вопрос дисциплины и одного скрипта, который вы прогоняете после каждого восстановления, а не только в первый раз, когда столкнулись с проблемой.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Файлы точно потеряны, если скрипт показывает missing?
Не обязательно. Сначала проверьте, не находится ли файл под другим путём (переезд хранилища), не отдаёт ли его ещё CDN по старому URL, и не осталась ли более свежая/старая ротация бэкапа хранилища с этим файлом. Реально потерянными считайте только те, что не нашлись ни в одном из этих мест.
Можно ли просто восстановить более старый бэкап базы, синхронный с бэкапом файлов, вместо починки путей?
Да, если расхождение по времени небольшое и вы готовы потерять записи, созданные между старым и новым дампом базы. Для активного сервиса это обычно хуже, чем починить пути точечно, но для тихого проекта с редкими изменениями — рабочий и самый простой вариант.
Как проверить объектное хранилище (S3-совместимое), а не локальный диск?
Логика та же, но вместо Path.exists() используется запрос к API хранилища — например, HEAD-запрос по ключу объекта через boto3 (client.head_object(Bucket=..., Key=...)) с обработкой исключения 404 как признака отсутствия файла.
Что делать, если рассинхрон обнаружился не сразу, а через несколько недель после восстановления?
Диагностика та же самая — выгрузка путей и сверка со скриптом. Единственная сложность: за это время пользователи могли догрузить новые файлы поверх восстановленной базы, и часть missing в отчёте окажется не про аварию, а про естественные новые записи без файлов по другим причинам (не путайте их между собой при разборе отчёта).
Нужно ли останавливать приложение на время сверки и починки путей?
Саму проверку — нет, она только читает. Массовый UPDATE путей в базе лучше проводить в окне минимальной нагрузки и, если объём таблицы большой, порциями (LIMIT/OFFSET или по диапазону id), чтобы не держать долгую блокировку на боевой таблице.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →