资讯详情 coturn UDP 中继数据路径性能迭代实录:11 个优化提交、recvmmsg/GSO 快速路径与严谨的 A/B 测量方法
📅 2026/11/12 18:52:37
coturn UDP 中继数据路径性能迭代实录11 个优化提交、recvmmsg/GSO 快速路径与严谨的 A/B 测量方法【免费下载链接】coturncoturn TURN server project项目地址: https://gitcode.com/GitHub_Trending/co/coturn本文基于 coturn 仓库中的性能迭代日志 docs/PerformanceIterationLog.md完整还原针对 UDP 中继数据路径UDP relay data path的多轮性能优化工作5 个微优化提交带来 5.8% 的累计吞吐提升后续 6 个recvmmsg批量接收提交、sendmmsg批量发送的阴性结论以及最终通过 LinuxUDP_SEGMENTUDP-GSO将服务端转发速率提升 91%、CPU 效率提升约 2.8 倍的完整过程。读完后你将掌握如何为长连接 UDP 服务定位内核态热点、如何设计交替式 A/B 压测避免环境漂移带来的假结论以及如何用--udp-recvmmsg、--multiplex-peer、--udp-gso等 Linux 专属快速路径配置一台生产级 turnserver。一、文档定位一份可续接的性能工作日志PerformanceIterationLog.md 在仓库中的定位非常明确压测工具链harness、基线命令与 droplet 拓扑的完整说明见 CLAUDE.md 的 Load Test on DigitalOcean 一节而本日志只记录增量——测到了什么、哪些优化合入landed、哪些没合入、下一轮该往哪走。这种把过程资产化的写法使任何接手者无需重新推导整个测量体系是性能调优工作值得借鉴的组织方式。二、压测环境两台 c-4 droplet 的最小可行拓扑原始迭代使用的测试环境为 DigitalOceannyc1区域的两台 Ubuntu 24.04c-44 vCPU / 8 GBdroplet位于同一 VPCdefault-nyc1角色公网 IP内网 IPcoturn-turnserver157.230.3.10210.116.0.2coturn-loadgen167.99.153.21610.116.0.3早期第 5 轮累计测试还使用过另一对 droplet公网68.183.121.197/68.183.132.220内网同为10.116.0.2/3。droplet 通过 DigitalOcean v2 API 创建环境未装doctl用curl$DIGITALOCEAN_TOKENSSH 走~/.ssh/id_rsa。turnserver 侧 droplet 上刻意保留了两份并存的构建产物作为每轮 A/B 的固定参照物/root/coturn_clean.tar— master 起点的git archive HEAD应用任何新补丁前先重新解压/root/coturn_baseline/build/bin/turnserver— 干净的基线二进制作为每轮 A/B 中的 B绝不覆盖/root/coturn/build/bin/turnserver— 当前迭代二进制。loadgen 侧则是/root/coturn/build/bin/turnutils_uclient与作为守护进程跑在10.116.0.3:3480的turnutils_peerpid 记录在/root/peer.pid。标准 packet-flood 压测命令与 CLAUDE.md 基线一致默认不带--udp-recvmmsg开启批量收/发路径时该 flag 加在turnserver上而非客户端timeout -s INT 30s /root/coturn/build/bin/turnutils_uclient \ -Y packet -m 1 -l 120 \ -e 10.116.0.3 -r 3480 -X -g \ -u user -W secret \ 10.116.0.2指标选择取最后一行start_mclient:日志中的tot_recv_msgs字段。这代表测试窗口内真正往返穿过中继的包数send_pps只是 loadgen 单侧的发送速率即使中继丢包大半它也能到 262 K因此不能作为中继吞吐的代理指标——这一条是后面所有 A/B 结论可信的前提。三、累计优化清单11 个提交两次质变点前 5 个提交微优化累积出 5.8%分支claude/beautiful-black-c3b741上从727ec2abloadgen到321a2d18之间的 5 个提交#Commit优化内容1ce7e7e53将turn_server_get_engine()移出每包热路径hoist out of per-packet hot path28e28491aioa_socket_check_bandwidth增加早期快速退出删除send_data_from_ioa_socket_nbh中已恒假的if (!(s-done \|\| s-fd-1))3344360f6在write_to_peerchannel、handle_turn_send、read_client_connection中缓存get_relay_socket_ss()与ioa_network_buffer_get_size()的结果4a6f6767f通过 ns_turn_ioaddr.h 内联get_ioa_addr_len()5321a2d18通过 ns_turn_ioaddr.h 内联addr_cpy()同一对 droplet 上交替 A/Bm1 packet flood每轮 30 s切换二进制间留 4 s 预热基线干净 master 二进制均值146,984round-trips / 30 s累计5 个迭代全部合入均值155,468round-trips / 30 s吞吐 5.8%单个迭代的 delta 都在 5–10% 的运行噪声带内累计效应才是可见的——这是后文方法论教训的核心伏笔。后续 6 个提交relay 侧 recvmmsg 批量接收#Commit优化内容654c589d0/4b1a8d71UDP listener 与已连接中继 socket 的 Linuxrecvmmsg批量接收初版78d9a7292listener 与 relay UDP 路径共享同一个--udp-recvmmsgflag移除独立 relay flagancillary-data 解析复用 dtls_listener 中的共享解析器8d48686b7将 relay 每 socket 的 recvmmsg 状态从 16 × 64 KiB cmsg 缓冲缩减为 TTL/TOS 尺寸缓冲避免一次多余的 would-block 回退recvmsg部分批次后清理全部预分配缓冲9ad81705e增加每 engine 的 recvmmsg 占用计数与 10 s 日志摘要calls、packets、avg_batch、wouldblock、unavailable、no_buffer、批次大小直方图10388b15d4将已连接 relay UDP 的 recvmmsg scratch 从 per-socket 状态迁移到 per-engine/per-thread 状态114c4fd67e占用摘要改为--udp-recvmmsg-log显式开启使--udp-recvmmsg可以在不产生周期性统计日志的情况下交付#7–#11 的本地验证cmake -S . -B build -DBUILD_TESTINGON通过cmake --build build --parallel 8通过ctest --test-dir build --output-on-failure3/3 通过build/bin/turnserver --udp-recvmmsg --udp-recvmmsg-log --version正确解析两个 flag 并打印4.11.0Linux Dockerturnserver构建在 #7、#8、#10 之后均通过。交付取舍的经验shipping cleanup learning占用计数器保留——开销低且对 DigitalOcean 诊断有用周期性摘要默认关闭——--udp-recvmmsg-log只在日志流本身就是观测对象的测量轮次中启用。这一点在当前源码中可以直接印证mainrelay.c的默认值表中udp_recvmmsg为true、udp_recvmmsg_log为false见 mainrelay.c统计输出格式为udp-recvmmsg stats: calls... packets... avg_batch... wouldblock... unavailable... no_buffer... hist_1... hist_2... hist_3_4... hist_5_8... hist_9_16...ns_ioalib_engine_impl.c同一套计数还通过turn_udp_recvmmsg_calls暴露给 Prometheusprom_server.c并在 telnet 管理接口中回显turn_admin_server.c。两轮 droplet 验证与占用率反直觉结论第一轮验证2026-05-09构建自d48686b7同二进制--udp-recvmmsg开/关-Y packet -m 1 -l 120各 5 轮交替 30 s关均值 154,527中位 154,596标准差 3,467开均值 149,994中位 153,011标准差 7,174 → 均值-2.9%、中位-1.0%-Y packet -m 100 -l 120各 5 轮客户端在 30 s 超时前跑完且落入两个发送量级桶只能当作粗粒度多连接信号——关均值 59,432 / 开均值 59,6400.3%。追加m100 -n 1000各 3 轮该日志格式缺tot_recv_msgs用tot_recv_bytes / 120推导接收数关均值 8,540 / 开均值 8,857均值 3.7%、中位 -2.7%。当时结论修正后的 relayrecvmmsg可构建、对多连接安全但没看到明确吞吐收益flag 保持 opt-in下一步是给已连接中继 socket 打占用率仪表如果大多数 readiness 事件只返回 1 个 datagramrecvmmsg只会增加准备开销而不减少系统调用。第二轮验证2026-05-09构建自388b15d4即 #10 的 per-thread scratch 改动后直接推翻了上述假设m1关均值 153,133 / 开均值 148,452均值 -3.1%、中位 -2.5%m1开启时的占用率1,129,427 次recvmmsg调用返回 17,660,300 个包平均批次 15.64。直方图hist_11,353、hist_21,496、hist_3_43,707、hist_5_814,817、hist_9_161,108,057——98.1% 的调用落在 9..16 桶m100关均值 55,443 / 开均值 60,596均值 9.3%、中位 29.1%客户端再次落入两个发送量桶delta 视为噪声较大m100全 relay 线程合计1,426,401 次调用返回 16,188,946 包平均批次 11.359..16 桶占 66.3%。真正的学习点接收侧占用率很高recvmmsg 大多只返回 1 个包的假设在这个 harness 下是错的。剩余瓶颈在接收之后每包回调、TURN 处理、尤其每个转发包一次sendto。per-thread scratch 改动因为数千 socket 下的内存/cache 行为仍值得保留但下一个杠杆在发送侧。四、热路径地图为什么微优化单步不可见第 5 轮结束时用perf record -F 99 -g对压测中12 s、-Y packet -m 1的 turnserver 采样按用户态 self-time 排序0.80 % send_data_from_ioa_socket_nbh 0.76 % socket_input_worker 0.69 % read_client_connection.isra.0 0.60 % turn_report_session_usage 0.53 % peer_input_handler 0.51 % udp_server_input_handler 0.35 % udp_recvfrom # iter 1 时为 0.76 % 0.34 % lm_map_get 0.27 % stun_is_channel_message_str 0.27 % get_relay_socket 0.26 % ioa_socket_check_bandwidth # iter 1 时为 0.33 % 0.26 % udp_send # iter 1 时为 0.60 % 0.18 % ioa_network_buffer_get_size用户态 coturn 代码合计只占 relay 线程5–7%的周期relay 线程钉在单核上跑满 ~100%m1 单流测试只有一个五元组SO_REUSEPORT下哈希到一个 SO_REUSEPORT worker另外 3 个 relay 线程闲着。真正的成本在内核侧children 聚合36 % udp_sendmsg (sendto 路径) 14 % udp_recvmsg 17 % ip_finish_output / ip_output / __dev_queue_xmit ~23 % 系统调用进/出机制 (sysret, SYSRETQ, SYSCALL_64*)~23% 的系统调用开销是下一个大杠杆——靠批量减半它约值 ~10% 的墙钟 CPU。这也解释了微优化迭代单次落在噪声带内、累计才可见的现象用户态每砍一半墙钟最多改善 ~2.5%单轮测不出来。五、源码纵深recvmmsg 快速路径到底改了哪些东西日志中的提交最终落在 ns_ioalib_engine_impl.c 等文件中当前代码可以完整对应资格判定一次性固化。ioa_socket结构体上有udp_recvmmsg_eligible布尔ns_ioalib_impl.h在 socket 创建时设置客户端 listenerns_ioalib_engine_impl.c与--multiplex-peer模式下的每线程共享 relay socket#L1497、#L1500、#L1991置位per-session 中继 socket 永不置位。读路径每唤醒只做一次布尔与位与ns_ioalib_engine_impl.c/* udp_recvmmsg_eligible is set once at socket creation (shared fan-in * sockets only); no per-wakeup recomputation here. */ if (turn_params.udp_recvmmsg s-udp_recvmmsg_eligible !s-ssl s-read_cb) { int batch_len -1; if (socket_udp_read_batch_recvmmsg(s, batch_len)) { ... return batch_len; } }正是日志 What didnt work 一节所述的解决方案早期版本把 16 缓冲批量路径套到每个 per-session 已连接 relay socket每 socket 只有一个流预分配开销吃掉了 listener 侧收益后续把范围收敛到真正的 fan-in 点后--udp-recvmmsg在 Linux 上默认开启运维可用--udp-recvmmsgfalse或0退出。DTLS 会话 socket 始终走 SSL 读路径、永不批量。批量读取本体socket_udp_read_batch_recvmmsgns_ioalib_engine_impl.c批次上限为IOA_UDP_RECVMMSG_MAX_BATCH 16ns_ioalib_impl.h为每个槽位从 engine 缓冲池取一个stun_buffer_list_elemUDP_STUN_BUFFER_SIZE用MSG_DONTWAIT一次recvmmsg()最多收 16 个 datagram返回 0 记 wouldblockENOSYS/EINVAL/EOPNOTSUPP时打 WARNING 并sticky 关闭该快速路径turn_params.udp_recvmmsg false避免在不支持的内核上热循环。发送侧合并包裹。批量回调循环被udp_sendmmsg_batch_begin()/udp_sendmmsg_batch_end()包住ns_ioalib_engine_impl.c注释写得很直白没有这个包裹relay 侧 recvmmsg 路径会为每个交付 datagram 发一次 send 系统调用。批量状态是thread-local的udp_sendmmsg_batch_state对应 #10 的 per-thread scratch 改动上限MAX_SENDMMSG_BATCH 32、最小批次MIN_SENDMMSG_BATCH 4ns_ioalib_engine_impl.c不足 4 的尾巴退化为逐包udp_send其余走sendmmsg()ns_ioalib_engine_impl.c。mmsghdr数组在 enqueue 时就填好且连续排布flush 时直接交给内核无需重建。TTL/TOS cmsg 复用。#8 所述TTL/TOS 尺寸缓冲体现在 cmsg 区大小按实际需要的IPV4_TTL/IPV4_TOSIPv6 对应RECVHOPLIMIT/RECVTCLASScmsg 计算ns_ioalib_engine_impl.c而不是早期每包 64 KiB 的整块缓冲dtls_listener.c中的共享 ancillary-data 解析器#7 提到与MAX_SINGLE_UDP_BATCH IOA_UDP_RECVMMSG_MAX_BATCHdtls_listener.c复用同一批次上限常量。六、阴性结论 Isendmmsg 为何没有赢2026-05-03 的 follow-up 在sfo3的两台c-4dropletturnserver10.124.0.2、loadgen10.124.0.3上测试了 Linux-only 的--udp-sendmmsg与--udp-recvmmsg联用Run代码/flag生成器峰值 pps生成器均值 pps服务器 RX 均值 pps服务器 TX 均值 pps服务器 TX 峰值 ppsCPU 均值结论iter0baseline --udp-recvmmsg335,872286,721360,900257,357323,48897.8%sendto/udp_sendmsg主导iter1--udp-sendmmsg双向409,600312,662428,184197,300260,45399.8%走 sendmmsg 路径TX 回退iter2仅批次 ≥4 才 sendmmsg393,216315,393398,121163,626215,06898.9%阈值没能挽回 TXiter3仅 listener 侧批量425,984286,038376,444210,050332,41797.4%入口/TX 峰值改善均值 TX 仍低于基线perf 对比揭示了原因sendmmsg()确实减少了系统调用进入次数但每个 datagram 仍然完整走一遍udp_sendmsg与 IP 输出路径baselineudp_send - sendto - __sys_sendto - udp_sendmsg - udp_send_skb - ip_outputsendmmsg 变体udp_sendmmsg_flush - __sendmmsg - __sys_sendmmsg - ___sys_sendmsg - udp_sendmsg - ip_output额外mmsghdr拷贝/循环开销足以抵消系统调用节省。结论sendmmsg在此负载下不是已证明的普适收益保持 opt-in当前源码中udp_sendmmsg默认值即由此派生随--multiplex-peer开启见 mainrelay.c。七、阳性结论UDP-GSO--udp-gso91% 转发速率这是 2026-05-09 落地的 GSO 发送路径对应第四节热路径地图中内核 TX 是主成本的判断。原理recvmmsg/sendmmsg的 follow-up 都证实本负载的主导成本是每 datagram 的内核 TX 路径udp_sendmsg → ip_finish_output → __dev_queue_xmit → start_xmitmmsg 式批量压不掉它而 UDP-GSOLinuxUDP_SEGMENTcmsg能——N 个同目的、同尺寸 datagram 以一次sendmsg iovec 提交内核分配一个 super-skb 只遍历一次协议栈在出口NIC处切分。实现位于 ns_ioalib_engine_impl.c复用现有--udp-sendmmsg批量状态。每次udp_sendmmsg_enqueue都追踪 GSO 资格同 fd、同目的、同尺寸、每 datagram ≤ 1472 B即MAX_UDP_GSO_DGRAM_SIZE批次下限MIN_UDP_GSO_BATCH 2见 ns_ioalib_engine_impl.c资格满足时先经udp_gso_attempt_flush()ns_ioalib_engine_impl.c尝试 flush构造SOL_UDP/UDP_SEGMENTcmsg 携带分段长度后一次sendmsgEINVAL/ENOPROTOOPT/EOPNOTSUPP时sticky 关闭WARNING 一次进程内不再重试内核重启可恢复保证在无 GSO 支持的内核/NIC 上优雅回退relay 侧socket_udp_read_batch_recvmmsg的回调循环被udp_sendmmsg_batch_begin/end包裹见第五节第 3 点使 recvmmsg 批次内被触发的 peer→client 发送也能合并。验证2026-05-09nyc1 新建c-4dropletturn10.116.0.4、load10.116.0.5全部变体由同一源码树在/root/coturn/build构建-Y packet -m 1 -l 120sar -n DEV监测eth1配合mpstat/pidstat先 12 s sweep 定序再 30 s 交替 A/Bbaseline → gso → baseline → gso确认量级变体eth1 RX ppseth1 TX ppssys CPUidle CPUbaseline_r1322,091127,44522.9%67.5%--udp-recvmmsg --udp-sendmmsg --udp-gsogso_r1266,068257,99615.0%78.7%baseline_r2309,475125,57320.9%70.7%gso_r2275,992225,36614.9%74.3%均值服务端转发速率eth1 TXbaseline 126,509 pps → GSO 241,681 pps91%1.91×sys CPU 从 21.9% 降到 14.9%——相当于 TX pps 每 sys-CPU-% 的CPU 效率约 2.8×。12 s 包长 sweepuclient 上报的 send_pps 均值只用于定序——绝对吞吐以 eth1 TX 为准变体m1m2m4m8m16m32baseline230,401150,189187,055174,771160,871167,789--udp-recvmmsg255,660148,824174,767142,997150,743144,200--udp-recvmmsg --udp-sendmmsg231,766146,776148,826136,542148,955143,575--udp-recvmmsg --udp-sendmmsg --udp-gso136,876147,458124,250131,081137,636114,714注意 m1 列的 GSO 数字偏低是假象uclient 报的是生成器自身发送速率GSO 下 loadgen 侧单线程的turnutils_peer变成新瓶颈、无法回射 240 k pps所以生成器侧速率被拉低30 s 的 eth1 抓取才是权威的服务端指标。m2..32区间保留该 sweep 只为了说明 GSO 相对recvmmsgsendmmsg没有回退。perf children 占比m1、12 sturnserver 进程符号baselinerecvmmsgrecvsendmmsggso__x64_sys_sendtochildren43.6 %47.6 %22.8 %0.0 %__x64_sys_sendmsgchildren———38.1 %__x64_sys_sendmmsgchildren——27.0 %0.0 %udp_sendmsg38.8 %41.9 %20.6 %35.9 %__dev_queue_xmit18.5 %——29.3 %skb_segment出口 GSO 切分无无无2.2 %syscall_return_via_sysretself7.2 %4.7 %4.4 %2.4 %entry_SYSCALL_64_after_hwframeself4.1 %3.6 %2.6 %1.8 %GSO 列里每包内核栈成本被摊销到单个 super-skb 的分段上__dev_queue_xmit占比上升具有迷惑性——分母CPU 占用变小了单包绝对成本实际在下降。运维注意事项flag 为 opt-in。--udp-gso依赖--udp-sendmmsg的批量状态无该 flag 则状态从不累积GSO 无东西可 flush--help文本中写明了依赖。结合 CLAUDE.md 的 flag 清单--udp-sendmmsg当前由--multiplex-peer派生开启因此单独传--udp-gso是静默 no-opGSO 资格在每次_begin/_end时重置混合目的/混合尺寸负载透明回退到既有sendmmsg与udp_send路径sticky disable 保证不支持 GSO 的主机不会在失败路径上热循环只记一次 WARNING实测环境 Linux 6.8 virtio-netDOc-4gso_max_segs65535内核 4.18 完全缺少UDP_SEGMENT由 sticky-disable 路径覆盖。CLAUDE.md 中 2026-05-16 的三 droplet 8 流矩阵进一步印证了这套快速路径组合的整体效应45 s、8 路-m 1并发、c-4配置recv pps8 流合计turnserver CPU服务器 host idlesoftirqbaseline91 k367 %24 %14.1 %--udp-recvmmsg99 k229 %−38%48 %2.6 %--multiplex-peer97 k293 %−20%42 %13.3 %--multiplex-peer --udp-gso96 k134 %−63%65 %0.8 %recv-pps 上限~95-100 k是c-4上 loadgen 单侧瓶颈uclient host 的%sys %softirq各核合计 ~70%内核网络栈已饱和——收益体现在服务端 CPU 节省而非更多 pps要把服务器推向饱和需先把 uclient 升到c-8或更大。八、没走通的路What didnt work1.--udp-recvmmsg默认全量开启第 1–11 轮时期——当时失败后来收敛范围后成功。原始发现flag 把 16 缓冲批量路径套到每个 per-session 已连接 relay socket而这些 socket 永远只收一条流m1与m100的多轮 A/B 反复确认吞吐持平或微负——per-session 预分配churn 吃掉了 listener 侧收益故保持 opt-in。 解决后续提交把recvmmsg限定到共享 fan-in socketudp_recvmmsg_eligible见第五节listener 在客户端并发不低时是真正的收益点、空闲时成本极低per-session 税消除后flag 在 Linux 上默认开启--udp-recvmmsgfalse为退出方式。2. 缓存get_relay_socket_ss第 3 轮——无可测的墙钟收益。该函数本就是static inline底层get_relay_socket()是 4 行访问器。缓存确实省了每包一次跨编译单元的函数调用编译器无法证明中间的set_df_on_ioa_socket/ioa_network_buffer_*调用之间get_relay_socket是纯函数perf 能看到小的重分布但吞吐停在噪声带内。保留理由清理本身站得住脚且与第 4/5 轮的内联方向一致。九、方法论教训比优化本身更值钱每轮交替 A/B不要先跑 5×B 再跑 5×I。droplet 环境在几分钟内就有明显漂移hypervisor 上的其他租户、NIC 环形缓冲背压等顺序块会偏向落在较差半程的二进制。turnserver 重启后丢弃第一次运行。loadgen 在服务器重启后的首跑一致性地比稳态慢 30–80%——看起来是客户端侧 channel/permission 状态在预热与服务器无关。测量前加一次 4 s 丢弃跑 即可。运行间方差 ~5–10%即使交替。声称 10% 的收益前计划 6–8 轮约 8 分钟墙钟。单靠 3 轮 A/B 会骗人。用tot_recv_msgs不要用send_pps。loadgen 发送速率无论中继容量如何都饱和在 ~262 K pps——那只是 loadgen 内核 UDP 发送缓冲能吞下多少接收数才是真正往返穿过中继的量。中继是内核绑定的。用户态 coturn 只占 ~5% 周期砍半至多 ~2.5% 墙钟——单轮基本不可测只有累计可见。别指望一次 CSE 带来 10% 跳跃。单流测试钉死单核。SO_REUSEPORT下内核按五元组哈希到 worker socket一个客户端 → 一个元组 → 一个 worker 线程另外 3 核闲着。要压满 4 个 relay 线程需要 m≥4 且源端口互不相同——而该 loadgen 复用端口无法干净铺开。不要在迭代之间重新解压/root/coturn以免破坏git apply式补丁droplet 上的拷贝不是 git checkout是git archivetar用patch -p1每轮上传累计diff当前分支 vsmaster先解压/root/coturn_clean.tar保证干净应用。十、已排查并排除项不要再做set_socket_ttl/set_socket_tos已按s-current_ttl ! ttl/s-current_tos ! tos在不变时短路稳态 flood 中每包调用直接返回、不发setsockopt。已优化完毕。set_df_on_ioa_socket有同样的守卫见 ns_ioalib_engine_impl.c。turn_report_session_usage慢路径每 4096 包才跑一次第 1 轮提交每次调用开销现在约 3 次读 1 次位掩码测试 1 次条件返回。sendto加MSG_CONFIRM可跳过 ARP 刷新但neigh_resolve_outputneigh_hh_output合计 ~17% 只因为包量太大——单包看是正常缓存邻居路径不是刷新。把socket_input_worker的MAX_TRIES从 16 提到 64 不改变系统调用次数只是推迟返回 libevent没有第 1 项发送批量配合就是无用功。十一、后续优化清单按 m1 指标预期收益排序发送侧批量sendmmsg或让接收批次传递更深。占用计数已证明接收批量在工作m1 均 15.6 包/次、m100 均 11.4。代码随后立即对每个 datagram 调用既有每包回调每个转发包仍付一次 send 系统调用。下一个可测杠杆在接收批次期间按线程排队出站 datagram 并用sendmmsg刷出或为热 UDP 中继路径引入批次感知回调。后续已被 UDP-GSO 方案承接见第七节。开发发送批量时保留recvmmsg占用计数。足够便宜用于定向性能构建且能一眼看出基准在压一个 relay 线程还是全部线程。周期性日志在大范围交付前考虑藏进 verbose/debug 选项。已按此执行--udp-recvmmsg-log。发送路径 GSOUDP_SEGMENT。Linux 可把一个大 datagram 在内核里切分服务同一目的地的背靠背发送channel-data flood 正是同目的地。设UDP_SEGMENT后用一次 N×packet_size 的sendmsg可大幅削减 skb 分配 /__dev_queue_xmit开销。需小心处理短尾与不均匀尺寸与 (2) 互补。已落地见第七节。继续内联跨编译单元的每包访问器。第 4/5 轮模式继续适用addr_eq每个 channel-data 包做 permission 查找时调用、ioa_network_buffer_get_size、get_ioa_socket_type/_app_type。各自足够小唯一谨慎点在于它们声明于ns_turn_ioalib.h——属半公开服务器库 API把函数体改为内联不破 ABI 但需重编所有消费者。预计每项 1%但便宜。计量后重估--udp-recvmmsg默认值。已完成。把recvmmsg收敛到共享 fan-in socket 消除了阻止默认开启的 per-session-relay 税listener 是真正的 fan-in 点客户端并发不低时受益、空闲时成本低。现已在 Linux 上默认开启--udp-recvmmsgfalse为退出项。十二、如何续接这项工作确认 droplet 仍在运行第二节所列 IP。若已销毁用c-4/nyc1/default-nyc1VPC 与pavelSSH keyid 23704483重建。若基线二进制丢失从git archive master重新上传/tmp/coturn_clean.tar并重建/root/coturn_baseline/build/bin/turnserver。A/B harness 依赖两个二进制在 turnserver droplet 上并排存在。跑一轮 6 轮交替 A/B 作健康检查当前分支尖端仍应比master快 ~5%。若不是环境已漂移需要重新锚定基线。从清单第十一节挑下一项。当时下一个实质性收益在socket_input_worker中引入recvmmsg后续的 sendmmsg/GSO 章节即是该项的延续与结论。十三、GSO 之后的下一步杠杆与遗留重构日志末尾2026-05-09 GSO 验证后给出的建议下一步把 loadgen 从turnutils_peer迁走。GSO 下 240 k → 90 k tot_recv_msgs/30 s 的差距由单线程 peer 回射主导而非 TURN 服务器多线程 peer 或pktgen式回射器才能测出真实上限。CLAUDE.md 中 peer 现已支持recvmmsg批量接收 GSO 回射见 src/apps/peer/udpserver.c并给出 8 进程 peer 机群与三 droplet 拓扑的升级方案。per-peer 已连接 relay socket。同目的地是 GSO 资格谓词已连接 relay socket 永远同目的地还能省掉每次发送的route_lookup。GSO sendmsg 上加MSG_ZEROCOPY。rep_movs_alternative在 GSO 下仍占 3% selfzerocopy 可免用户态→内核拷贝对 32 B STUN 包收益可能不大负载变大时再看。sendmmsg 运行期间被推迟的更大重构同样未做供后来者参考per-peer 已连接 UDP relay socket 或目的缓存改变 relay socket 语义与接收过滤把一个热 allocation/流分片到多个 relay worker需要严格的顺序、会话记账、socket 所有权与锁竞争设计io_uring发送批量或 kernel-bypass 式发送只作为更大架构实验以及一个在受控输入速率下测量已交付 relay pps 的专用基准模式——当前饱和 packet flood 适合找热函数但会掩盖端到端交付变化。原始产物perf.data、sar/mpstat/pidstat、sweep 日志、AB 日志保存在工作树的perf-results-20260508-213056/目录随日志一起归档。【免费下载链接】coturncoturn TURN server project项目地址: https://gitcode.com/GitHub_Trending/co/coturn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考