最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

Golang 在基于 UDP 的用户态协议的性能提升

字节跳动视频云技术团队

  • 2023-03-05
    北京
  • 本文字数:4866 字

    阅读完需:约 16 分钟

Golang 在基于 UDP 的用户态协议的性能提升

介绍


随着近年来直播、短视频、在线会议等音视频相关应用愈发普及,对用户体验的要求也随之提高。为了实现把声音和画面送达到用户面前就需要通过网络来分发,所以如何在各种网络环境下提高网络的效率,从而提高用户体验就变得很重要。如果使用基于 TCP 的协议来分发音视频数据,不能较大程度地满足用户体验上的需求,比如在直播场景下,播放快、卡顿少、延迟低等问题。我们可以通过减少建联时间,减少重传等手段来优化体验。所以基于 UDP 构建传输协议的需求越来越多,比如 quic , rtp 。其次随着云服务的兴起, golang 也逐渐在很多地方使用了起来。在我们直播 CDN 场景下,流媒体服务器需要承担较高的流量, golang+udp 这个组合在我们内部实践中会遇到性能不满足要求的情况,所以本文会分享如何提高用 golang 实现的基于 UDP 的传输协议的性能提升。


分析方法


早期,我们的流媒体服务器在基于 UDP 的可靠传输协议下,我们的服务器能承受的带宽远低于 TCP 几倍,导致了基于 UDP 的协议无法大面积应用。所以便有了着手提升相关性能的需求,主要是通过分析程序性能火焰图,操作系统各项指标(如锁,软中带,负载分布等)来确定性能低的原因。下面会介绍下常用的几个工具. pprof 是 golang 自带的一款性能分析工具,由于其相当的简单好用,所以不管是在性能分析,还是 bug 定位等场景出现的频率都很高。要是用 pprof ,比较简单的一个方法是通过引入net/http/pprof 包,会自动嵌入到默认 http 服务器里,如果服务没有 http 服务可以通过在 goroutine 里创建一个。



然后通过127.0.0.1:5601/debug/pprof这个地址就 可以获取到。当然 golang 也提供了可视化的工具,go tool pprof -http=:5555 http://127.0.0.1:5601/debug/pprof/profile 可以从网页里以火焰图的方式分析各部分开销占比,然后针对性的制定我们的优化方法。对于操作系统的一些开销,通过 pprof 我们是捕捉不到的,可能得借助一些其他的工具来分析。


比较常用的就是 top 、 perf 、 sar 、 ss 、 ethtool 等。


perf 可以比较方便的统计出比如 CPU 开销, cache miss 等。 top 可以比对分析进程和总 CPU 的占用,软中断等的占用。 sar 可以统计网卡 PPS 等。 ss 和 ethtool 可以用于诊断 socket 和网卡的一些信息。通过 pprof ,可以比较容易的得出 CPU 开销主要在内存分配,系统调用, runtime ,内存拷贝几部分。


下面会分享一些有用和没用的手段。


系统调用


mmsg


收发数据包的系统调用的占比比较大,这两个系统调用正好提供了在一次系统调用下收发多个包,极大的降低系统调用的数量级,所以用好这两个 API ,我们的性能就能有较大的提升了。



参数指定了要读取或者发送的数据包的数量,以及一些设置标志,接收还有个超时时间的参数,这个参数有点坑,后面会讲到。这两个系统调用其实是属于 recvmsg/sendmsg 的封装,大致类似于 for 循环里调用 sendmsg/recvmsg 。


sendmmsg


sendmmsg 的主要点在于如何聚合更多的包在一次系统调用中发送,可以选择把多个 session 的发送数据包进行聚合,那么聚合数量就可以大大提高。但是这样的话,会对质量有一定的影响,因为聚合的话可能得花一点时间来聚合足够多的数据包。


recvmmsg


socket ,属于 noneblock socket ,在 recvmmsg 的时候会出现读取到很少量的东西就返回了,因为当没有东西可读的时候会返回 EAGAIN ,会导致系统调用直接返回,需要等待一会。但是当使用自己创建的 block 的 socket 的时候,如果没制定 none block ,会出现未读满指定数量的包是不会返回的,即使设置了超时时间,导致前面的包一直得不到处理,这个情况下我们可以使用MSG_WAITFORONE这个 flag 。 在 CDN 场景下,主要是下行压力, recvmmsg 的主要特点在于可靠传输协议的 ack 数据包上。如果可以,我们可以考虑降低 ack 的数量比,从而减少 recvmmsg 的系统调用开销和中断开销。


效果


从给出的测试性能数据来看,在一次发送 64 个数据包的情况下能够提高 20% 的性能 **[1]** 。这个测试有一定局限性,在实际应用场景下提升还是远远超过这个值的。实际在服务器配置和网卡更好的情况下,首先可以提高 mmsg 的数量,达到 128 、 256 等。在协议栈应用上能提升一倍的效果。


GSO


GSO 的全称是( Generic Segmentation Offload ),他还有另外一个兄弟 GRO 。由于 MTU 的限制,所以对于 UDP 的写入,如果写入的数据超过 MTU 大小,且没有禁用 IP 分片,那么将会被进行 IP 分片,但是 IP 分片是不利于做可靠传输协议的,因为丢包成本太高了,丢一个 IP 包就等于丢了所有。前面我们提到减少系统调用,如果使用 GSO 的话也是可以的,我们可以一次写入更大的 buffer 来达到减少系统调用的目的,不过这个有个前提是每次需要写入的数据足够大,且对内核版本要求较高。另外 GSO 除了可以一次写入较大 buffer ,在支持的 GSO offload 的网卡还有相应的硬件加速,可以通过ethtool -K eth0 tx-udp-segmentation on来开启。


GSO 可以通过两种方式去使用,一种是设置 socket option 开启,一种是通过 oob 去对 msg 级别设置。 一般通过 oob 的方式去进行设置,因为这样比较灵活一些,缺点的话就是会多 copy 一点内存。



允许小于 64K 的 buffer 一次性写入且不会分片,会按照 gsoSize 进行分成多个 IP 包。根据 Google 的测试数据,性能提升能达到 1.7 倍左右 [2] 。不过在一次写入的数据量较少的情况下是比较难以利用起来的。比如直播的码率不高的情况下,单链接能写入的数据量是有限的。不过在码率较低的直播流,一次写入的数据较少的情况下 GSO 效果不是特别明显。在高码率下这个还是能带来很大的提升的,值得尝试。如果可以的话,也是可以聚合下多次的数据来提高 GSO 写入数据量。不过由于 GSO 是等分,所以在如何让每一个 UDP 包等大上比较麻烦。


内存分配

包相关的内存分配



比如像上面的 recvmmsg 系统调用,我们需要为本次系统调用准备内存空间, iovec 结构, msghdr 结构等等。对于一个几百万 PPS 的协议栈来说,上面的内存肯定不能每次都分配,我们必须得采取复用。对于 iovec 这样的结构,完全可以每次系统调用复用同一块内存,把对应的值 copy 过去就行。对于目的地址, oob 等,我们也可以与分配并存储在 session 的内存里,直接传他的地址就行。 oob 的话,可能每次传递的都不一样,我们可以准备多份,比如是否使用 GSO ,选择需要的那一份就行。像 golang 的 github.com/golang/net/… 提供的 recvmmsg API 的话由于没有处理内存分配的问题,所以不适合直接使用。数据包的 buffer 这个也是毋庸置疑需要复用的,简单点的场景我们可以使用 sync.Pool 。这个 buffer 我们可以和 sockaddr 、 oob 等的 buffer 复用同一片 buffer 。



其中 rawBuffer 是真正分配的内存区域。 Data 指向 rawBuffer 中数据包的位置, SockAddr 指向 rawBuffer 中相应的位置, Oob 同理。这样可以减少多次 sync.Pool 的调用和内存分配次数。


interface


在 golang 里, interface (指 empty interface )由下面的 struct 表示。



当把一个具体类型的变量转换为 interface 类型时,如果这个值是 copy 类型,那么需要给他分配空间然后拷贝过去,所以这是一个潜在的内存分配 case 。下面的代码展示把 64 位的整数转换到 interface ,最近的版本增加了优化,对于小于 256 的数字提前预分配好。



举个例子:


1 log.Debug(formats string, v ...interface)


像上面的那个 debug 的 log 函数,虽然在线上的日志级别不会执行,但是由于参数是 interface ,所以会涉及到类型转换,在大量出现调用的情况下就会有很大的内存分配开销。下面是一个简单场景测试,说明下问题。



测试结果



锁开销


在我们引入了一批 96C 的服务器后,发现性能并没有提升,反而有所下降。通过 perf top 可以发现__raw_caller_save___pv_queued_spin_unlock 和trigger_load_blance的 CPU 占用比较高。根据测试, 88 核机器和 44 核机器的服务能力差不多一致。所以可以认为 88 核机器改造成两台 44 核的性能提升能有一倍。推荐机器虚拟化/容器化为多个小核心数的机器或者多进程模式,这样能够更好的利用机器资源。包括对于两颗及以上 CPU 的机器,多进程分别绑定到每颗也能有较可观的提升,可以减少 CPU 间切换的代价。绑定可以通过 numactl 来进行控制。


zerocopy



UDP 的 ZEROCOPY 在 5.0 内核也支持上了, zerocopy 的 API 主要两部分组成,一个发送时声明使用 zerocopy ,另一个是接收内存可回收的回调。但是对于小数据包的 ZEROCOPY ,性能反而会负向,因为会多产生一次系统调用。并且在 UDP 下并不能在一次 MSG 里传送超过 MTU 的数据包,即使开启 GSO ,也不过 64K ,从 Google 给出的数据来看,有 13% 的提升 [3] 。根据我们的测试,在 GSO 数量较小的情况下,性能负向还挺多的。这个特性使用起来也有一定的复杂性,所以不建议使用,可以更多的考虑下 UIO 相关的技术。


RPS


我们接入了一批核心数比较高的虚拟机后,发现负载并没有上升。通过观察 CPU 负载,发现只有少数核心负载比较高。在查询资料后了解到这可能是网卡接收队列数和 CPU 核心数不匹配,导致接收队列的软中断都调用到前面的 CPU 核心上了。通过 ethtool 可以查看网卡队列的数量,如果和 CPU 核心数量不匹配,目前解决方法有两个,一个是要求机器的核心数和网卡队列要匹配;另一个较次一点的方案是配置 RPS , RPS 可以让一个队列绑定到指定的 CPU 核心上。这个实际上会带来一部分的额外 CPU 开销,因为他的实现原理是收到软中断后再分发到其他的 CPU 核心上,不过好处是可以把空置的 CPU 核心利用起来。 RPS 通过/sys/class/net/eth0/queues/rx-[n]/rps_cpus 这个文件来进行配置,文件内容代表每个网卡队列到 CPU 核心的映射关系,每一个二进制位为 1 代表可映射,以 16 进制字符串保存。


IO 模型

关键手段


  • reuseport

  • 原生 socket

  • 无锁队列

  • 每个 CPU 一个处理循环


详细实现和效果


在做性能提升工作期间,也摸索了一些代码结构模型相关的东西。可以作为一个参考,每个项目自身不一样,所以并不一定适合。



  1. 直接通过 syscall 创建 socket ,因为 golang 的 API 创建的 socket 是绑定了 epoll 的,在我们的场景下 socket 的数量是一定且少量的,不需要 IO 多路复用来提高吞吐,这样能够避免掉 epoll 带来的 CPU 开销。设置 socket 为非阻塞的,这样才可以在一个 goroutine 处理收和发。对于创建的 socket ,通过使用 reuseport 来负载均衡到所有 CPU 核心上,提高机器的利用率。现在的网卡一般都是多队列的,所以是能够做到分发到多个核心上的。分发策略一般通过ethtool -N eth0 rx-flow-hash udp4 sdfn来设置,具体设置项可以参考 man page ,分发策略一般也是不需要动的。


  1. goroutine 来处理链接。每个这样的 goroutine 处理一组链接的收包,发包,超时等,也可以比较方便的汇聚多个链接的数据调用一次系统调用。协议栈和应用层,数据包收发层通过无锁队列传递数据,采用这样的结构设计主要目的是为了减少 golang 调度的触发和锁的开销。 goroutine 的数量不随链接增加,也可以减少调度的压力。


  1. 前面两步的 goroutine 可以调用runtime.LockOSThread把自己送入调度器全局队列来提高调度优先级,避免调度延迟。


总结


在用 golang 来实现的协议栈,性能问题可以分为两块:语言层面和系统层面。语言层面内存分配,和 rutime 的开销一般比较多。系统层面系统调用和锁的开销比较多。像开源的 quic-go 就会有这些问题,需要我们逐步去分析并解决。在 QUIC 和 HTTP3 的趋势下,内核在 UDP 方面的功能和性能相关工作也开始增多,相信也会有更多的方法来提高性能。不过个人觉得 Golang 确实不太适合在高性能,高调度精度等场景, GC 和内存分配的不可控性,与操作系统的抽象隔离也会在这些场景下限制发挥。现在服务器配置也越来越高,核心数量多,代码就容易出现锁竞争激烈的情况,导致额外开销大幅增加。所以容器化、虚拟化等技术把机器变为核心数较小的多实例模式也是很有价值的。



参考文献

[1] lwn.net/Articles/44…

[2] vger.kernel.org/lpc_net2018…

[3] patchwork.ozlabs.org/project/net…

[4] lwn.net/Articles/75…

[5] lwn.net/Articles/35…

公众号推荐:

跳进 AI 的奇妙世界,一起探索未来工作的新风貌!想要深入了解 AI 如何成为产业创新的新引擎?好奇哪些城市正成为 AI 人才的新磁场?《中国生成式 AI 开发者洞察 2024》由 InfoQ 研究中心精心打造,为你深度解锁生成式 AI 领域的最新开发者动态。无论你是资深研发者,还是对生成式 AI 充满好奇的新手,这份报告都是你不可错过的知识宝典。欢迎大家扫码关注「AI前线」公众号,回复「开发者洞察」领取。

2023-03-05 10:406097
用户头像
鲁冬雪 InfoQ 策划主编

发布了 337 篇内容, 共 195.7 次阅读, 收获喜欢 270 次。

关注

评论 1 条评论

发布
用户头像
干货挺多的
2023-03-06 16:35 · 上海
回复
没有更多了
发现更多内容

什么是智慧公厕?智慧公厕存在的意义!

光明源智慧厕所

智慧城市

Matlab常用图像处理命令108例(八)

timerring

图像处理

未来智安入选FreeBuf《CCSIP 2022中国网络安全行业全景册(第五版)》

未来智安XDR SEC

前端有边界,但低代码没有

引迈信息

前端 后端 低代码 ChatGPT

华为工单宝——为企业客户提供更强大的数字化现场作业解决方案

科技怪授

CDN 华为云

从“可用”到“好用” 京东云构建融合开放适配国产化应用的全栈产品矩阵

京东科技开发者

国产化 京东云 国产化替代 京东云峰会

企业支出如何一眼看全局,用友BIP很在行

用友BIP

商旅费控

浪潮inBuilder低代码平台社区版来了!

inBuilder低代码平台

开源 低代码 企业级低代码平台

AGI时代即将降临,现代化产业建设的出路又在何处?

加入高科技仿生人

低代码 AGI 现代化产业

浅谈DWS函数出参方式

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 3 月 PK 榜

火山引擎DataLeap:数据秒级生产,揭秘电商实时数仓最佳实践!

字节跳动数据平台

数据治理 电商 数据研发 企业号 3 月 PK 榜

Tapdata Connector 实用指南:云原生数仓场景之数据实时同步到 Databend

tapdata

数据库 大数据

什么是安全文件传输

镭速

《中国奇谭》打动万千观众,一首歌道尽现代人心酸

HIFIVE音加加

音乐 音乐播放

共享订阅--MQTT 5.0新特性

EMQ映云科技

物联网 IoT mqtt 企业号 3 月 PK 榜 共享订阅

Macbook技巧,Type-c接口失灵怎么办

互联网搬砖工作者

焱融为国家电网打造存算一体的融合基础架构 助推能源行业新基建

焱融科技

文件存储 容器存储 分布式文件存储 高性能存储 国家电网

实战|网站监控如何做好监测点管理与内网数据采集

云智慧AIOps社区

安全 监控 监控宝 云智慧 网站监控

【3.10-3.17】写作社区优秀技术博文一览

InfoQ写作社区官方

热门活动 优质创作周报

基于声网 Flutter SDK 实现互动直播

声网

flutter

8年Java架构师面试官教你正确的面试姿势,10W字面试题搞定春招!

小小怪下士

Java 程序员 后端 java面试

强大的录屏:Camtasia 2022 汉化激活版

真大的脸盆

Mac 软件 屏幕录制 录屏软件 录屏工具

全能代码编辑器:CodeRunner 最新激活版

真大的脸盆

Mac 代码编辑器 Mac 软件 代码编辑 编辑代码

大资管行业数字化转型解决方案 | 行业方案

袋鼠云数栈

大数据 数字化转型 解决方案

保姆级教程!基于声网 Web SDK实现音视频通话及屏幕共享

声网

如何通过Java更改Word中的页面大小和页面方向

在下毛毛雨

Java word 页面布局

Application Loader及Transporter App上传ipa外、可以在Windows上架iOS APP工具

雪奈椰子

ios打包

CDN省钱大法!华为云开年采购季低价购,CDN0.03元/GB起,短信0.006元/条起

科技怪授

CDN 华为云

物联网数据应用开发最佳实践——数据价值类

阿里云AIoT

数据挖掘 物联网 存储 数据管理 调度

解决运行VMWare虚拟机报错“打不开 /dev/vmmon:断裂管道”

互联网搬砖工作者

专场直播预约 | KaiwuDB 离散制造业场景解决方案

KaiwuDB

数据库 KaiwuDB 离线制造业 行业解决发展

Golang 在基于 UDP 的用户态协议的性能提升_语言 & 开发_InfoQ精选文章