Производительность
От чего зависит нагрузка, как настроить хост, как правильно измерить загрузку процессора и что делать, когда ресурсов не хватает.
Сервер рассчитан на тысячи потоков и тысячи зрителей на одной машине. Чтобы это работало, нужно понимать три вещи: что именно тратит ресурсы, как их правильно измерять и какие настройки хоста обязательны.
Ресурсы расходуются в строгом порядке значимости: процессор → память → сеть. На реальной установке первым заканчивается процессор, вторым — память, и только в последнюю очередь упирается сеть. Планируйте и диагностируйте в том же порядке.
Во всех примерах media.example.com — ваш сервер, $TOKEN — токен доступа к API. Получить токен:
TOKEN=$(curl -sS -X POST https://media.example.com/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"password":"<пароль-администратора>"}' | sed -E 's/.*"token":"([^"]+)".*/\1/')
Что нагружает сервер#
| Ресурс | Основной потребитель | Растёт от |
|---|---|---|
| Процессор | приём и упаковка потоков + отправка байт зрителям | числа потоков и числа зрителей |
| Память | кольцевые буферы фрагментов, по одному на поток | числа потоков × битрейт × глубина окна |
| Сеть | сумма входящего и исходящего трафика | битрейтов потоков и числа зрителей |
Процессор#
Работа делится на две неравные части.
- Приём и упаковка потока. Выполняется один раз на поток, независимо от того, сколько протоколов раздачи включено и сколько зрителей смотрит. Поток закреплён за одним ядром — см. Архитектуру.
- Отправка зрителю. Выполняется для каждой сессии. Это самая массовая работа на нагруженном сервере, и она пропорциональна числу отправленных байт.
Отсюда практические выводы:
- Лишние протоколы раздачи почти бесплатны. Упаковка общая; второй и третий протокол добавляют только отправку. Не выключайте протоколы «ради экономии» — экономии не будет.
- Тысяча зрителей одного потока дешевле, чем сто зрителей десяти потоков. В первом случае упаковка выполняется один раз, во втором — десять. То же верно и для памяти: зритель на другом ядре получает ссылку на готовый фрагмент, а не его копию.
Дороже среднего обходятся:
| Что | Почему дороже |
|---|---|
| HTTPS / RTMPS / RTSPS / WSS | шифрование каждого исходящего байта и рукопожатие на каждое соединение |
| WebRTC (WHEP) | у каждого зрителя своё шифрование и свой поток пакетов — общей отправки нет |
| RTSP по UDP | отправка отдельными RTP-пакетами вместо крупных фрагментов |
Запись архива (DVR), напротив, дешёвая: на диск дописываются уже упакованные фрагменты, без повторной упаковки и без копирования.
Пока поток открыт, сегменты отдаются как неизменяемые и кэшируются браузером и CDN. Как только
включается авторизация просмотра (auth_backend) или защита от хотлинка (allowed_referers),
сегменты этого потока помечаются как некэшируемые — и весь трафик приходит на сервер, а не в
кэш. Для крупной раздачи это может означать кратный рост исходящего трафика. Планируйте заранее.
Память#
Основной потребитель — кольцевой буфер фрагментов у каждого потока. Его размер оценивается как:
память на поток ≈ битрейт потока × segment_seconds × (segments_count + 1)
Единица прибавляется потому, что сверх окна манифеста в памяти удерживается ещё один сегмент — для
клиентов, которые запросили его с опозданием. При значениях по умолчанию (segment_seconds 2,
segments_count 5) поток на 4 Мбит/с занимает под буфер примерно 6 МБ.
Зрители памяти почти не добавляют: они читают общий буфер по курсору. Отдельных очередей на зрителя нет — медленный клиент отстаёт только сам и никогда не тормозит ни поток, ни соседей.
file:// целиком лежит в памятиЛокальный файл, поднятый как живой поток, читается в память на весь срок жизни потока. Десять
потоков из одного файла — это десять его копий в оперативной памяти. Для file:// используйте
короткие ролики; для больших файлов — библиотеку VoD, она читает с диска порциями.
Служебные буферы наблюдаемости фиксированы и в расчёт ёмкости не входят: журнал в памяти — 4096 записей, аудит — 4096, лента событий — 512 кадров, ряды метрик — по 60 отсчётов на уровень. Ничто из этого не переживает перезапуск.
Сеть#
Считается прямо: сумма входящего трафика (битрейты всех источников) плюс сумма исходящего (битрейт × число зрителей на каждом потоке). Отдельно учитывайте копирование архива в S3, если оно настроено, — это дополнительный исходящий трафик.
Директива cores#
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
cores |
целое ≥ 0. 0 — все производительные ядра хоста; положительное число — ровно столько ядер, начиная с самых быстрых |
0 |
рестарт |
cores задаёт, сколько рабочих ядер займёт сервер. Каждое ядро владеет своими соединениями и
своими потоками; поток целиком обрабатывается на одном ядре, а зрители распределяются по всем.
Проверить, что применилось, можно по строке в журнале при старте:
sudo journalctl -u hastreamer -n 50 --no-pager | grep 'hastreamer —'
hastreamer — 3 stream(s) · http :8080 (HLS+player+api) · rtsp :8554 · 16 cores (cpu affinity [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])
cores 1 — это не «поменьше нагрузки»Директива не ограничивает потребление, а сводит всю работу на одно ядро. С cores 1 все потоки
до единого встают на ядро 0: оно упирается в 100 %, а остальные ядра простаивают. Симптом на
работающем сервере — рывки видео и рост задержки при том, что общая загрузка хоста невелика.
Если нужно ограничить потребление, ограничивайте его средствами systemd (CPUQuota=), а не
директивой cores.
Посмотреть фактическое распределение потоков по ядрам:
curl -sS "https://media.example.com/api/v1/streams?fields=id,core&limit=200" \ -H "Authorization: Bearer $TOKEN"
{"items":[{"core":0,"id":"cam1"},{"core":1,"id":"cam2"},{"core":2,"id":"cam3"}]}
Если в ответе у всех потоков "core": 0 — проверьте cores в конфигурации.
Как посчитать ёмкость сервера#
Универсальной формулы «столько-то потоков на ядро» не существует: цена зависит от битрейта, разрешения, длины GOP, набора протоколов и доли TLS. Ёмкость считают измерением на пилоте, а не по таблице.
Порядок действий.
-
Поднимите репрезентативную выборку. Возьмите 10–20 реальных источников — тех же моделей и тех же профилей, что пойдут в эксплуатацию.
-
Дайте типовую нагрузку зрителями. Важно попасть в реальное соотношение: сколько зрителей на поток, по каким протоколам, какая доля HTTPS.
-
Измерьте занятые ядра и память способом из следующего раздела, на установившемся режиме, а не в первые секунды после старта.
-
Разделите и экстраполируйте. Стоимость приёма линейна по числу потоков, стоимость отдачи — по числу отправленных байт. Считайте два слагаемых отдельно:
занятые ядра ≈ (ядер на поток) × число потоков + (ядер на Гбит/с отдачи) × отдача
-
Оставьте запас. Планируйте установившуюся загрузку не выше 70 % рабочих ядер. Запас уходит на пики переподключений, на утренние всплески зрителей и на обслуживание архива.
По памяти считайте по формуле из раздела выше и добавьте служебный расход процесса. Текущий расход
всегда виден в rss_mb.
Практическое правило: процессор упирается раньше памяти, а память — раньше сети. Если у вас
кончилась память при незагруженном процессоре, ищите причину, а не докупайте память: почти всегда
это источник file:// или слишком большое окно сегментов.
Настройка хоста#
Часть настроек сервер применяет сам при старте — но только если запущен от root (штатный
hastreamer.service так и запускается). Всё, что он не смог применить, он печатает в журнал в виде
готовых команд.
Проверьте, что он написал при последнем старте:
sudo journalctl -u hastreamer --no-pager | grep 'tune:' | tail -20
Нормальный вывод на настроенном хосте:
tune: fd limit 1048576 ✓ tune: host sysctl + governor already optimal ✓
Если хост настроен не полностью, вывод будет таким:
tune: ⚠ host is not optimally tuned. Run as root (or drop into /etc/sysctl.d/99-hastreamer.conf): sysctl -w net.core.somaxconn="65535" # a 25k-viewer connect storm overflows a 4096 accept backlog
Лимит файловых дескрипторов#
Каждое соединение зрителя, каждый источник и каждый файл архива — это дескриптор. Значения по умолчанию в дистрибутивах (1024) не годятся: сервер упрётся в них мгновенно.
Лимит задаётся в юните systemd и поднимается сервером до жёсткого значения при старте:
# /etc/systemd/system/hastreamer.service LimitNOFILE=1048576 LimitMEMLOCK=infinity
LimitMEMLOCK=infinity нужен потому, что io_uring регистрирует буферы и файлы в ядре и не должен
наследовать низкий лимит дистрибутива.
sudo journalctl -u hastreamer --no-pager | grep 'tune: fd limit' | tail -1
Ожидается tune: fd limit 1048576 ✓. Строка вида tune: ⚠ fd limit is 1024 (< 100k) означает, что
LimitNOFILE не применился — проверьте юнит и выполните systemctl daemon-reload.
Текущее число открытых дескрипторов всегда видно в GET /api/v1/system (поле open_fds).
Параметры ядра (sysctl)#
Установщик кладёт файл /etc/sysctl.d/99-hastreamer.conf. Его содержимое:
net.ipv4.tcp_ecn = 0 kernel.io_uring_disabled = 0 net.ipv4.ip_local_port_range = 1024 65535 net.core.netdev_max_backlog = 50000 net.core.somaxconn = 65535 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.ipv4.tcp_rmem = 4096 131072 16777216 net.ipv4.tcp_wmem = 4096 16384 16777216 net.ipv4.udp_rmem_min = 16384 net.ipv4.udp_wmem_min = 16384 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15 fs.file-max = 2097152 fs.nr_open = 2097152 vm.swappiness = 1 vm.max_map_count = 1048576
Применить без перезагрузки:
sudo sysctl --system
Смысл ключевых значений:
| Параметр | Зачем |
|---|---|
kernel.io_uring_disabled = 0 |
обязателен — без io_uring сервер не работает |
net.ipv4.tcp_ecn = 0 |
на некоторых маршрутах согласование ECN приводит к потере фрагмента TLS-приветствия, и соединение зависает до завершения рукопожатия |
net.ipv4.ip_local_port_range |
тысячи исходящих подключений к камерам и пары UDP-портов на зрителя исчерпывают узкий диапазон |
net.core.somaxconn, netdev_max_backlog |
всплеск подключений не должен переполнять очереди приёма |
net.core.rmem_max, wmem_max |
потолок буферов сокета; ниже него udp_ingest_rcvbuf будет обрезан ядром |
vm.swappiness = 1 |
живые буферы видео не должны уходить в подкачку |
Ядро Linux 5.1 или новее с разрешённым io_uring. Если kernel.io_uring_disabled равно 1 или 2,
сервер не сможет работать — политики безопасности, запрещающие io_uring, несовместимы с продуктом.
Проверка: cat /proc/sys/kernel/io_uring_disabled должно вернуть 0.
Регулятор частоты процессора#
Экономичные режимы регулятора дают заметный разброс задержки. При запуске от root сервер сам
переводит все ядра в режим performance; иначе печатает команду. Проверить вручную:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
performance
Ограничения ресурсов в юните#
В штатном юните заданы жёсткие потолки cgroup v2 — подгоните их под свою машину:
MemoryHigh=6G # мягкий порог: ядро начинает вытеснять страницы MemoryMax=8G # жёсткий потолок TasksMax=8192 # CPUQuota=800% # 800% = восемь ядер; снимите комментарий, чтобы ограничить процессор
MemoryMax — это гарантия, что процесс не уронит хост. Сопоставляйте CPUQuota с cores, оставляя
одно-два ядра хосту.
Как измерять загрузку правильно#
Это самая частая ошибка при разборе жалоб на производительность.
%cpu из topМгновенный %cpu в top, htop или ps — это одна выборка, и на многоядерном сервере с
короткими всплесками работы её шум сопоставим с самим значением. По одному такому числу нельзя ни
принять решение, ни сравнить два замера.
Правильно: взять прирост процессорного времени процесса за интервал и разделить его на длину интервала. Получится число занятых ядер — величина, которую можно складывать, сравнивать и экстраполировать.
Способ 1 — считать самому#
Скрипт ниже реализует ровно это правило: (cputime₂ − cputime₁) ÷ длину интервала.
sudo tee /usr/local/bin/hs-cpu > /dev/null <<'EOF' #!/usr/bin/env bash # Загрузка процессора сервером = прирост процессорного времени ÷ длину интервала. # Результат — число ЗАНЯТЫХ ЯДЕР. Использование: hs-cpu [интервал_сек] PID=$(systemctl show -p MainPID --value hastreamer) SECS=${1:-10} HZ=$(getconf CLK_TCK) ticks() { awk '{ n = split($0, a, ") "); split(a[n], f, " "); print f[12] + f[13] }' "/proc/$PID/stat"; } T1=$(ticks); sleep "$SECS"; T2=$(ticks) awk -v d="$((T2 - T1))" -v hz="$HZ" -v s="$SECS" \ 'BEGIN { printf "занято ядер: %.2f\n", d / hz / s }' EOF sudo chmod +x /usr/local/bin/hs-cpu sudo hs-cpu 30
занято ядер: 3.41
Интервал берите не меньше 10 секунд, а для отчётных замеров — 30–60. Короткий интервал возвращает тот же шум, от которого вы уходили.
Способ 2 — спросить сервер#
Сервер измеряет себя тем же способом и публикует результат:
curl -sS https://media.example.com/api/v1/system \ -H "Authorization: Bearer $TOKEN"
{"cpus":20,"worker_cores":16,"host_cpu_busy_pct":22.4, "per_core_busy_pct":[24.1,21.7,23.0,19.8,22.5,20.9,23.6,21.2, 22.8,20.4,23.9,21.6,22.1,20.7,23.3,21.9,0.0,0.0,0.0,0.0], "proc_cpu_cores":3.41,"rss_mb":1024, "mem_total_mb":65536,"mem_available_mb":40960,"open_fds":4231,"uptime_s":86400, "udp_ingest_kernel_drops":0,"udp_ingest_queue_dropped":0, "udp_ingest_recv_errs":0,"udp_ingest_rx_queued_kb":0,"udp_egress_bind_fails":0}
| Поле | Что означает |
|---|---|
proc_cpu_cores |
занятые ядра процессом — прирост процессорного времени ÷ время. Сравнивать с показаниями top нельзя |
worker_cores |
сколько ядер занял сервер (результат cores) |
per_core_busy_pct |
загрузка каждого ядра хоста — так видно перекос на одно ядро |
rss_mb |
память процесса |
open_fds |
открытые дескрипторы |
udp_ingest_kernel_drops |
датаграммы, отброшенные ядром на приёме RTSP-по-UDP |
Способ 3 — метрики#
Те же величины отдаются в формате Prometheus:
curl -sS https://media.example.com/metrics \ -H "Authorization: Bearer $TOKEN" | grep -E '^hastreamer_(process|worker)'
hastreamer_process_cpu_cores 3.4100 hastreamer_worker_cores 16 hastreamer_process_rss_bytes 1073741824
Это те же величины, что в /api/v1/system, только в форме экспозиции: proc_cpu_cores →
hastreamer_process_cpu_cores, rss_mb → hastreamer_process_rss_bytes.
/metrics требует учётных данныхЭндпоинт закрыт и принимает те же токены, что и /api/v1, только заголовком Authorization.
Выдайте системе мониторинга отдельный токен чтения — см.
Мониторинг и метрики.
Что делать при нехватке ресурсов#
Сначала определите, какой ресурс кончился, а потом действуйте по таблице.
| Симптом | Что это значит | Что делать |
|---|---|---|
proc_cpu_cores близко к worker_cores |
процессор исчерпан | увеличьте cores, если ядра хоста свободны; иначе разносите нагрузку на второй сервер |
одно ядро в per_core_busy_pct занято, остальные простаивают |
вся работа на одном ядре | проверьте cores — вероятно, стоит 1; либо на этом ядре стоит поток с непропорционально высоким битрейтом |
rss_mb растёт, процессор свободен |
память тратится не на отдачу | ищите источники file://; уменьшите segments_count и segment_seconds |
mem_available_mb близко к нулю |
память хоста исчерпана | уменьшите окно сегментов, снизьте MemoryHigh, вынесите часть потоков |
udp_ingest_kernel_drops растёт |
ядро не успевает вычитывать RTSP-по-UDP | поднимите udp_ingest_rcvbuf (и net.core.rmem_max под него), затем udp_ingest_pool_slots |
open_fds близко к лимиту |
кончаются дескрипторы | поднимите LimitNOFILE, проверьте, не осталось ли брошенных сессий |
| растёт задержка, ресурсы свободны | это не ресурсы | см. Диагностику неисправностей |
Что даёт наибольший эффект, в порядке убывания:
- Уберите из отдачи то, чего никто не смотрит. Включите
on_demandна потоках, которые запрашивают редко: без зрителей они не подключаются к источнику вовсе. Работает только на потоках без записи архива — у записываемого потокаon_demandигнорируется. - Снизьте битрейт на источниках. Самый действенный рычаг: он уменьшает сразу процессор, память и сеть. Перекодирования на сервере нет — профили задаются на камере.
- Ограничьте число сессий.
max_sessions <n>;— общий потолок по всем протоколам; сверх него новые зрители получают отказ, а уже подключённые не отключаются. - Уменьшите окно сегментов.
segments_countиsegment_secondsпрямо задают память на поток. - Сохраните кэшируемость сегментов там, где это допустимо. Если поток и так публичный, не
включайте на нём
allowed_referers— иначе весь его трафик пойдёт мимо кэшей на сервер.
Тонкая настройка приёма по RTSP/UDP#
Эти параметры имеют смысл только если камеры забираются по UDP (source_transport udp или
udp-then-tcp). При транспорте по умолчанию (tcp) они не действуют.
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
udp_ingest_rcvbuf |
байты, 65536 … 1073741824 |
16777216 |
рестарт |
udp_ingest_shards |
целое 1 … 16 |
2 |
рестарт |
udp_ingest_pool_slots |
целое 64 … 4096 |
1024 |
рестарт |
udp_pull_rate |
попыток в секунду, 0 … 100000; 0 — без ограничения |
100 |
рестарт |
Порядок действий при потерях: сначала растёт udp_ingest_kernel_drops — поднимайте
udp_ingest_rcvbuf (и net.core.rmem_max не ниже него, иначе ядро обрежет значение). Если растёт
udp_ingest_recv_errs — поднимайте udp_ingest_pool_slots. udp_pull_rate сглаживает всплеск
одновременных подключений после перезапуска.
udp://Перечисленные параметры настраивают маршрутизатор RTSP-по-UDP. У приёма MPEG-TS через
input udp://… собственный маршрутизатор с фиксированными буферами, и он этими директивами
не настраивается.
Куда двигаться дальше#
- Мониторинг и метрики — что снимать постоянно и на что настраивать оповещения.
- Диагностика неисправностей — если проблема не в ресурсах.
- Требования и подбор оборудования — с чего начинать выбор машины.
- Применение изменений — какие из перечисленных директив требуют перезапуска.