SIRINVIDEO Документация администратора и интегратора На сайт 2026.08

Хранилища и 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 целое, процент заполнения; осмысленны 1100, 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 с) и начинает с освобождения места — так исчерпание квоты или инодов всё равно разбирается.
Значение больше 100 молча выключает клапан

Диапазон 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 с правами только на один бакет.

Куда двигаться дальше#