Файлы в БД или в объектном хранилище: связь метаданных и объектов

↑ 4.2 Объектное хранилище: MinIO и S3 · ← Предыдущая · → Следующая

Желательно~4 мин чтенияУровень: middle

Зачем это на собесе

Классический архитектурный вопрос: где хранить файлы и как сохранять согласованность между БД и хранилищем.

Где хранить содержимое

ВариантПлюсыМинусы
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, смена домена).

Согласованность (нет общей транзакции)

Сбои: объект загружен, а запись в БД не создана (сирота в хранилище); запись есть, а объекта нет; удалили запись, объект остался.

Паттерны:

  1. Статусы: сначала запись pending, затем загрузка, затем ready. Фоновая задача удаляет зависшие pending (и их объекты) старше N часов.
  2. Порядок: при создании — сначала объект, затем запись (сирота лучше, чем битая ссылка); при удалении — пометить запись deleted, затем удалить объект асинхронно.
  3. Outbox: команда удаления/обработки сохраняется в БД в той же транзакции, воркер выполняет её идемпотентно.
  4. Реконсиляция: периодическое сравнение списка объектов и записей (по инвентаризации S3), удаление сирот и алерты на расхождения.
  5. События хранилища: ObjectCreated → подтверждение в БД без участия клиента.
  6. Неизменяемые ключи: новая версия файла = новый ключ, старый удаляется позже (безопасные откаты и кэш CDN).
  7. Идемпотентность: повторное выполнение операций (загрузка, удаление) не ломает состояние.

Контроль целостности

Content-MD5/x-amz-checksum-sha256 при загрузке, сравнение ETag/SHA-256 с метаданными, проверка размера.

Доступ и выдача

  • приватные файлы: проверка прав в API → presigned GET (короткий TTL) или проксирование;
  • публичные: CDN + неизменяемые ключи;
  • превью и производные (миниатюры, транскодирование): воркер по событию, хранятся рядом с ключом (thumb/…), статус в БД.

Удаление и ретенция

Мягкое удаление (deleted_at) → отложенное физическое удаление (окно восстановления) → lifecycle. Соответствие требованиям (GDPR): удаление по запросу, журнал.

Бэкапы

БД и хранилище должны быть согласованы по времени: версионирование бакета и PITR БД, репликация. Восстановление проверяйте учениями.

Вопросы с ответами