Как устроено · s5w

Архитектура

Из чего состоит S5W, где лежат данные и что происходит с файлом от загрузки до удаления. Страница для архитектора и для DevOps, которому важно понимать, что именно он запускает.

Процессы в контейнере

Корневой процесс контейнера — супервизор s6. Он запускает и перезапускает всё остальное и по docker stop останавливает части в правильном порядке.

Процесс Что делает Если упадёт
PostgreSQL 18 Хранит метаданные. Слушает только 127.0.0.1 и unix-сокет — наружу не открыт. s6 перезапускает его.
Приложение (winter-http) Клиентский API и раздача файлов на порту 9090. Перезапускается своим супервизором.
Планировщик Задачи обслуживания по расписанию. Перезапускается.
Админ-панель (AdminUi) Отдаёт веб-панель на 9091 и проксирует её запросы к API внутри контейнера. Перезапускается; её падение не трогает API и раздачу.

Состояние частей видно командой:

bash
docker exec s5w su-exec winter php call app status

В выводе — каждая часть, её состояние, PID, сколько она работает и сколько раз перезапускалась.

Порядок запуска

  1. Если каталог базы пуст — initdb, создание роли и базы s5w, создание таблиц.
  2. Запуск PostgreSQL.
  3. Запуск приложения: оно проверяет код, строит кэш и только потом принимает запросы. Если сборка кэша не удалась, приложение не стартует и пишет причину в лог.

Порядок остановки

Сначала останавливается приложение: дорабатывает запросы и компоненты, до 35 секунд. Затем PostgreSQL — быстрой остановкой, без ожидания посторонних сессий. Поэтому контейнеру нужно дать на остановку не меньше 45 секунд: --stop-timeout 45 или stop_grace_period: 45s. Если Docker убьёт базу раньше, при следующем старте она будет восстанавливаться по журналу.

Модель данных

Бакет

Изолированное пространство: своя квота, свои токены, своя политика кэша. Файлы разных бакетов не пересекаются ни в базе, ни на диске.

Папка

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

Файл

Запись с именем, папкой, видимостью и сроком. Ссылается на содержимое — блоб.

Блоб

Само содержимое, по одному на каждый уникальный SHA-256 в бакете. Знает, сколько файлов на него ссылаются.

Ещё есть токены (доступ к бакету по API), загрузки (незавершённые загрузки частями), временные ссылки и статистика трафика по дням.

Хранение файлов

На диске содержимое лежит по хешу, а не по имени:

text
/var/lib/s5w/
  postgresql/                  кластер PostgreSQL — метаданные
  buckets/
    <bucketId>/
      <sha256>                 содержимое, имя файла — его хеш
  staging/
    <uploadId>                 незавершённая загрузка частями
  .runtime_secrets             сгенерированный WINTER_KEY

Что из этого следует:

  • Имена, папки и видимость — только в базе. Переименование и перенос файла не трогают диск.
  • Без базы файлы на диске ничего не значат. Резервная копия должна включать и базу, и buckets/ — сделанные в один момент. Проще всего копировать весь том целиком.
  • Имена файлов на диске безопасны. Что бы ни прислал клиент в имени, на диск попадает только хеш.

Дедупликация

Одинаковое содержимое хранится в бакете один раз. При загрузке сервер считает SHA-256; если такой блоб в бакете уже есть, новый файл просто ссылается на него.

  • Копия стоит строку в базе, а не место на диске.
  • Квота считает блобы, а не файлы: десять копий одного файла занимают квоту один раз.
  • У блоба есть счётчик ссылок. Удаление файла уменьшает его; когда ссылок не осталось, блоб удаляется и место возвращается в квоту.
  • Дедупликация работает внутри бакета. Одинаковые файлы в разных бакетах хранятся отдельно — так бакеты остаются изолированными.

Клиент может проверить наличие содержимого до загрузки: POST /v1/uploads с sha256. Если такое уже есть, файл создаётся сразу, без передачи байтов. Подробно — в загрузке частями.

Квоты и место на диске

  • У каждого бакета есть квота (по умолчанию 100 МиБ, меняется в панели). Загрузка, которая её превысит, отклоняется с кодом 413 и сообщением, сколько места свободно.
  • Загрузка частями проверяет квоту и свободное место на диске заранее, при старте. Сервер держит запас 512 МиБ: если после загрузки на диске останется меньше, она отклоняется с кодом 507.
  • Занятое место хранится счётчиком и раз в сутки пересчитывается заново, чтобы исправить расхождения после сбоев.

Загрузка частями

Части пишутся в staging/<uploadId>. Когда пришли все, файл проверяется по хешу и переименованием переезжает в buckets/. Переименование атомарно только в пределах одной файловой системы, поэтому staging/ и buckets/ обязаны лежать на одном томе. Если это не так, сервер пишет предупреждение и копирует файл вместо переименования — медленнее и с двойным расходом места на время копирования.

Брошенная загрузка истекает через 24 часа после последней принятой части (каждая часть продлевает срок); уборщик, который ходит раз в час, удаляет её вместе с частями.

Статистика трафика

Счётчики отдачи, приёма и запросов копятся в общей памяти процесса и раз в 20 секунд сбрасываются в базу. При аварийной остановке теряется не больше 20 секунд статистики; сами файлы и метаданные при этом не страдают.

Обслуживание по расписанию

Когда Что Зачем
каждые 20 с сбросить счётчики трафика в базу статистика
каждые 5 мин удалить файлы с истёкшим сроком хранения папки сроки хранения
каждый час удалить брошенные загрузки частями место в staging/
03:15 удалить истёкшие и отозванные временные ссылки чистота базы
03:30 удалить блобы, на которые ничто не ссылается место на диске
вс 03:45 удалить файлы на диске, о которых не знает база место на диске
04:00 удалить каталоги удалённых бакетов место на диске
04:10 удалить статистику старше года размер базы
04:20 пересчитать занятое место по бакетам точность квот

Время указано в UTC: по нему работает планировщик, TIME_ZONE на расписание не влияет. Задачи защищены от параллельного запуска: одна и та же задача не выполняется дважды одновременно.