Архитектура
Из чего состоит S5W, где лежат данные и что происходит с файлом от загрузки до удаления. Страница для архитектора и для DevOps, которому важно понимать, что именно он запускает.
Процессы в контейнере
Корневой процесс контейнера — супервизор s6. Он запускает и перезапускает всё
остальное и по docker stop останавливает части в правильном порядке.
| Процесс | Что делает | Если упадёт |
|---|---|---|
| PostgreSQL 18 | Хранит метаданные. Слушает только 127.0.0.1 и unix-сокет — наружу не открыт. |
s6 перезапускает его. |
Приложение (winter-http) |
Клиентский API и раздача файлов на порту 9090. |
Перезапускается своим супервизором. |
| Планировщик | Задачи обслуживания по расписанию. | Перезапускается. |
Админ-панель (AdminUi) |
Отдаёт веб-панель на 9091 и проксирует её запросы к API внутри контейнера. |
Перезапускается; её падение не трогает API и раздачу. |
Состояние частей видно командой:
docker exec s5w su-exec winter php call app statusВ выводе — каждая часть, её состояние, PID, сколько она работает и сколько раз перезапускалась.
Порядок запуска
- Если каталог базы пуст —
initdb, создание роли и базыs5w, создание таблиц. - Запуск PostgreSQL.
- Запуск приложения: оно проверяет код, строит кэш и только потом принимает запросы. Если сборка кэша не удалась, приложение не стартует и пишет причину в лог.
Порядок остановки
Сначала останавливается приложение: дорабатывает запросы и компоненты, до 35 секунд.
Затем PostgreSQL — быстрой остановкой, без ожидания посторонних сессий. Поэтому
контейнеру нужно дать на остановку не меньше 45 секунд: --stop-timeout 45 или
stop_grace_period: 45s. Если Docker убьёт базу раньше, при следующем старте она
будет восстанавливаться по журналу.
Модель данных
Изолированное пространство: своя квота, свои токены, своя политика кэша. Файлы разных бакетов не пересекаются ни в базе, ни на диске.
Необязательная группа файлов внутри бакета: видимость, срок хранения, политика кэша. Вложенных папок нет.
Запись с именем, папкой, видимостью и сроком. Ссылается на содержимое — блоб.
Само содержимое, по одному на каждый уникальный SHA-256 в бакете. Знает, сколько файлов на него ссылаются.
Ещё есть токены (доступ к бакету по API), загрузки (незавершённые загрузки частями), временные ссылки и статистика трафика по дням.
Хранение файлов
На диске содержимое лежит по хешу, а не по имени:
/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 на расписание не влияет.
Задачи защищены от параллельного запуска: одна и та же задача не выполняется дважды
одновременно.