Производительность RTP Translator
Резюме
RTP Translator был оптимизирован в два этапа:
- Из hot path убраны повторный разбор, сериализация и копирование RTP для каждого получателя. Маршрут ищется один раз, а отправка выполняется по immutable snapshot вне mutex.
- На 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%.
Что именно оптимизировано
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%
Результаты: 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 и реальную сеть.
Следующий обоснованный этап:
- Добавить счетчик kernel RX overflow (
SO_RXQ_OVFL) в диагностику Translator. - Измерить
recvmmsgдля входного RTP без изменения порядка обработки. - Повторить A/B на 16, 64 и 128 получателях.
- После выкладки сравнить production CPU и packet loss при реальных конференциях.
Не оптимизировать вслепую
recvmmsg имеет смысл только после появления наблюдаемого RX overflow или подтвержденного профилем bottleneck в recvfrom. Текущий sendmmsg уже устраняет наиболее дорогую часть fan-out и дает измеримый выигрыш на том же сервере.