Как мы раздали 8 Гбитс видео на одном ядре CPU на GO. Проблема грохочущего стада
- пятница, 7 августа 2026 г. в 00:00:06
Я до сих пор помню тот день, когда случайно положил собственный тестовый сервер.
Схема была банальной: пара IP-камер, простенький бэкенд, всё это выплёвывает видео в веб-морду. На тестах работало идеально. А потом кто-то закинул ссылку на трансляцию в корпоративный чат.
По ссылке одновременно кликнула тысяча(ладно, ладно вру конечно, но запросов было много) человек. Сервер завизжал кулерами, оператива кончилась за три секунды, а камера вообще перестала пинговаться.
Добро пожаловать в проблему Thundering Herd (эффект леммингов).
Если вы хоть раз пробовали масштабировать раздачу live-видео, вы эту боль знаете. Когда толпа зрителей ломится смотреть один и тот же поток, типичный сервер пытается честно открыть новое соединение или запустить отдельный процесс отдачи для каждого. Итог всегда один — OOM (Out of memory) и лежачий процессор.
Я не хотел решать это вливанием денег в толстые AWS-инстансы или покупкой нового железа. Была идея маршрутизировать всё максимально дешево, используя Go. Спойлер не будет долгим: в итоге мы выжали 8.8 Гбит/с на одном ядре процессора, причем сборщик мусора (GC) в этот момент вообще отдыхал.
Рассказываю, как мы собрали Ruseon Core.
Ну, FFmpeg — это великий инструмент. Я его обожаю. Вы его обожаете. Но запускать тысячу инстансов FFmpeg для рестрима — это самоубийство. Даже если использовать его просто как ingest-прокси, накладные расходы дикие.
Многие делают через MediaMTX. Отличная штука, правда. Но нам нужна была жесткая интеграция с пайплайном для нейросетей (AI Data Pipeline) и чтобы архив писался непрерывно в fMP4.
Поэтому мы написали движок с нуля. Главное правило — никакого транскодирования. Мы просто перекладываем байты.
Когда с камеры прилетает кадр (обычно это H.264, хотя реализовали и 265 кодек), это просто кусок байтов.
Самый тупой способ раздать его тысяче зрителей — скопировать этот кусок тысячу раз в разные буферы ответа. В Go это верная смерть: сборщик мусора сойдет с ума, пытаясь всё это вычистить. Процессор будет заниматься только уборкой, а не стримингом.
Мы пошли другим путем. Сделали Zero-Copy RingBuffer и прикрутили к нему sync.Pool.
Как это работает в голове:
Прилетает кадр с камеры.
Мы берем пустой байтовый буфер из заранее выделенного пула.
Записываем туда кадр (один раз!).
Раздаем указатель на этот буфер тысяче подписчиков.
Когда все прочитали — возвращаем буфер обратно в пул.
Аллокаций нет. Пауз на сборку мусора нет.
В бенчмарках это выглядит так:
BenchmarkWriteFrame-12 13.9 ns/op 0 B/op 0 allocs/op
Когда на горячем пути передачи видео ты видишь 0 B/op — это кайф. Движок молотит десятки тысяч кадров, а куча вообще не растет.
Писать быстрый код весело, но мне хотелось посмотреть, как он сломается.
Мы подняли нагрузочный тест через 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