简体中文
性能测试报告
比较 rtp2httpd、msd_lite、udpxy 和 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 |
| msd_lite | fa68e131,2026-07-20;liblcb e2f420a2 |
| udpxy | 31d4bcfa,2026-04-13 |
| TVGate | 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。准备对应版本的二进制文件后,运行四种场景:
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/,测试记录仅在本地保留。