Skip to content

Кэш архива на эдже

Нода, которая отдает не свой архив, а чужой — через remotes или legacy-источник, — по умолчанию ходит за каждым фрагментом к источнику. Кэш архива меняет это: прочитанный фрагмент оседает на локальном кэш-диске, и следующее обращение к этому же участку обслуживается локально, без сетевого запроса.

Так строится эдж CDN: origin хранит архив, эджи держат у себя то, что реально смотрят зрители.

Кэш прозрачен. Он меняет источник байтов, но не выдачу: URL фрагмента, плейлисты и границы архива у зрителя те же самые.

Что отличает кэш-диск от архивного

Кэш-диск — это роль диска, а не флаг на нем:

  • живая запись его не выбирает — архив пишется только на диски из disks;
  • ретенция потоков им не распоряжаетсяmax_depth и max_bytes потока к кэшу не применяются;
  • место освобождает вытеснение — по давности последнего обращения, а не по возрасту содержимого;
  • в каталоге он изолирован — кэш-блобы лежат в своих таблицах, и архивные обходы (зачистка, backfill, разбивка занятого места на доли) их не видят.

Поэтому кэш эджа не выглядит на дашборде бесхозным архивом, а вытеснение кэша не конкурирует с записью архива за один мьютекс.

Настройка

Кэш настраивается в двух местах: диски — на ноде, включение — у потока.

dvr:
  root: /storage
  cache_disks:
  - path: ssd1              # путь относительно root, как у архивного диска
    max_bytes: 536870912000 # потолок кэша на этом диске, байт
    min_free_bytes: 10737418240
  - path: ssd2

streams:
  - name: cam1
    remotes:
    - url: http://origin:5080
    cache: {}               # включает кэширование архивных чтений

Параметры кэш-диска:

Параметр Описание
path путь диска относительно root
max_bytes сколько кэшу позволено занять на этом диске; пусто — ограничен только свободным местом
min_free_bytes сколько свободного места оставить на томе

Лимиты независимы: вытеснение работает, пока диск не уложится в оба. mode у кэш-диска нет — Degraded относится к участнику RAID архива, а кэш ничего не реплицирует.

Секция cache у потока — top-level секция документа, соседняя с dvr, а не поле внутри него: кэш и запись независимы. Эдж кэширует чужой архив, своего не записывая. Полей у секции пока нет, само ее присутствие включает кэширование.

Валидация конфига отвергает:

  • путь, указанный одновременно в disks и в cache_disks;
  • дубликат внутри cache_disks;
  • кэш-диск, чей путь совпадает с root — вытеснение стерло бы дом каталога.

Разные пути на одном физическом томе — валидная конфигурация, но при старте пишется предупреждение: вытеснение освобождает место, которое тут же занимает архив.

Секция cache у потока на ноде без кэш-дисков — валидный no-op с предупреждением в логе. Это нормально для кластера, где один документ потока разъезжается по разным нодам.

Чистый эдж

Хранилище из root и одних кэш-дисков, без архивных, — валидная конфигурация: каталог есть, живой записи нет, архив подкачивается из remotes.

dvr:
  root: /storage
  cache_disks:
  - path: ssd1
    max_bytes: 536870912000

streams:
  - name: cam1
    remotes:
    - url: http://origin:5080
    cache: {}

Warning

root — дом каталога, а не диск записи. Раньше пустой disks означал «пиши прямо в root»; теперь это ошибка записи, видимая в статистике, а не молчаливая запись мимо RAID. Если нода должна писать архив — перечислите диски явно в disks.

Допуск в кэш

Класть в кэш все подряд с первого запроса — значит вытеснять популярное случайным: одиночная перемотка в глубину архива выбила бы с диска то, что смотрят все. Поэтому у допуска две оси, и настраиваются они на ноде, целиком на весь кэш:

Параметр Описание
cache_fresh_age_secs фрагмент моложе этой глубины (секунд от текущего времени) кэшируется с первого же запроса; по умолчанию — сутки
cache_admission_hits с какого по счету запроса кэшируется фрагмент старше границы свежести; по умолчанию — со второго, 1 выключает барьер
dvr:
  root: /storage
  cache_fresh_age_secs: 86400
  cache_admission_hits: 2
  cache_disks:
  - path: ssd1

Свежий хвост архива смотрят почти все, поэтому он попадает в кэш сразу. Глубина архива интересна единицам — она попадает в кэш, только если ее запросили повторно.

Порядок источников чтения

Архивный фрагмент ищется по цепочке, и первый ответивший источник закрывает запрос:

  1. сегментатор живого потока;
  2. кэш;
  3. локальный архив;
  4. кластер (remotes);
  5. legacy-источник old_m4f;
  6. legacy-архив на диске.

Попадание в кэш отвечает, не обращаясь к нижестоящим источникам. Если фрагмент есть и на архивном диске, и на кэш-диске, чтение обслуживает кэш: SSD-кэш быстрее шпинделей. У потока без секции cache шага кэша в цепочке нет вовсе.

Что попадает в кэш при промахе

Успешное чтение из источника ниже кэша записывается на кэш-диск вместе с init-фрагментом. Отличия от архивной записи:

  • отсечка max_depth не применяется — в кэш кладется то, что попросил зритель, даже если это глубже любой настроенной глубины архива;
  • многотрековый сегмент кладется целиком, всеми дорожками — поэтому переключение битрейта у зрителя обслуживается попаданиями, а не промахами;
  • запись идемпотентна — одновременный промах нескольких зрителей по одному фрагменту оставляет в кэше ровно одну копию;
  • ошибка записи в кэш не влияет на ответ клиенту — байты у него уже есть, ошибка только логируется и учитывается в метриках.

Догрузка вперед (read-ahead)

Просмотр архива последователен, поэтому после обслуженного чтения Sapsan фоном подкачивает следующий по времени фрагмент той же дорожки, если его в кэше нет. Догрузка работает на чистом эдже и при чтении с удаленного источника, ограничена одним фрагментом вперед и ответ клиенту не задерживает.

Это не прогрев: заполнения кэша по расписанию или по EPG здесь нет.

Вытеснение

Место на кэш-диске освобождается часовыми блобами в порядке давности последнего обращения — наименее недавно использованный уходит первым, пока диск не вернется в оба лимита.

  • Возраст содержимого критерием не служит: вчерашняя популярная передача переживет сегодняшнюю непопулярную, а текущий час вытесняется наравне с остальными.
  • Единственное изъятие — блоб, в который прямо сейчас дописывает писатель: он остается, вытесняется следующий по давности.
  • Зачистка архива (max_depth и max_bytes потока) и защита эпизодов кэш-диски не рассматривают вовсе.

Порядок вытеснения держит свой индекс каталога и переживает рестарт. Отметка обращения пишется в каталог лениво — не чаще раза примерно в 10 минут на блоб, чтобы чтение не превращалось в запись.

Учет кэша (занятый объем, времена обращения) восстанавливается при старте range-сканом каталога, без обхода файлов данных. Стертый кэш-диск — валидное холодное состояние: сервер стартует, кэш наполняется с нуля, архив не затронут.

Таймлайн и глубина на эдже

Глубина rewind и диапазоны архива кэшируемого потока считаются объединением локального каталога (архив и кэш) и всех удаленных источников — и анонсируются в rewind-плейлистах и в ответах о диапазонах архива. Зритель на эдже видит ту же глубину, что на origin.

Окна источников объединяются списком, с сохранением дыр между ними: сплошной интервал от начала до конца источника подменять список нельзя — иначе таймлайн красит зеленым участки, по которым данных нет, и клик по ним падает в 404. Дыры не больше запрошенного разрешения при этом сливаются по общему правилу.

Если origin недоступен, а запрошенный диапазон закэширован целиком, плейлист строится по локальному каталогу и фрагменты отдаются из кэша — просмотр продолжается.

Наблюдаемость

Каждое архивное чтение атрибутируется звеном цепочки, которое его обслужило:

  • cache — отдал кэш-диск ноды (попадание);
  • local — отдал архивный диск ноды;
  • remote — пришлось идти к удаленному источнику (промах).

Архивным чтением считается только клиентское чтение фрагмента. Выдача живым сегментатором и догрузка read-ahead в атрибуцию не входят — иначе read-ahead завышал бы хитрейт тем сильнее, чем лучше он работает; догрузки считаются отдельным счетчиком.

В Prometheus и local-prom уходят кумулятивные счетчики:

Метрика Смысл
dvr_archive_reads_total{source} архивные чтения по осям cache / local / remote
dvr_archive_read_bytes_total{source} байты этих чтений
dvr_read_ahead_fetches_total догрузки read-ahead
dvr_disk_reads_total{disk_id}, dvr_disk_read_bytes_total{disk_id} обращения к диску; у кэш-диска это его попадания
dvr_disk_cache_bytes{disk_id} объем кэша на диске

Локальная ручка GET /streamer/api-v4/dvr отдает по каждому кэш-диску его роль, объем и лимит, а по ноде — скорости чтений по трем осям. Кумулятивных счетчиков в management API нет: там скорости и гейджи.

Хитрейт нигде не передается готовым числом — это cache / (cache + local + remote), и потребитель считает его сам: скорости складываются, деление идет после. Среднее из средних врет на неравном весе нод.

Экраны консоли, где это видно, описаны в документации Catena: ёмкость DVR в кластере и настройки ноды.

Чем это отличается от ленивой репликации remotes

У remotes есть похожее поведение: фрагмент, прочитанный с удаленного сервера, дописывается в локальный архив. Разница принципиальная:

Ленивая репликация remotes Кэш архива
Куда пишется на архивные диски на кэш-диски
Зачем перенести чужую историю к себе навсегда обслужить повторные обращения локально
Когда освобождается зачисткой по ретенции потока вытеснением по давности обращения
Нужны ли архивные диски да нет, эдж может быть без них

Репликация — про переезд архива, кэш — про экономию сети на популярном.

Что дальше