Файлы в БД или в объектном хранилище: связь метаданных и объектов
↑ 4.2 Объектное хранилище: MinIO и S3 · ← Предыдущая · → Следующая
Зачем это на собесе
Классический архитектурный вопрос: где хранить файлы и как сохранять согласованность между БД и хранилищем.
Где хранить содержимое
| Вариант | Плюсы | Минусы |
|---|---|---|
BLOB в БД (bytea, Large Objects) | транзакционность, единый бэкап, простая схема | раздувает БД, бэкапы и репликация тяжелее, нагрузка на кэш и I/O БД, дорогое масштабирование |
| Файловая система | просто | масштабирование, отказоустойчивость, доступ из нескольких инстансов |
| Объектное хранилище | дёшево, масштабируется, CDN, presigned URL, lifecycle | нет транзакций с БД, нужна синхронизация |
Рекомендация: содержимое — в объектном хранилище, метаданные — в БД. BLOB в БД оправдан для очень маленьких файлов (иконки, подписи) и когда критична атомарность.
Схема метаданных
CREATE TABLE files (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id bigint NOT NULL REFERENCES users(id),
bucket text NOT NULL,
object_key text NOT NULL,
original_name text NOT NULL,
content_type text NOT NULL,
size_bytes bigint NOT NULL CHECK (size_bytes >= 0),
checksum text, -- SHA-256 / ETag
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','ready','deleted','failed')),
created_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
UNIQUE (bucket, object_key)
);
CREATE INDEX ON files (owner_id, created_at DESC);В других таблицах храните file_id (внешний ключ), а не URL: адрес меняется (CDN, смена домена).
Согласованность (нет общей транзакции)
Сбои: объект загружен, а запись в БД не создана (сирота в хранилище); запись есть, а объекта нет; удалили запись, объект остался.
Паттерны:
- Статусы: сначала запись
pending, затем загрузка, затемready. Фоновая задача удаляет зависшиеpending(и их объекты) старше N часов. - Порядок: при создании — сначала объект, затем запись (сирота лучше, чем битая ссылка); при удалении — пометить запись
deleted, затем удалить объект асинхронно. - Outbox: команда удаления/обработки сохраняется в БД в той же транзакции, воркер выполняет её идемпотентно.
- Реконсиляция: периодическое сравнение списка объектов и записей (по инвентаризации S3), удаление сирот и алерты на расхождения.
- События хранилища:
ObjectCreated→ подтверждение в БД без участия клиента. - Неизменяемые ключи: новая версия файла = новый ключ, старый удаляется позже (безопасные откаты и кэш CDN).
- Идемпотентность: повторное выполнение операций (загрузка, удаление) не ломает состояние.
Контроль целостности
Content-MD5/x-amz-checksum-sha256 при загрузке, сравнение ETag/SHA-256 с метаданными, проверка размера.
Доступ и выдача
- приватные файлы: проверка прав в API → presigned GET (короткий TTL) или проксирование;
- публичные: CDN + неизменяемые ключи;
- превью и производные (миниатюры, транскодирование): воркер по событию, хранятся рядом с ключом (
thumb/…), статус в БД.
Удаление и ретенция
Мягкое удаление (deleted_at) → отложенное физическое удаление (окно восстановления) → lifecycle. Соответствие требованиям (GDPR): удаление по запросу, журнал.
Бэкапы
БД и хранилище должны быть согласованы по времени: версионирование бакета и PITR БД, репликация. Восстановление проверяйте учениями.
Вопросы с ответами
Почему файлы лучше хранить вне БД?
Объектное хранилище дешевле и лучше масштабируется, не нагружает I/O и бэкапы БД, поддерживает CDN и presigned URL. В БД остаются метаданные и связи.
Как избежать «осиротевших» файлов?
Использовать статусы (
pending/ready), фоновую очистку зависших загрузок, outbox для удаления и периодическую реконсиляцию бакета и БД.
Что хранить в БД: URL или ключ?
Ключ объекта (и бакет): URL зависит от домена, CDN и подписи и может меняться; его генерируют при необходимости.