---
url: /reference/benchmark.md
---
# 性能测试报告

比较 **rtp2httpd**、**[msd\_lite](https://github.com/rozhuk-im/msd_lite)**、**[udpxy](https://github.com/pcherenkov/udpxy)** 和 **[TVGate](https://github.com/qist/tvgate)** 在多频道、同频道多客户端及高码率负载下的 CPU 和内存占用。

## 测试环境与版本

* 测试日期：2026-09-06。
* 主机：Apple M3 Max；Parallels Ubuntu 24.04 虚拟机，16 vCPU、16 GiB 内存。
* 系统：Linux 6.8.0-138-generic，aarch64；所有程序均为原生 ARM64 执行。
* 编译器：GCC 13.3.0。rtp2httpd 使用 Release 与 `ENABLE_AGGRESSIVE_OPT=ON`；msd\_lite 使用 `-O3`、LTO 及相同的内联、循环展开、向量化优化选项；udpxy 使用 `-O3 -flto`。TVGate 使用官方发布的二进制文件。
* 组播输入及 HTTP 输出均经过 `lo`，不修改内核网络参数。`net.core.rmem_max` 和 `net.core.wmem_max` 均为 212992，TCP 拥塞控制为 cubic。

| 程序 | 测试版本 |
| --- | --- |
| rtp2httpd | [v3.17.0-beta.1](https://github.com/stackia/rtp2httpd/releases/tag/v3.17.0-beta.1) |
| msd\_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95)，2026-07-20；liblcb `e2f420a2` |
| udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae)，2026-04-13 |
| TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0)，2026-09-06 |

## 测量方法

rtp2httpd 设置 `-C -w 1`，msd\_lite 配置一个事件循环线程，udpxy 保留每个客户端一个子进程的原生模型；这三个程序的全部进程及线程固定在一个 vCPU。TVGate 不设置 `GOMAXPROCS`，不额外限制 CPU 亲和性，使用虚拟机全部 16 个 vCPU 上的默认多核调度。调度方式不同，结果反映各自配置在指定输入负载下的服务 CPU 成本。

CPU 使用率取完整测量窗口内 `/proc/PID/stat` 的用户态与内核态时间增量，除以实际墙钟时间，再对整个服务进程树求和。**100% 表示占满一个 vCPU**，多核程序可以超过 100%。负载发送器、接收器和测量控制进程固定在其他 vCPU，其 CPU 另行记录，不计入服务 CPU；TVGate 的默认调度可使用这些 vCPU。

PSS 和 USS 从 `smaps_rollup` 每秒采样并对进程树求和。PSS 按比例计入共享页，USS 仅统计私有页，均不包含全部内核 socket 内存或未映射的匿名文件页缓存，因此不能作为服务总内存成本。

每个 RTP 数据报携带 7 个 188 字节 MPEG-TS 空包，共 1316 字节负载。接收端解析 HTTP 分块编码后持续读取流。每轮重启服务与负载进程，所有客户端开始收到数据后再预热，测试逐项串行执行，每轮轮换程序顺序，四轮中每个程序在各个执行位置各出现一次。

msd\_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB 环形缓冲；仅适配监听地址、接口、线程数、日志和拥塞控制。udpxy 保留默认缓冲设置。TVGate 仅配置监听端口及 loopback 组播接口，并发、缓冲、连接上限及日志均使用程序默认值。

## 测试场景

| 场景 | 客户端数 | 组播源数 | 单源负载码率 | 重复次数 | 每轮预热 / 采样 |
| --- | ---: | ---: | ---: | ---: | --- |
| 多频道 | 8 | 8 | 40 Mbps | 4 | 5 s / 15 s |
| 同频道 8 客户端 | 8 | 1 | 40 Mbps | 4 | 5 s / 15 s |
| 同频道 64 客户端 | 64 | 1 | 20 Mbps | 4 | 5 s / 15 s |
| 高码率 | 1 | 1 | 400 Mbps | 4 | 5 s / 15 s |

## 测试结果

以下 CPU 和内存统计包含每个场景全部完成采样的轮次，衡量指定负载下的资源占用，不用于认证转发正确性或最大可持续吞吐量。

### CPU 使用率

数值为逐轮平均 CPU 的均值。

| 场景 | rtp2httpd | msd\_lite | udpxy | TVGate |
| --- | ---: | ---: | ---: | ---: |
| 8 频道，各 40 Mbps | 🏆 3.69% | 12.28% | 23.02% | 56.65% |
| 8 客户端同频道，40 Mbps | 🏆 4.87% | 5.58% | 30.92% | 45.09% |
| 64 客户端同频道，20 Mbps | 🏆 4.78% | 5.50% | 59.93% | 115.08% |
| 单客户端，400 Mbps | 🏆 12.88% | 15.46% | 26.45% | 57.86% |

### PSS 内存占用（MiB）

| 场景 | rtp2httpd | msd\_lite | udpxy | TVGate |
| --- | ---: | ---: | ---: | ---: |
| 8 频道，各 40 Mbps | 1.86 | 8.95 | 🏆 0.79 | 25.82 |
| 8 客户端同频道，40 Mbps | 1.13 | 1.35 | 🏆 0.79 | 22.76 |
| 64 客户端同频道，20 Mbps | 🏆 1.30 | 1.37 | 4.55 | 46.26 |
| 单客户端，400 Mbps | 1.26 | 1.34 | 🏆 0.32 | 19.16 |

### USS 内存占用（MiB）

| 场景 | rtp2httpd | msd\_lite | udpxy | TVGate |
| --- | ---: | ---: | ---: | ---: |
| 8 频道，各 40 Mbps | 1.24 | 8.94 | 🏆 0.52 | 25.82 |
| 8 客户端同频道，40 Mbps | 🏆 0.51 | 1.34 | 0.52 | 22.76 |
| 64 客户端同频道，20 Mbps | 🏆 0.68 | 1.35 | 3.96 | 46.26 |
| 单客户端，400 Mbps | 0.63 | 1.33 | 🏆 0.12 | 19.16 |

## 附：rtp2httpd 的性能优化策略

### 按生命周期分配内存

连接只为实际使用的协议分配 RTSP 或 HTTP 代理状态，FEC 分组表在需要保存恢复分组时才分配。HTTP 输入缓冲和已解析请求使用独立的匿名内存映射：输入缓冲在解析和路由完成后释放，普通媒体流的请求数据在响应头生成后释放。HTTP 代理保留其仍在使用的请求头和请求体。临时请求页可直接交还操作系统，避免长期连接将这些已空闲的页留在堆中。

普通共享组播客户端使用源级重排窗口，不再各自分配闲置的重排数组。RTSP、FCC、截图或 FEC 独立处理路径才分配客户端窗口；流中途启用 FEC 时，已有客户端和后续加入的客户端都会取得各自的窗口。

包缓冲池初始分配 128 个缓冲，按 128 个扩展；控制缓冲池初始分配 16 个，按 16 个扩展。worker 定时回收完全空闲的分段，保留必要的基础容量。客户端队列的逻辑预算与初始预分配量分开计算，减少初始内存不改变原有的缓冲余量。

### 每个 worker 共享组播订阅

每个 worker 维护一张共享源表，按照解析后的组播地址、端口、SSM 源地址、有效上游接口及 FEC 端口匹配。频道名称、`/rtp/` 与 `/udp/` 的路径写法、FCC 服务器参数不参与匹配。相同资源只建立一份主组播 socket，以及按需建立的一份 FEC socket。

订阅源拥有独立的生命周期、超时和重加组定时状态。每个客户端持有订阅引用，最后一个引用释放时关闭 socket 并销毁源状态。最早接入的客户端离开时，事件分发会转交给仍存活的订阅者。各 worker 仍独立管理自己的源表。

这主要减少本机重复 socket 接收、系统调用和应用层处理。多个本机 socket 加入同一个组播组，并不等于上游链路一定传输了相同数量的完整数据流。

### 共享解析、重排和批次数据

普通组播由共享源统一进行 RTP 解析及重排，然后将有效负载合并到容量为 64 KiB 的批次中。每个 RTP 负载保持完整，在下一个负载会超出容量时先发送当前批次；1316 字节负载对应每批 49 个包、64484 字节。这样既减少每客户端重复解析，也减少逐包分发和发送调用。未满的批次在满 100 ms 后的下一次 worker 定时检查中发送；检查周期为 100 ms，实际延迟还受调度影响。

Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外，增加按需分配的 64 KiB 批次池。批次池归 worker 所有，已排队的数据可在组播源销毁后继续存活。默认初始分配 4 个批次，按 4 个扩展，最大容量按 `buffer-pool-max-size × 1536` 字节预算换算，并至少容纳 4 个批次。这是批次池自身的上限，与原有包池分别计量。批次池耗尽时，仍可通过小包引用继续分发。

多个客户端共享底层 payload，各自拥有独立的 `buffer_ref_t` 视图。视图通过 `owner` 指向同一份不可修改的数据，并保留自己的链表指针、发送偏移和剩余长度。客户端的部分发送只修改自己的视图；最后一个视图释放后，底层内存才返回池中。每个客户端仍有自己的发送队列和容量限制，慢客户端不会暂停其他订阅者的接收。只有一个订阅者时，直接使用批次自身的描述符，省去额外视图分配。

队列限额改为按底层缓冲容量计费，不能再使用「缓冲数量 × 1536」。一个只剩少量字节未发送的批次，仍占用完整 64 KiB 容量，直到该客户端释放引用。这避免共享大缓冲绕过原有慢客户端内存限制。

### 减少收发路径的固定开销

支持 `recvmmsg` 的平台一次接收最多 16 个数据报，worker 复用接收描述符和未消耗的包缓冲。处理完且没有其他引用的包缓冲直接用于下一次接收；重排、FEC 或发送队列仍持有引用时，使用另一个缓冲，避免覆盖待处理数据。数据直接进入缓冲池，不增加一次接收后的复制；不支持批量接收时使用逐包接收。对于已完成起始重排、序号连续且窗口为空的 RTP 流，未启用 FEC 时直接交付负载，省去重排槽位的插入和移除。

主组播 socket 首次就绪时立即读取；后续按实际接收缓冲容量和每次收到的数据报数量，合并最多 1–2 ms 内的接收工作，减少持续小包带来的事件唤醒。系统报告的接收缓冲至少为 128 KiB 时才允许等待 1 ms，至少为 256 KiB 且包量较小时才允许 2 ms；实际间隔还受系统调度影响。延后读取期间暂停该 socket 的就绪通知，读空后重新启用。worker 只遍历仍有待处理接收任务的源，最后一个订阅者离开时同时移除其待处理任务。

接收缓冲较小或无法查询容量时使用水平触发通知。单次读取达到按缓冲容量计算的保守包量阈值时，也恢复即时读取，并按至少 100 ms 的窗口重新估计包速率；速率降低且留有两倍调度余量时再尝试合并，避免一次突发永久关闭优化。水平触发下读到不足一批即可返回，剩余数据仍会通知；延后读取路径则必须确认读空，避免中断造成的短读留下未处理数据。每次回调最多接收 256 个主组播数据报，使持续到来的数据不会长期占用事件循环。这些策略自动选择，无需额外配置。

发送任务先进入 worker 内部队列，只有 socket 暂时无法继续写入时才订阅内核可写事件，减少每个批次的事件监听切换。每个连接单次最多发送 256 KiB，每轮最多处理 128 个发送任务，剩余任务继续排队，使接收、定时器和其他客户端都能得到处理。

状态统计的客户端归属校验使用进程内缓存的 PID，并在每次 fork 后刷新，省去队列更新和发送统计中重复的 `getpid()` 调用。

队列限额、计数和高水位仍在每次队列操作时更新，向共享状态区发布则集中到 worker 的 100 ms 定时检查中。这样可减少每批数据入队、发送时重复执行的共享内存写入和同步操作；状态页显示最近一次发布的队列快照。

### 不可修改的批次快照

支持内存文件封存的平台会通过 `memfd_create` 为共享批次建立匿名内存文件，写入一次后封存，再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径；普通内存发送仍通过 `sendmsg` 完成。

每批创建新的文件，发布后禁止写入、增长和截断，不会在缓冲池复用时覆盖旧文件。即使 `sendfile` 已返回、应用已关闭最后一个文件引用，TCP 仍可能持有旧数据页；文件不可变保证这些待发送内容不会被新一批数据改写。创建、写入或封存失败时保留内存发送；客户端的 `sendfile` 不受支持时，只将该客户端的视图回退到内存发送。

### FCC、FEC 与客户端隔离

FCC 单播和切换状态按客户端独立维护。衔接时先共享组播 socket；等单播和待处理数据完成、重排序号与共享源对齐，再加入共享批次分发。切换前先发送旧批次，避免新订阅者重放之前的内容。

截图处理仍保留独立状态。配置 FEC 端口的源使用共享 socket、每客户端独立的重排及 FEC 恢复路径；运行中首次发现带内 FEC 时，会先发送现有批次，并把共享重排窗口交接给各客户端，再转入独立处理。因而本报告普通组播的 CPU 结果不能直接代表 FCC 补流阶段、FEC 恢复或截图负载。

## 适用范围

这是 ARM64 Linux 虚拟机内的固定码率转发资源测量，不是物理网卡吞吐上限、视频解码性能或最大可承载客户端数量测试。loopback 内核工作中记在发送器及读流进程上的部分不属于服务 CPU，主机调度也会引入波动。其他硬件、码率、客户端速度、并发频道数量及网络路径应单独测量。

## 复现测试

脚本及参数见 [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md)。准备对应版本的二进制文件后，运行四种场景：

```bash
scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \
  --cases distinct8 shared8 shared64 high400 \
  --repetitions 4 --warmup 5 --duration 15
```

通过 `--binary 名称=路径` 与 `--revision 名称=版本` 指定实际使用的二进制及版本，按上表设置各场景的重复次数和采样时长。CPU、PSS、USS 汇总位于输出目录中的 `resources.json`。输出目录默认位于 `build/benchmark/`，测试记录仅在本地保留。
