Хранилища и S3
Локальные дисковые хранилища архива, вытеснение по заполнению, проверка тома и холодный ярус в S3-совместимом объектном хранилище.
Хранилище — именованное место, куда пишется архив. Хранилища бывают двух видов:
- дисковое — каталог на локальной файловой системе; только оно может быть целью записи;
- S3 — учётная запись объектного хранилища; служит холодной копией уже записанного на диск.
Хранилища объявляются один раз на весь сервер, а потоки ссылаются на них по имени в строке dvr
(см. Запись архива).
Дисковое хранилище#
storage archive /var/lib/hastreamer/dvr { segment_secs 900; evict_at_percent 90; identity <UUID>; }
Тело { } необязательно — короткая форма полностью рабочая:
storage archive /var/lib/hastreamer/dvr;
Два позиционных аргумента обязательны: имя и путь (либо литерал s3 — см. ниже). Иначе:
line 12: `storage` takes <name> <path>, or <name> s3 { … }
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
<имя> |
уникальное имя хранилища | — (обязателен) | горячее |
<путь> |
путь к каталогу — указывайте абсолютный | — (обязателен) | горячее |
segment_secs |
только 900, 1800 или 3600 (секунды) |
900 |
горячее |
evict_at_percent |
целое, процент заполнения; осмысленны 1–100, 0 — выключить |
90 |
горячее |
identity |
строка до 128 символов; для копирования в S3 — канонический UUID строчными буквами | пусто (не проверять) | горячее |
Все три параметра относятся к классу «рабочая нагрузка»: изменение применяется по
systemctl reload hastreamer, но перезапускает каждый поток, который в это хранилище пишет.
Зрители затронутых потоков переподключаются, остальные потоки не трогаются.
segment_secs — длительность одного файла записи#
Множество допустимых значений закрытое. Любое другое — ошибка:
storage "archive": segment_secs must be one of 900, 1800 or 3600 seconds
Ограничение осмысленное: более короткие файлы взрывают число объектов на диске и в S3, более длинные откладывают завершение записи (а значит, и её появление в архиве) и замедляют восстановление после сбоя. Значение по умолчанию — 900 с (15 минут, 96 файлов на поток в сутки).
Помните, что длительность влияет на экспорт: фрагмент нельзя выгрузить через границу файла — см. Просмотр и экспорт.
Права на каталог#
Каталог должен существовать и быть доступен на чтение и запись пользователю, от имени которого
запущена служба hastreamer.service. Дополнительно:
- отводите хранилищу отдельный раздел или том — тогда порог заполнения считается по архиву, а не по всей корневой файловой системе;
- не размещайте архив на том же разделе, что и системный журнал, — конкуренция за место приводит к тому, что оба переполняются одновременно;
- сетевые файловые системы работают, но их задержки увеличивают риск разрывов непрерывности записи.
Узнайте, от имени какого пользователя работает служба, и проверьте права его глазами:
systemctl show -p User --value hastreamer # пусто означает root sudo -u <пользователь> test -w /var/lib/hastreamer/dvr && echo OK
OK
Если каталог недоступен, запись не начнётся, а поток продолжит раздаваться живьём — ошибка будет видна только в журнале, поэтому проверяйте права до, а не после.
Несколько хранилищ#
Хранилищ может быть сколько угодно — по одному блоку на каждое. Имена должны быть уникальны:
storage fast /mnt/nvme/dvr { segment_secs 900; } storage archive /mnt/raid/dvr { segment_secs 3600; evict_at_percent 85; }
duplicate storage name "archive"
Один поток пишется ровно в одно дисковое хранилище. Распределяйте потоки по хранилищам явно:
stream cam1 { input rtsp://…; dvr fast 24h 200G; } stream cam2 { input rtsp://…; dvr archive 90d 4T; }
Типичное разделение — быстрый NVMe под потоки, которые часто смотрят в архиве, и ёмкий массив под долгое хранение.
DELETE /api/v1/storages/{имя} отвечает 422, если удаление осиротит пишущий поток: разбор всей
конфигурации выполняется до записи файла, поэтому сервер не попадает в нерабочее состояние. Сначала
переведите потоки на другое хранилище, потом удаляйте. Файлы записей на диске при удалении блока
не трогаются.
Вытеснение по заполнению диска#
Ретенция потока (<глубина>/<объём> в строке dvr) отвечает за собственный архив потока.
Под ней работает вторая, общая защита — клапан давления на диск.
Когда файловая система хранилища пересекает evict_at_percent использованного места (считается как
всего − доступно, по правилам df), проход обслуживания удаляет самые старые записи по всем
потокам этого хранилища, пока заполнение не опустится на 2 процентных пункта ниже порога.
Гистерезис в 2 пункта не даёт клапану дребезжать на границе. За один такт удаляется не более 256
записей, порядок детерминирован.
evict_at_percent 0;— клапан выключен полностью. Место контролируется только ретенцией потоков.- Правило «последняя запись потока не удаляется никогда» действует и здесь: клапан не может вычистить поток до пустоты.
- Если запись упирается в переполнение диска, проход обслуживания просыпается досрочно (в пределах ~2 с) и начинает с освобождения места — так исчерпание квоты или инодов всё равно разбирается.
Диапазон evict_at_percent не проверяется. evict_at_percent 120; — корректная конфигурация,
которая просто никогда не срабатывает: заполнение файловой системы не может превысить 100 %.
Чтобы выключить клапан осознанно, пишите 0 — это документированное значение «выключено».
Период прохода задаётся глобальной директивой dvr_cleanup_secs (по умолчанию 600 с), поэтому
реакция клапана отстаёт от роста заполнения на один такт. См. Запись архива.
Проверка тома: identity#
Между «архивный диск не смонтирован» и «архив пуст» файловая система разницы не показывает — в обоих
случаях каталог пустой. Чтобы сервер эту разницу видел, хранилищу можно выдать identity — UUID
тома.
Когда identity задан, сервер кладёт на том файл-маркер .hastreamer-storage-identity и строго
его проверяет: это должен быть обычный файл, на том же устройстве, что и корень хранилища, во
владении пользователя службы, с правами ровно 0600 и побайтово совпадающим содержимым.
Если проверка не проходит:
GET /api/v1/storagesвозвращает"identity_healthy": falseи текст ошибки;- запись не начинается — сервер не станет наполнять точку монтирования на системном диске, приняв её за архивный том;
- чтение архива отвечает
503(повторяемая ошибка), а не404, — то есть «архив временно недоступен», а не «записи нет».
Сгенерируйте значение и подставьте его в блок:
uuidgen | tr 'A-Z' 'a-z'
storage archive /mnt/raid/dvr { identity <UUID>; }
identity необязателен — до появления S3Без identity маркер не создаётся и не проверяется; хранилище работает. Но identity становится
обязательным в тот момент, когда вы включаете копирование в S3: идентификатор тома служит корнем
пространства имён объектов. Если вы вообще допускаете, что архив когда-нибудь поедет в S3, задайте
identity сразу — менять его потом означает перезапуск всех пишущих потоков.
Холодный ярус: S3-совместимое хранилище#
Копирование в S3 позволяет держать на диске короткое «горячее» окно (часы), а длинную историю (недели и месяцы) — в объектном хранилище. Просмотр и перемотка при этом не меняются: индекс всегда остаётся на локальном диске, поэтому таймлайн, календарь и переход в произвольную точку работают мгновенно независимо от того, где лежит само видео.
Шаг 1. Объявите учётную запись S3#
storage coldtier s3 { endpoint s3.example.com; region eu-central-1; access_key <ключ-доступа>; secret_key <секретный-ключ>; addressing vhost; tls_verify system; }
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
endpoint |
хост либо хост:порт — без схемы |
— (обязателен) | горячее |
region |
строка | us-east-1 |
горячее |
access_key |
строка | — (обязателен) | горячее |
secret_key |
строка | — (обязателен) | горячее |
addressing |
vhost — бакет в имени хоста; path — бакет в пути |
пусто = vhost |
горячее |
tls_verify |
system · insecure · pin:<64-hex> · ca:<путь> |
пусто = system |
горячее |
Блок S3 не содержит ни path, ни identity, ни имени бакета. Бакет указывается на потоке.
Схема (s3:// или s3s://) в endpoint не пишется — она живёт в ссылке copy= у потока:
s3 storage "coldtier": endpoint is a bare host[:port] — the s3://|s3s:// scheme lives on the stream's copy= reference s3 storage "coldtier": endpoint, access_key and secret_key are all required s3 storage "coldtier": addressing must be "vhost" or "path" (got "virtual")
addressing path; нужен объектным хранилищам, которые не поддерживают бакет в имени хоста.
tls_verify pin:<64-hex> закрепляет SHA-256 сертификата конечной точки, ca:<путь> подключает
собственный корневой сертификат; insecure отключает проверку и в продуктивной установке
неуместен.
Шаг 2. Свяжите поток с бакетом#
storage fast /mnt/nvme/dvr { identity <UUID>; } stream cam1 { input rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101; dvr fast 2h 3G copy=s3s://cam-archive@coldtier 30d 100G; }
s3s:// — с TLS, s3:// — без. Слева от @ — имя бакета, справа — имя объявленного блока
storage … s3 { }. На диске остаются последние 2 часа (не более 3 ГиБ), в S3 — 30 суток (не более
100 ГиБ).
Условия, проверяемые при загрузке конфигурации#
| Условие | Сообщение при нарушении |
|---|---|
| Поток не может писать напрямую в S3 | record needs a known local DISK storage …; use copy= for an s3 cold tier |
У локального хранилища должен быть канонический identity |
copy= requires local storage "fast" to have a canonical non-nil lowercase UUID identity |
При copy= обязательна локальная ретенция |
copy= needs a LOCAL retention — dvr <storage> <time|size> copy=… — else local media never evicts |
| Ретенция S3 покрывает локальную (или без ограничения) | s3 retention time "7d" is shorter than local retention time "14d"; cold retention must be uncapped or cover local retention to prevent delete/re-upload churn |
Цель copy= объявлена как хранилище S3 |
copy= target "coldtier" is not a declared s3 storage |
| Имя бакета соответствует грамматике S3 | copy= bucket "Cam_Archive" must be [0-9a-z.-] (S3 bucket grammar) |
Ретенция S3 не задаётся без copy= |
an s3 retention is set but there is no copy= target |
Два условия про ретенцию существуют не из педантизма: если холодная ретенция короче локальной, сегмент удалится в S3 и тут же будет загружен туда заново — бесконечный цикл выгрузки за ваши деньги. А без локальной ретенции диск не освобождается вовсе, и копия в S3 не даёт ничего.
Сервер проверяет это при работе и отказывается писать в версионированный бакет. В версионированном хранилище удаление по ретенции оставляет предыдущие версии объектов, и счёт за хранение растёт, хотя архив «очищается».
Как это работает в эксплуатации#
- Копирование сквозное: завершённая запись выгружается сразу после закрытия, а не «когда-нибудь по расписанию». Выбор кандидатов идёт от самых старых за пределами горячего окна; самая свежая локальная запись не выгружается никогда.
- Выгрузка ограничена по бюджету на такт (по числу записей, по объёму и по числу потоков), чтобы
не отнимать ресурсы у раздачи. Невыгруженный остаток учитывается как
tier_backlog_bytesи не теряется. - Выгрузка доказательная: объект проверяется по размеру и SHA-256; уже существующий, но не совпадающий объект считается чужим и никогда не перезаписывается. Локальная копия удаляется только после подтверждения, что удалённая копия принадлежит именно этому серверу.
- Индекс остаётся на диске — даже для записей, тело которых уже только в S3. Поэтому календарь, таймлайн, плейлист и перемотка не ходят в сеть.
- Каждый ответ несёт заголовок
X-HAS-Source: localилиX-HAS-Source: s3— по нему видно, откуда реально пришли байты.
Пространство имён объектов:
hastreamer/dvr/v1/<identity-хранилища>/<поток>/ГГГГ/ММ/ДД/<запись>.mp4 hastreamer/dvr/v1/<identity-хранилища>/<поток>/ГГГГ/ММ/ДД/<запись>.mp4.idx
Эти три величины входят в идентификатор цели. После их изменения все локальные подтверждения «сегмент уже в S3» перестают действовать, и сервер начнёт выгружать заново. Планируйте переезд между бакетами как отдельную операцию, а не как правку одной строки.
Что произойдёт, если хранилище недоступно#
| Ситуация | Поведение сервера |
|---|---|
Диск не смонтирован, identity задан |
Запись не начинается; identity_healthy: false; чтение архива — 503 |
Диск не смонтирован, identity не задан |
Записи пойдут в точку монтирования на корневой ФС — сервер не может отличить это от пустого архива |
| Диск заполнен | Досрочный проход обслуживания освобождает место; приём потока не тормозится, в архиве появляется разрыв |
| Каталог недоступен на запись | Запись не начинается; ошибка в журнале, поток продолжает раздаваться живьём |
| S3 недоступен | Чтение холодных записей — 503 (никогда не 404); выгрузка накапливается в tier_backlog_bytes |
| S3 отвечает ошибками | Предохранитель на учётную запись выдерживает паузу от 1 до 60 с, не давая зрителям умножать нагрузку повторами |
Общее правило: недоступность архива — это всегда 503, а не 404. 404 означает «такой записи
нет», и клиент вправе на этом успокоиться; 503 означает «повторите позже».
Наблюдение за хранилищами#
curl -sS "http://media.example.com:8080/api/v1/storages" \ -H "Authorization: Bearer $TOKEN"
Ключевые поля ответа:
| Поле | Смысл |
|---|---|
total_bytes · used_bytes · avail_bytes |
ёмкость файловой системы |
identity_healthy · maintenance_healthy |
проверка тома и состояние прохода обслуживания |
evict_at_percent |
действующий порог клапана |
streams |
тройки [поток, число записей, байт] |
reclaimed_files · reclaimed_bytes |
что освободил последний проход |
used_growth_bps · fill_eta_secs |
скорость роста и прогноз до порога вытеснения |
read_bps · write_bps · read_iops · write_iops |
активность устройства |
cold_bytes · tiered_bytes · tier_backlog_bytes |
объём в S3, выгружено, осталось выгрузить |
s3_healthy · s3_errors · s3_ops |
состояние учётной записи S3 |
Всё, кроме счётчиков s3_*, пересчитывается проходом обслуживания — то есть раз в
dvr_cleanup_secs (по умолчанию 600 с). Не ждите, что used_bytes отреагирует на удаление файла
за секунду.
Управлять хранилищами можно и без правки файла: POST /api/v1/storages создаёт блок,
PATCH /api/v1/storages/{имя} заменяет его целиком, DELETE /api/v1/storages/{имя} удаляет.
В панели управления те же операции — на странице Storage.
GET /api/v1/storages/{имя}/config возвращает secret_key в открытом видеТак сделано намеренно: форма редактирования должна вернуть блок целиком, а тот же администратор всё
равно читает ключ из GET /api/v1/config. Практический вывод — доступ к API администратора
равносилен доступу к учётным данным объектного хранилища. Ограничивайте его так же строго и заводите
для сервера отдельного пользователя S3 с правами только на один бакет.
Куда двигаться дальше#
- Запись архива (DVR) — строка
dvr, глубина и единицы измерения. - Просмотр и экспорт — как отдать архив зрителю.
- Резервное копирование — что именно нужно копировать, а что восстанавливается само.