coturn UDP 中继数据路径性能迭代实录:11 个优化提交、recvmmsg/GSO 快速路径与严谨的 A/B 测量方法
2026/9/14 20:42:20 网站建设 项目流程

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_SEGMENT(UDP-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-4(4 vCPU / 8 GB)droplet,位于同一 VPCdefault-nyc1

角色公网 IP内网 IP
coturn-turnserver157.230.3.10210.116.0.2
coturn-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_TOKEN),SSH 走~/.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:3480turnutils_peer(pid 记录在/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上从727ec2ab("loadgen")到321a2d18之间的 5 个提交:

#Commit优化内容
1ce7e7e53turn_server_get_engine()移出每包热路径(hoist out of per-packet hot path)
28e28491aioa_socket_check_bandwidth增加早期快速退出;删除send_data_from_ioa_socket_nbh中已恒假的if (!(s->done \|\| s->fd==-1))
3344360f6write_to_peerchannelhandle_turn_sendread_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/B(m=1 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 flag;ancillary-data 解析复用 dtls_listener 中的共享解析器
8d48686b7将 relay 每 socket 的 recvmmsg 状态从 16 × 64 KiB cmsg 缓冲缩减为 TTL/TOS 尺寸缓冲;避免一次多余的 would-block 回退recvmsg;部分批次后清理全部预分配缓冲
9ad81705e增加每 engine 的 recvmmsg 占用计数与 10 s 日志摘要(callspacketsavg_batchwouldblockunavailableno_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_TESTING=ON通过;
  • 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.0
  • Linux Dockerturnserver构建在 #7、#8、#10 之后均通过。

交付取舍的经验(shipping cleanup learning):占用计数器保留——开销低且对 DigitalOcean 诊断有用;周期性摘要默认关闭——--udp-recvmmsg-log只在日志流本身就是观测对象的测量轮次中启用。这一点在当前源码中可以直接印证:mainrelay.c的默认值表中udp_recvmmsgtrueudp_recvmmsg_logfalse(见 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暴露给 Prometheus(prom_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,640,+0.3%
  • 追加m=100 -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 个 datagram,recvmmsg只会增加准备开销而不减少系统调用"。

第二轮验证(2026-05-09,构建自388b15d4,即 #10 的 per-thread scratch 改动后)直接推翻了上述假设:

  • m=1:关均值 153,133 / 开均值 148,452,均值 -3.1%、中位 -2.5%;
  • m=1开启时的占用率:1,129,427 次recvmmsg调用返回 17,660,300 个包,平均批次 15.64。直方图hist_1=1,353hist_2=1,496hist_3_4=3,707hist_5_8=14,817hist_9_16=1,108,057——98.1% 的调用落在 9..16 桶
  • m=100:关均值 55,443 / 开均值 60,596,均值 +9.3%、中位 +29.1%(客户端再次落入两个发送量桶,delta 视为噪声较大);
  • m=100全 relay 线程合计:1,426,401 次调用返回 16,188,946 包,平均批次 11.35,9..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%(m=1 单流测试只有一个五元组,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 等文件中,当前代码可以完整对应:

  1. 资格判定一次性固化ioa_socket结构体上有udp_recvmmsg_eligible布尔(ns_ioalib_impl.h),在 socket 创建时设置:客户端 listener(ns_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 didn't work" 一节所述的解决方案:早期版本把 16 缓冲批量路径套到每个 per-session 已连接 relay socket(每 socket 只有一个流),预分配开销吃掉了 listener 侧收益;后续把范围收敛到真正的 fan-in 点后,--udp-recvmmsg在 Linux 上默认开启,运维可用--udp-recvmmsg=false(或=0)退出。DTLS 会话 socket 始终走 SSL 读路径、永不批量。

  2. 批量读取本体socket_udp_read_batch_recvmmsg(ns_ioalib_engine_impl.c):批次上限为IOA_UDP_RECVMMSG_MAX_BATCH = 16(ns_ioalib_impl.h);为每个槽位从 engine 缓冲池取一个stun_buffer_list_elemUDP_STUN_BUFFER_SIZE),用MSG_DONTWAIT一次recvmmsg()最多收 16 个 datagram;返回 0 记 wouldblock,ENOSYS/EINVAL/EOPNOTSUPP时打 WARNING 并sticky 关闭该快速路径(turn_params.udp_recvmmsg = false),避免在不支持的内核上热循环。

  3. 发送侧合并包裹。批量回调循环被udp_sendmmsg_batch_begin()/udp_sendmmsg_batch_end()包住(ns_ioalib_engine_impl.c),注释写得很直白:"没有这个包裹,relay 侧 recvmmsg 路径会为每个交付 datagram 发一次 send 系统调用"。批量状态是thread-localudp_sendmmsg_batch_state(对应 #10 的 per-thread scratch 改动),上限MAX_SENDMMSG_BATCH = 32、最小批次MIN_SENDMMSG_BATCH = 4(ns_ioalib_engine_impl.c):不足 4 的尾巴退化为逐包udp_send,其余走sendmmsg()(ns_ioalib_engine_impl.c)。mmsghdr数组在 enqueue 时就填好且连续排布,flush 时直接交给内核,无需重建。

  4. TTL/TOS cmsg 复用。#8 所述"TTL/TOS 尺寸缓冲"体现在 cmsg 区大小按实际需要的IPV4_TTL/IPV4_TOS(IPv6 对应RECVHOPLIMIT/RECVTCLASS)cmsg 计算(ns_ioalib_engine_impl.c),而不是早期每包 64 KiB 的整块缓冲;dtls_listener.c中的共享 ancillary-data 解析器(#7 提到)与MAX_SINGLE_UDP_BATCH IOA_UDP_RECVMMSG_MAX_BATCH(dtls_listener.c)复用同一批次上限常量。

六、阴性结论 I:sendmmsg 为何没有赢

2026-05-03 的 follow-up 在sfo3的两台c-4droplet(turnserver10.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%阈值没能挽回 TX
iter3仅 listener 侧批量425,984286,038376,444210,050332,41797.4%入口/TX 峰值改善,均值 TX 仍低于基线

perf 对比揭示了原因:sendmmsg()确实减少了系统调用进入次数,但每个 datagram 仍然完整走一遍udp_sendmsg与 IP 输出路径

  • baseline:udp_send -> sendto -> __sys_sendto -> udp_sendmsg -> udp_send_skb -> ip_output
  • sendmmsg 变体: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-gso)+91% 转发速率

这是 2026-05-09 落地的 GSO 发送路径,对应第四节热路径地图中"内核 TX 是主成本"的判断。原理:recvmmsg/sendmmsg的 follow-up 都证实本负载的主导成本是每 datagram 的内核 TX 路径udp_sendmsg → ip_finish_output → __dev_queue_xmit → start_xmit),mmsg 式批量压不掉它;而 UDP-GSO(LinuxUDP_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/EOPNOTSUPPsticky 关闭(WARNING 一次,进程内不再重试,内核重启可恢复),保证在无 GSO 支持的内核/NIC 上优雅回退;
  • relay 侧socket_udp_read_batch_recvmmsg的回调循环被udp_sendmmsg_batch_begin/end包裹(见第五节第 3 点),使 recvmmsg 批次内被触发的 peer→client 发送也能合并。

验证(2026-05-09,nyc1 新建c-4droplet:turn10.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/B(baseline → gso → baseline → gso)确认量级):

变体eth1 RX ppseth1 TX ppssys CPUidle CPU
baseline_r1322,091127,44522.9%67.5%
--udp-recvmmsg --udp-sendmmsg --udp-gso(gso_r1)266,068257,99615.0%78.7%
baseline_r2309,475125,57320.9%70.7%
gso_r2275,992225,36614.9%74.3%

均值服务端转发速率(eth1 TX):baseline 126,509 pps → GSO 241,681 pps,+91%(1.91×),sys CPU 从 21.9% 降到 14.9%——相当于 TX pps 每 sys-CPU-% 的CPU 效率约 2.8×

12 s 包长 sweep(uclient 上报的 send_pps 均值,只用于定序——绝对吞吐以 eth1 TX 为准):

变体m=1m=2m=4m=8m=16m=32
baseline230,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

注意 m=1 列的 GSO 数字偏低是假象:uclient 报的是生成器自身发送速率,GSO 下 loadgen 侧单线程的turnutils_peer变成新瓶颈、无法回射 240 k pps,所以生成器侧速率被拉低;30 s 的 eth1 抓取才是权威的服务端指标。m=2..32区间保留该 sweep 只为了说明 GSO 相对recvmmsg+sendmmsg没有回退。

perf children 占比(m=1、12 s,turnserver 进程):

符号baselinerecvmmsgrecvsendmmsggso
__x64_sys_sendto(children)43.6 %47.6 %22.8 %0.0 %
__x64_sys_sendmsg(children)38.1 %
__x64_sys_sendmmsg(children)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_sysret(self)7.2 %4.7 %4.4 %2.4 %
entry_SYSCALL_64_after_hwframe(self)4.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-op;
  • GSO 资格在每次_begin/_end时重置,混合目的/混合尺寸负载透明回退到既有sendmmsgudp_send路径;
  • sticky disable 保证不支持 GSO 的主机不会在失败路径上热循环,只记一次 WARNING;
  • 实测环境 Linux 6.8 + virtio-net(DOc-4),gso_max_segs=65535;内核 <4.18 完全缺少UDP_SEGMENT,由 sticky-disable 路径覆盖。

CLAUDE.md 中 2026-05-16 的三 droplet 8 流矩阵进一步印证了这套快速路径组合的整体效应(45 s、8 路-m 1并发、c-4):

配置recv pps(8 流合计)turnserver CPU服务器 host idlesoftirq
baseline91 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 didn't work)

1.--udp-recvmmsg默认全量开启(第 1–11 轮时期)——当时失败,后来收敛范围后成功。原始发现:flag 把 16 缓冲批量路径套到每个 per-session 已连接 relay socket,而这些 socket 永远只收一条流;m=1m=100的多轮 A/B 反复确认吞吐持平或微负——per-session 预分配churn 吃掉了 listener 侧收益,故保持 opt-in。 解决:后续提交把recvmmsg限定到共享 fan-in socketudp_recvmmsg_eligible,见第五节),listener 在客户端并发不低时是真正的收益点、空闲时成本极低;per-session 税消除后,flag 在 Linux 上默认开启,--udp-recvmmsg=false为退出方式。

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_ppsloadgen 发送速率无论中继容量如何都饱和在 ~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 次条件返回。
  • sendtoMSG_CONFIRM可跳过 ARP 刷新,但neigh_resolve_output+neigh_hh_output合计 ~17% 只因为包量太大——单包看是正常缓存邻居路径,不是刷新。
  • socket_input_workerMAX_TRIES从 16 提到 64 不改变系统调用次数,只是推迟返回 libevent;没有第 1 项(发送批量)配合就是无用功。

十一、后续优化清单(按 m=1 指标预期收益排序)

  1. 发送侧批量(sendmmsg)或让接收批次传递更深。占用计数已证明接收批量在工作(m=1 均 15.6 包/次、m=100 均 11.4)。代码随后立即对每个 datagram 调用既有每包回调,每个转发包仍付一次 send 系统调用。下一个可测杠杆:在接收批次期间按线程排队出站 datagram 并用sendmmsg刷出,或为热 UDP 中继路径引入批次感知回调。(后续已被 UDP-GSO 方案承接,见第七节。)
  2. 开发发送批量时保留recvmmsg占用计数。足够便宜,用于定向性能构建,且能一眼看出基准在压一个 relay 线程还是全部线程。周期性日志在大范围交付前考虑藏进 verbose/debug 选项。(已按此执行:--udp-recvmmsg-log。)
  3. 发送路径 GSO(UDP_SEGMENT)。Linux 可把一个"大" datagram 在内核里切分,服务同一目的地的背靠背发送;channel-data flood 正是同目的地。设UDP_SEGMENT后用一次 N×packet_size 的sendmsg可大幅削减 skb 分配 /__dev_queue_xmit开销。需小心处理短尾与不均匀尺寸;与 (2) 互补。(已落地,见第七节。)
  4. 继续内联跨编译单元的每包访问器。第 4/5 轮模式继续适用:addr_eq(每个 channel-data 包做 permission 查找时调用)、ioa_network_buffer_get_sizeget_ioa_socket_type/_app_type。各自足够小;唯一谨慎点在于它们声明于ns_turn_ioalib.h——属半公开服务器库 API,把函数体改为内联不破 ABI 但需重编所有消费者。预计每项 <1%,但便宜。
  5. 计量后重估--udp-recvmmsg默认值。(已完成。)把recvmmsg收敛到共享 fan-in socket 消除了阻止默认开启的 per-session-relay 税;listener 是真正的 fan-in 点,客户端并发不低时受益、空闲时成本低。现已在 Linux 上默认开启,--udp-recvmmsg=false为退出项。

十二、如何续接这项工作

  1. 确认 droplet 仍在运行(第二节所列 IP)。若已销毁,用c-4/nyc1/default-nyc1VPC 与pavelSSH key(id 23704483)重建。
  2. 若基线二进制丢失,从git archive master重新上传/tmp/coturn_clean.tar并重建/root/coturn_baseline/build/bin/turnserver。A/B harness 依赖两个二进制在 turnserver droplet 上并排存在。
  3. 跑一轮 6 轮交替 A/B 作健康检查:当前分支尖端仍应比master快 ~5%。若不是,环境已漂移,需要重新锚定基线。
  4. 从清单(第十一节)挑下一项。当时下一个实质性收益在socket_input_worker中引入recvmmsg;后续的 sendmmsg/GSO 章节即是该项的延续与结论。

十三、GSO 之后的下一步杠杆与遗留重构

日志末尾(2026-05-09 GSO 验证后)给出的建议下一步:

  1. 把 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 拓扑的升级方案。)
  2. per-peer 已连接 relay socket。同目的地是 GSO 资格谓词;已连接 relay socket 永远同目的地,还能省掉每次发送的route_lookup
  3. GSO sendmsg 上加MSG_ZEROCOPYrep_movs_alternative在 GSO 下仍占 3% self,zerocopy 可免用户态→内核拷贝;对 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询