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

Схема была банальной: пара IP-камер, простенький бэкенд, всё это выплёвывает видео в веб-морду. На тестах работало идеально. А потом кто-то закинул ссылку на трансляцию в корпоративный чат.

По ссылке одновременно кликнула тысяча(ладно, ладно вру конечно, но запросов было много) человек. Сервер завизжал кулерами, оператива кончилась за три секунды, а камера вообще перестала пинговаться.

Добро пожаловать в проблему Thundering Herd (эффект леммингов).

Если вы хоть раз пробовали масштабировать раздачу live-видео, вы эту боль знаете. Когда толпа зрителей ломится смотреть один и тот же поток, типичный сервер пытается честно открыть новое соединение или запустить отдельный процесс отдачи для каждого. Итог всегда один — OOM (Out of memory) и лежачий процессор.

Я не хотел решать это вливанием денег в толстые AWS-инстансы или покупкой нового железа. Была идея маршрутизировать всё максимально дешево, используя Go. Спойлер не будет долгим: в итоге мы выжали 8.8 Гбит/с на одном ядре процессора, причем сборщик мусора (GC) в этот момент вообще отдыхал.

Рассказываю, как мы собрали Ruseon Core.

Почему мы просто не взяли FFmpeg?

Ну, FFmpeg — это великий инструмент. Я его обожаю. Вы его обожаете. Но запускать тысячу инстансов FFmpeg для рестрима — это самоубийство. Даже если использовать его просто как ingest-прокси, накладные расходы дикие.

Многие делают через MediaMTX. Отличная штука, правда. Но нам нужна была жесткая интеграция с пайплайном для нейросетей (AI Data Pipeline) и чтобы архив писался непрерывно в fMP4.

Поэтому мы написали движок с нуля. Главное правило — никакого транскодирования. Мы просто перекладываем байты.

Трюк с Zero-Copy RingBuffer

Когда с камеры прилетает кадр (обычно это H.264, хотя реализовали и 265 кодек), это просто кусок байтов.

Самый тупой способ раздать его тысяче зрителей — скопировать этот кусок тысячу раз в разные буферы ответа. В Go это верная смерть: сборщик мусора сойдет с ума, пытаясь всё это вычистить. Процессор будет заниматься только уборкой, а не стримингом.

Мы пошли другим путем. Сделали Zero-Copy RingBuffer и прикрутили к нему sync.Pool.

Как это работает в голове:

  1. Прилетает кадр с камеры.

  2. Мы берем пустой байтовый буфер из заранее выделенного пула.

  3. Записываем туда кадр (один раз!).

  4. Раздаем указатель на этот буфер тысяче подписчиков.

  5. Когда все прочитали — возвращаем буфер обратно в пул.

Аллокаций нет. Пауз на сборку мусора нет.

В бенчмарках это выглядит так:

BenchmarkWriteFrame-12    13.9 ns/op      0 B/op       0 allocs/op

Когда на горячем пути передачи видео ты видишь 0 B/op — это кайф. Движок молотит десятки тысяч кадров, а куча вообще не растет.

Насилуем сервер через k6

Писать быстрый код весело, но мне хотелось посмотреть, как он сломается.

Мы подняли нагрузочный тест через Grafana k6. Сценарий жесткий: 1000 виртуальных пользователей долбят Edge HLS Muxer, постоянно скачивая index.m3u8 и вытягивая бинарные куски .ts по мегабайту так быстро, как сервер может их отдавать.

Честно? Я думал, роутер захлебнется на 300 юзерах.

Но вот что выдал терминал через 70 секунд избиения одного ядра старенького Ryzen 5 5600X:

data_received..................: 81 GB  1.1 GB/s
http_req_failed................: 0.00%  ✓ 0 ✗ 60822
http_req_duration..............: avg=3.13ms p(95)=6.13ms

Это 8.8 Гбит/с пропускной способности. 60 тысяч успешных HTTP-ответов. Ни одного обрыва соединения.

Секрет в том, что HLS-сегменты лежат тупо в оперативке. Когда толпа леммингов запрашивает segment_104.ts в одну и ту же миллисекунду, Go-сервер просто плюет им одни и те же байты из кэша. Диск спит. Переупаковки нет.

Где эта магия ломается

Если честно то — это не серебряная пуля. У физики есть свои лимиты.

Пока CPU чиллит на 1-2% загрузки, ваша сетевая карта борется за жизнь. 8.8 Гбит/с — это предел обычного 10G интерфейса. Если нагнать 2000 юзеров, код выдержит, но сетевое железо начнет дропать TCP-пакеты. Чудес не бывает.

А еще, если вы хоть немного накосячите с реализацией sync.Pool, вы получите ад. Я однажды вернул буфер в пул на 5 миллисекунд раньше, чем самый медленный клиент успел его дочитать. Итог? Нижняя половина видео просто становилась зеленой, потому что другой поток уже начал писать туда новые данные. Дебажить такие гонки данных в видеоплеере — то еще удовольствие.

Короче, если вы собираете пайплайн для нейросеток или просто раздаете камеры на кучу операторов, перестаньте забивать гвозди микроскопом и плодить сотни FFmpeg’ов. Контролируйте память. Перекладывайте байты без копирования.

Исходники и скрипты нагрузочного тестирования лежат в репозитории Ruseon Core в папке benchmarks/. Идите и попробуйте положить свой комп(я свой убил не раз).

Исходники: https://github.com/RUSEGAL/ruseon-core

Комментарии (8)


  1. ilya_panow
    06.08.2026 12:37

    Очень интересное инженерное решение - возьму на заметку!


    1. rusegal Автор
      06.08.2026 12:37

      Подскажите, что именно вызвало интерес, если не секрет конечно?


      1. Sap_ru
        06.08.2026 12:37

        Аккаунт зарегистрирован 2 дня назад, это его первый комментарий. Весьма бессмысленный. Возможно, что интерес вызвали нейросети.

        Хотя мне ваша статья в целом зашла. Всегда интересен чужой практический опыт.
        Разве что можно было бы буквально чуть-чуть подробнее.

        И есть вот такой принципиальный вопрос, который почему-то обойдён вниманием:
        Вы каким протоколом (протоколами?) стримите? Что будет, если у некоторого числа пользователей стабильно плохая связь, пошли ретрансмиты и ваши буфера стали накапливаться? Во что упрётся? Что если рассинхронизация и запаздывание у пользователей - как глубину регулируете?


        1. rusegal Автор
          06.08.2026 12:37

          Да, не догадался посмотреть на аккаунт ответившего выше, оказия :)

          Что касательно ответа на ваши вопросы, хотя я вроде старался подробно объяснить, видимо не все учел:

          1. Приходит к нам rtsp, отдаем по HLS на текущей реализации. В ближайшее время(уже в роадмапе) добавить WebRTC.

          2. Бесконечного накопления буфера(и впоследствии ООМ) не будет, реализована некоторая защита от этого:

            Базовый сетевой стек go (net/http), который по сути работает на уровне доставки. Сервер просто отдает статический кусок памяти в сокет. Все. Быстро
            ли, медленно ли качает - без разницы.

            А вот на уровне ядра(Ring.go) реализация защиты уже интереснее - внутри ядра подписчики(HLS Muxer или ИИ-воркеры) читают кадры через каналы в Go. Запись в канал реализована как неблокирующая отправка(через select + default). Канал имеет жестко заданную глубину. Если подписчик тормозит, его канал забивается, ядро не ждет его и не выделяет новую память. Тут работает ветка default - кадр дропается для этого конкретного подписчика. Тут к сожалению приходятся "жертовать" плохим клиентом, ради условных 1000 "хороших".

          3. Насчет рассинхронизации и запаздывания, если у подписчика внутри ядра случится дроп кадра из-за тормозов, ему нельзя давать следующий P-кадр т.к картинка рассыплется(к слову об этом даже есть упоминание в gortsplib, используемой в проекте либы для rtsp). В таком случае ядро ставит флаг NeedsIFrame в true, для него. Подписчик начинает молчать пока не прилетит следующий I-кадр(ключевой), с него чтение возобновится чисто и с минимальными потерями. В реальной работе, это милисекунды, для конечного зрителя по сути незаметна. Тем более что муксер хранит в памяти "скользящее окно" в 5 последний сегментов. Если у конечного зрителя сильно плохая связь, то при попытке скачать устаревший сегмент он получит 404. Плеер на клиенте(в 99% случаев все плеер у пользователей это hls.js, или его реализации) ловит 404, понимает что отстал от лайва, и перепргивает на актуальный сегмент, синхронизировавшись с остальными.


          1. Sap_ru
            06.08.2026 12:37

            2 Вот. Это прямо интересный момент, так как я видел запаздывание клиентов аж на 3 минуты. При этом нужно помнить, что буфера у сокетов в ядре тоже весьма приличные бывают. 1000 коннектов по 300 кб, это больше 300 МБ одних буферов.

            3 Тоже интересно. Миллисекунды, ха! Минуты! Один раз я видел 10 минут (!!!) запаздывание! У клиента проблемы со связью, он отстаёт, а потом связь восстанавливается, но с потерями пакетов, и скорости не хватает для полного потока. После чего запаздывание начинает расти бесконечно.
            Такие штуки часто забывают, и именно из-за этого или потоки битые начинают транслировать, или запаздывание до диких величин растёт. Приём это у топовых российских стриминговых платформ такие ошибки бывают. Например МатчТВ ЧМ по футболу транслировал через сервис с такой ошибкой, из-за чего у них в буферах по несколько минут видео зависало и серверам приходилось жёстко рвать соединения (или даже просто перезапускаться?).


            1. rusegal Автор
              06.08.2026 12:37

              По пункту 2(буфер сокетов в ос) вы абсолютны правы, физику не обманешь. В статье я естественно имею ввиду только юзерспейс память. Мой "поинт" был о том что не удваивать это проблему еще и на уровне ПО. А память ос, да неизбежный налог на сеть.

              А вот по пункту 3, мы видимо говорим немного о разных вещах. То что вы сейчас описали(накопление лага по сути до бесконечности) это типичная проблема Push-протколов. Сервер пытается протолкнуть данные в сокет, сокет блокируется из-за плохой сети клиента, пакеты копятся в очереди, и когда сеть "просирается", клиент начинает смотреть видео 10-минутной давности. Но мы мы раздаем поток по HLS(Pull-протколо) тут другая механика:
              1. HLS Muxer в нашей памяти держит жесткое скользящее окно (Live Playlist) — только 5 последних сегментов (как пример, это 10 секунд видео). Более старые сегменты беспощадно удаляются из кэша (GC собирает их).

              2. Клиент с плохой связью качает условный сегмент 100 3 минуты.

              3. Когда он его наконец скачал, плеер на клиенте просит 101 сегмент

              4. Но на сервере уже давно лежит сегмент 200. 101 уже физически нет.

              5. Сервер отдает честный 404 на запрос 101 сегмента.

              6. Плеер видит 404, перекачивает плейлист(index.m3u8) видит что актуальный 200, и делает прыжок на Live-край.

              Таким образом, бесконечного накопления лага не происходит. Клиент с плохой сетью просто будет видеть постоянные "прыжки" вперед и буфер (что логично при убитой сети), но он не заставит наш сервер хранить его персональный 10 минутный кэш.

              Проблема МатчТВ была, скорее всего, в том, что они использовали либо безразмерный DVR-плейлист (когда на сервере лежат вообще все сегменты матча), либо CDN кэшировал старые манифесты, из-за чего плееры сходили с ума. У нас же Live-окно жестко обрезается.


              1. Sap_ru
                06.08.2026 12:37

                Ну, вот этого в статье и не хватало :)


                1. rusegal Автор
                  06.08.2026 12:37

                  Ахах, принято, есть еще несколько интересных проблем которые решали/решаем в рамках этого проекта - так что в следующих статьях постораюсь побольше раскрыть "инженерной" мысли :)