Skip to content

Производительность RTP Translator

Резюме

RTP Translator был оптимизирован в два этапа:

  1. Из hot path убраны повторный разбор, сериализация и копирование RTP для каждого получателя. Маршрут ищется один раз, а отправка выполняется по immutable snapshot вне mutex.
  2. На Linux последовательные sendto заменены пакетной отправкой sendmmsg: один системный вызов обслуживает до 64 получателей.

На контрольном Linux-сервере пакетная отправка дала следующий результат:

Сценарий Scalar sendto Batch sendmmsg Результат
16 получателей, 10 000 RTP/с 2 590 ms CPU 2 475 ms CPU -4,5% CPU
64 получателя, 5 000 RTP/с 6 821 ns/packet 5 038 ns/packet -26,1% CPU/packet
Burst, 50 000 входных RTP 1 515 принято 1 652 принято +9,0% throughput
Syscall, 16 получателей 18 717 вызовов 2 217 вызовов -88,2% syscall

Главный результат

При штатной нагрузке обе реализации доставили 100% принятых пакетов. На границе производительности batch-режим сохранил 98,7-100% входного потока, когда scalar-режим принимал только 89,2-93,9%.

Сравнение CPU scalar и batch

Что именно оптимизировано

Translator получает plain RTP от автора и отправляет тот же datagram всем зарегистрированным получателям. Payload, RTP header и SSRC при fan-out не меняются, поэтому полная десериализация и повторная сериализация для каждого адресата не нужны.

flowchart LR
    P["Publisher UDP socket"]
    R["UDPSocket::run<br/>recvfrom"]
    V["RTP validation<br/>read SSRC"]
    M["Route lookup<br/>short mutex"]
    S["Immutable receiver snapshot"]

    subgraph Scalar["Scalar mode"]
        ST["N x sendto"]
    end

    subgraph Batch["Linux batch mode"]
        MM["sendmmsg<br/>up to 64 receivers"]
    end

    C["Real UDP receiver sockets"]

    P --> R --> V --> M --> S
    S -->|benchmark: scalar| ST --> C
    S -->|Linux production| MM --> C

До оптимизации

Для каждого входного RTP выполнялись:

  • разбор RTP в объект;
  • удержание mutex во время обхода получателей;
  • сериализация RTP для каждого получателя;
  • отдельный sendto для каждого адреса;
  • отдельные atomic increments для каждого отправленного пакета;
  • линейный поиск translation для adjustment RTP и RTCP.

После первого этапа

Текущий raw datagram path:

  • валидирует RTP header и извлекает SSRC без создания RTPPacket;
  • использует O(1) индекс receiver_ssrc -> author_ssrc;
  • берет immutable snapshot адресов под коротким mutex;
  • выполняет fan-out после освобождения mutex;
  • обновляет агрегированные счетчики один раз на входной RTP;
  • не выполняет heap allocation в packet fan-out path.

После второго этапа

Linux path дополнительно:

  • собирает до 64 mmsghdr и iovec на стеке;
  • передает один RTP buffer сразу нескольким адресатам;
  • повторяет sendmmsg, если kernel принял только часть batch;
  • при ошибке переключает оставшиеся адреса на scalar sendto;
  • делит fan-out больше 64 получателей на несколько batch.

На macOS и Windows SendBatch использует scalar fallback. Это сохраняет единый API и прежнее сетевое поведение на платформах без sendmmsg.

Методика измерения

Benchmark использует production-классы Translator, RTPSocket и UDPSocket, а не отдельную модель алгоритма.

sequenceDiagram
    participant B as Benchmark publisher
    participant T as Translator
    participant R as UDP receivers
    participant C as Collector

    B->>T: adjustment RTP per receiver_ssrc
    T->>T: store receiver UDP addresses
    B->>T: malformed RTP self-check
    T--xR: packet must not be forwarded
    B->>T: warmup RTP
    T->>R: scalar or batch fan-out
    B->>T: measured RTP stream
    T->>R: real loopback UDP datagrams
    R->>C: poll + recv counters
    C->>C: compare expected/attempted/received

Измеряются:

Метрика Значение
input_sent RTP, отправленные генератором в Translator.
input_translated RTP, принятые и распознанные Translator.
fanout_attempted Суммарное число отправок получателям.
fanout_received Datagrams, реально прочитанные receiver sockets.
input_acceptance_ratio Устойчивость входной UDP-очереди и Translator под нагрузкой.
fanout_delivery_ratio Доставка уже принятых Translator пакетов.
process_cpu_ms CPU всего benchmark-процесса, включая publisher и collector.
cpu_ns_per_attempted_packet CPU, нормализованный на одну fan-out отправку.

Почему CPU-оценка консервативна

Receiver collector работает в том же процессе и выполняет одинаковое количество recv в обоих режимах. Поэтому process CPU включает неизменяемую стоимость приема пакетов и занижает относительный выигрыш непосредственно внутри Translator. Для проверки механизма дополнительно использовался strace.

Тестовое окружение

Измерения выполнены 30 июля 2026 года на stage1.videograce.ru.

Параметр Значение
CPU Intel Xeon Processor (Icelake)
vCPU 2
ОС Ubuntu 24.04
Kernel 6.8.0-134-generic
glibc 2.39
Payload 1 200 bytes
Source revisions scalar baseline 06894fcf2, Linux batch 52a8e8d29
Build C++17, Release, -j2

Во время теста сервер не перезапускался. Benchmark запускался отдельным процессом на том же VPS, поэтому небольшая вариативность от фоновой нагрузки ожидаема.

Результаты: штатный fan-out

16 получателей, 10 000 RTP/с

Каждый режим запускался пять раз по три секунды. В каждом прогоне:

  • отправлено 30 000 входных RTP;
  • выполнено 480 000 fan-out отправок;
  • получено 480 000 UDP datagrams;
  • потерь на входе и fan-out не зафиксировано.
Метрика, медиана пяти прогонов Scalar Batch Изменение
Process CPU 2 590 ms 2 475 ms -4,5%
CPU на fan-out packet 5 396 ns 5 155 ns -4,5%
Input acceptance 100% 100% без изменений
Fan-out delivery 100% 100% без изменений
Все пять измерений CPU
Прогон Scalar, ms Batch, ms
1 2 590 2 358
2 2 510 2 542
3 2 588 2 509
4 2 696 2 322
5 2 740 2 475

Результаты: высокий fan-out

64 получателя, 5 000 RTP/с

Это нагрузка до 320 000 исходящих UDP datagrams в секунду. На ней scalar-режим перестает успевать разгружать входную очередь.

Прогон Scalar input Batch input Scalar ns/packet Batch ns/packet
1 89,2% 100% 7 036 5 038
2 90,2% 100% 6 821 4 569
3 93,9% 98,7% 6 173 5 650
Медиана 90,2% 100% 6 821 5 038

Batch-режим снизил нормализованный CPU примерно на 26,1% и сдвинул границу насыщения Translator.

Отдельная проверка chunking:

receivers=128
input_sent=1000
input_translated=1000
fanout_expected=128000
fanout_received=128000
delivery=100%

Устойчивость Translator под нагрузкой

Результаты: burst

Burst-сценарий отправляет 50 000 RTP без pacing. Он намеренно перегружает входную UDP-очередь и показывает, насколько быстро Translator успевает освобождать ее.

Метрика, медиана пяти прогонов Scalar Batch Изменение
Принято входных RTP 1 515 1 652 +9,0%
CPU на fan-out packet 10 211 ns 9 649 ns -5,5%
Fan-out delivery принятых RTP 100% 100% без изменений

Низкий процент input_acceptance_ratio в burst не означает потери внутри sendmmsg: пакеты отбрасываются раньше, когда генератор мгновенно переполняет входную UDP-очередь. Все RTP, которые Translator успел принять, были доставлены всем получателям.

Системные вызовы

strace -f -c выполнялся для 1 000 RTP/с, 16 получателей и 100 warmup packets.

Режим sendto sendmmsg Всего send syscall
Scalar 18 717 0 18 717
Batch 1 117 1 100 2 217

Из 1 117 sendto batch-прогона 1 100 относятся к publisher и warmup generator, еще 17 - к adjustment и malformed-packet проверкам. Сам Translator выполнил 1 100 sendmmsg вместо 17 600 sendto, то есть один fan-out syscall вместо шестнадцати.

Сокращение системных вызовов

Воспроизведение

Сборка

cmake -S . -B build-linux \
  -DVG_BUILD_TRANSLATOR_BENCHMARK=ON

cmake --build build-linux \
  --target TranslatorLoadBenchmark VideoGraceServer \
  -j2

Одинаковый workload в двух режимах

BENCH=./build-linux/Server/TranslatorLoadBenchmark

$BENCH \
  --receivers 16 \
  --rate-pps 10000 \
  --duration-seconds 3 \
  --warmup-packets 500 \
  --send-mode scalar

$BENCH \
  --receivers 16 \
  --rate-pps 10000 \
  --duration-seconds 3 \
  --warmup-packets 500 \
  --send-mode batch

Проверка syscall

strace -f -c -e trace=sendto,sendmmsg \
  $BENCH \
  --receivers 16 \
  --rate-pps 1000 \
  --duration-seconds 1 \
  --warmup-packets 100 \
  --send-mode batch

Benchmark печатает одну JSON-строку, пригодную для сохранения в CI или последующей агрегации.

Production behavior

Платформа Поведение
Linux Batch включен по умолчанию, до 64 адресатов на sendmmsg.
macOS Scalar fallback через общий SendBatch API.
Windows Scalar fallback через общий SendBatch API.
Benchmark Режим явно выбирается через --send-mode scalar\|batch.

При запуске Translator пишет активный режим:

Translator[5060] started (batch_send: Y)

Переключение режима разрешено только до Translator::Start(). Это исключает data race и изменение transport semantics во время активного потока.

Ограничения и следующий этап

Пакетная отправка устраняет основную стоимость исходящего fan-out, но не меняет входной путь:

  • Translator по-прежнему получает по одному datagram через recvfrom;
  • экстремальный burst может переполнить kernel receive queue;
  • benchmark process CPU включает receiver collector;
  • loopback не моделирует NIC queue, qdisc и реальную сеть.

Следующий обоснованный этап:

  1. Добавить счетчик kernel RX overflow (SO_RXQ_OVFL) в диагностику Translator.
  2. Измерить recvmmsg для входного RTP без изменения порядка обработки.
  3. Повторить A/B на 16, 64 и 128 получателях.
  4. После выкладки сравнить production CPU и packet loss при реальных конференциях.

Не оптимизировать вслепую

recvmmsg имеет смысл только после появления наблюдаемого RX overflow или подтвержденного профилем bottleneck в recvfrom. Текущий sendmmsg уже устраняет наиболее дорогую часть fan-out и дает измеримый выигрыш на том же сервере.