Обновить

Пишем терабайты видео на диск в Go и не даем ОС сожрать всю память

Уровень сложностиСредний
Время на прочтение4 мин
Охват и читатели7K
Всего голосов 15: ↑14 и ↓1+14
Комментарии28

Комментарии 28

А использовать O_DIRECT (+ FILE_FLAG_NO_BUFFERING для Windows) со всей остальной обвязкой - не подходит? Оно как раз для такого сценария, когда надо тупо много писать на диск....

на самом деле, O_DIRECT в Linux имеет свои недостатки, которые как раз использование page cache и решает, как ни странно. Так что управление через posix_fadvise — годное и более универсальное. O_DIRECT желательно оставить для совсем уже граничных случаев.

Тсс, мы видели не отредактированный комментарий :)

race condition, 🤷‍♂️😅

О, спасибо, за вопрос, если честно то изначально так и планировалось(даже в роадмапе до сих пор стоит), но есть нюансы. Эти флаги требуют жесткого выравнивания в памяти по границе стараницы(4 кб). А вот аллокатор го и сборщик, не может гарантировать что []byte слайс будет выровнен как надо. Что бы это завести пришлось бы мучиться с unsafe, аллоцировать куски руками высчитывать оффсеты и писать отдельный буфферизатор который добивал бы неровные кадры нулями до размера сектора. FADV_DONTNEED же дает тот же профит, но позволяет использовать просто os.File. Ось в данном случае просто сама разруливает невыровненые чанки, а флагом просто заставляем выкинуть кэш в конце.

Он требует ручного выравнивания буферов по размеру сектора диска, кмк проще юзать mmap для записи больших кусков и снимать грязные страницы мадвайзом

Mmap был бы неплох, если бы не один факт: мы заранее не знаем размер файла, т.к пишем лайв поток. При использовании mmap пришлось бы постоянно юзать ftruncate и ремаппить память пропорционально росту файла. В го это был бы дикий оверхед на page faults.
mmap - очень крутая штука для бд(когда используется random acces), у нас же идет строгая последовательная запись(append-only). Стнадрный write в Линуксе работает почти идеально, поэтому связка os.File и FADV_DONTNEED, тупо проще безопаснее и не жрет процессорное время.

под Linux можно ещё в сторону sync_file_range посмотреть. Это не замена sync(), но именно потому и стоит посмотреть.

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

там нужно будет не переезжать, а скорее доезжать — вот краткий hint от ChatGPT по смежным функциям:

sync_file_range -> довести dirty pages до writeback/clean

fdatasync -> durability данных + необходимых для них metadata

fsync -> durability данных + metadata файла

FADV_DONTNEED -> после этого попросить ядро выкинуть clean cache

Спасибо, звучит интересно

свободно 100 метров ОЗУ.

И available при этом ещё 30 gb?

Если вы постоянно пишете с sync, далеко масштабировать свое решение у вас не получится - IO дисков кончится раньше.

Для управления page cache есть vm.dirty, как минимум.

Для управления page cache есть vm.dirty, как минимум.

эту ручку лучше оставить админам, а вот про то, что вместо sync есть инструменты прицельнее, я уже в принципе писал.

Этим же админам лучше оставить и развертывание серверов, мониторинг и оценку состояния систем :)

А еще не помешало бы предварительно сделать нагрузочные тесты и увидеть работу page cache там.

Вангую: при аппаратной проблеме с хранилищем эта неубиваемая программа "замрёт как мертвец" в вечных access timeout's, но будет изображать что жива.

Самый простой краш тест, который давно использую: сервер, прога, скрипт, неважно какая сущность - пишут в папку, которая лежит на USB-HDD, а его вытащили из розетки. Вот здесь порой начинается такая магия...

Спасибо за этот "наброс". Да вы правы, но правы лишь в том как поведет себя ОСь. Действительно при физическом отвале носителя или шары системный вызов Write() или Sync() зависит в ожидании I/O таймаута от драйвера. И да горутина пишущая файл, как вы выразились "замрет как мертвец". Но, это пройзодет только с ней, сервер не пострадает и не зависнет . Подсистема записи (Recorder.go) жестко развязана с ядром получения потоков (Ring.go). Сам рекордер является лишь одним из подписчиков ядра(как и муксер или webrtc-клиент). Это равносильно тому что 1000 зрителей смотрят стрим на твиче, у одного вырубили свет, он перестал смотреть стрим. Все. Больше ничего не произойдет, сам стрим на твиче продолжится как не бывало. Более технично в проекте:
Рассыдка кадров из ядра всем поодписчикам реализована через неблокирующие каналы Go(select +default)

// Внутри ядра рассылки (RingBuffer):
select {
case sub.C <- frame:
    // Кадр ушел подписчику
default:
    // Подписчик тормозит или завис на I/O. 
    // Дропаем кадр, инкрементим счетчик потерь.
    atomic.AddUint64(&sub.Drops, 1)
}

Что произойдет если как вы выразились физически удалить устройство:

  1. Горутина рекордера намертво виснет в системном вызове. Она перестает вычитывать кадры из своего(!) канала.

  2. За милисекнуды канал рекордера забивается полностью(в моей реализации, наполнений = 100 кадров)

  3. Ядро (ringBuffer) при попытке отправить следущий кадр, попадает в ветку default, и просто начнет дропать кадры только для рекордера, не блокируя свою работу вообще, даже на милисекунду.

  4. Итог - запись архива ломается, в логах ошибки дропа, но все остальные подписчики (HLS/WebRTC зрители, ИИ-модули, и все остально что еще не прикручено) продолжать получать видеопоток в лайв режиме

Так что сервер, не будет изображать что "он жив", он действительно будет полностью работоспособен, просто изолировав аппартаный отказ хранилища, только в рамках одной горутины.

Надеюсь, я полностью объяснил то как это работает.

Дефолтная ветка в селекте гошных каналов как раз отлично отрабатывает такие аппаратные таймауты без влияния на соседние горутины стриминга

Спасибо за краткий пересказ моего комментария для тех, кому было лень читать текст и смотреть код. Всё так :)

Ну про vm.dirty замечание оличное, но есть два нюанаса:

  1. Sync не делается на каждый кадр, сброс идет только после закрытия целого чанка fMP4, а это раз в несколько секунд. Диск от такого редкого синка даже не почешется, и в I/O не упрется

  2. А вот касательно vm.dirty, это глобальная настройка всего железного сервера, и ставить ее больше зло чем добро, тк в таком случае поведение кеша изменится для всех и вся(может отвалится та же встроенная БД которой такой кеш жизненно нужен). Флаг же позволяет действовать более тонко, мы трогаем только кэш для наших видеофайлов, а у остальных процессов все по стандарту.

сброс идет только после закрытия целого чанка fMP4

В таком случае, да, выглядит разумным решением.

это глобальная настройка всего железного сервера

Это скорее повод задуматься об изоляции нагрузок. Например, через виртуализацию, контейнеры или те же cgroups.

Если что то записывать на диск в несколько потоков без танцев с бубном то страшный линукс сожрет всю память и позовет оом киллера?

Миллион раз писал тонны данных на диск, и банальной командой copy и торрентами и как угодно еще и ни разу с этим не было ни малейших проблем (если не считать 12309 бгг).

банальной командой copy

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

А что за изыски у алгоритма который пишет с камер?

Изыски это не весь контекст. Там было tiny buffers AND simple read-write loop.

Про отличие от записи с камер — многопоточность, возможно, как ключевое отличие, ну и объёмы записи. В общем, не примитивный /bin/cp, да.

Приветствую! Баг 12309 вы вспомнили очень даже кстати, суть проблемы там похожа. Но вы путаете простую десктопную нагрузку(скопировать файлы, торренты), с хайлоадом который пашет 24/7. При copy ядро линукса действительно само выбросит чистык страницы кеша, когда память кончается. Но разница в том, что 100 камер генерят огромный(по сути бесконечный) пооток грязных страниц, я граязные страницы ядро не может отбросить, пока не сбросит на физический блин диска. Если же диски не поспевают, эти грязные страницы копятся до конечной. И когда соседному процессу срочно нужна память - линукс фризит все I/O, либо вытесняет в своп полезную(!) память соседей по процессам, а вот потом он "позовет ООМ киллера".
Флаг FADV_DONTNEED в этой реализацци, это не костыль от падения, в скорее просто гарнатия того что архив не сожрет 90% озу, и системе останется ресурсы для бизнес-логики.

Звучит так, будто самым важным исправлением был как раз Sync, который приостановит писателя в ситуации “диски не поспевают”. А FADV_DONTNEED - лишь небольшая оптимизация поверх.

Вызов Sync на каждый чанк блокирует горутину, лучше сбрасывать буферы асинхронно через отдельный воркер, тогда ядро стриминга вообще не заметит задержек диска

Как я уже отвечал на другой комментарий, если бы Sync дергался в гланом цикле стриминга - то да вы бы были правы. Но именно поэтому сам рекордер, это отдельная асинхронная горутина. Ядро раздает всем всем воркерам кадры через неблокирующие каналы. Если диск тупит и Sync повиснет, ядро этого просто не замечает. Канал рекордера забивается = дропаем кадры только для него. Все остальные подписчики работают без задержек.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации