当前位置:策士生活网 >> 科技 >> 文章正文

为什么选择Rust开发顶级实时通讯产品?

发布于:2020-12-27 被浏览:2737次

作者|杰克麦金蒂

翻译|王强

规划|赵育莹

Tonari的目标是为虚拟世界中的人们创造一种真正自然的互动体验。我们已经开发了2年,应该是一款延迟最低的高分辨率“电话会议”产品,准备投入生产环境。

屏幕到屏幕的延迟为130毫秒(从光线照射到相机一侧到图片出现在另一侧的时间)

3K,60fps视频传输

高比特率48千赫立体声音频

我们对比了Zoom和WebRTC之间的典型延迟,测量了办公室同一网络上两台笔记本电脑(X1 Carbon和MacBook Pro)之间的315-500ms的数量,差距巨大。这么大的延迟意味着双方都要不断的打断对方,而tonari则可以自然流畅的交流。图像质量方面,市场解决方案只能提供模糊的面部图像,摄像头似乎对准了对方的鼻子;另一方面,tonari可以传输大视角的高保真图像,通过网络流畅远程地传输面对面交流中看到的细微肢体语言。

自2月份第一个试点项目启动以来,我们没有经历过软件导致的停机(以太网电缆断开是另一回事)。虽然我们真的很想把自己当成有才华的工程师,但我们真的很想感谢Rust,没有Rust,我们没有信心能达到这么高的稳定水平。

https://blog . tonari . no/changing-communication-and-culture-in-organization

一个

我们为什么不用WebRTC

Tonari的第一个概念验证产品使用了一个基本的投影仪、蓝牙扬声器和一个运行在标准WebRTC(JavaScript)上的网站。从那以后,我们走过了漫长的道路。

尽管这个原型(以及我们对未来的愿景)已经赢得了我们的投资,但我们知道,如果我们不能实现比WebRTC更低的延迟和更高的保真度,tonari将会被它彻底摧毁。

我们想到了一个办法:“好吧,那我们就直接修改WebRTC,用一个用C写的漂亮UI包装起来,然后就可以快速发布了。”

在WebRTC将近75万LoC的庞大代码库里挣扎了一个星期后,我们意识到,哪怕是很小的改变,也会带来很大的痛苦。

因此,在一次研讨会上,我们决定从零开始重新实现整个技术堆栈要容易得多。我们希望了解和理解在我们硬件上运行的每一行代码,我们应该根据我们想要的特定硬件来设计它。

于是我们开始了一段全新的旅程,从浏览器或者现有RTC项目等高级界面,到更核心的部分,从零开始进入底层系统与硬件交互的世界。

我们需要系统本身有足够的安全性来保护tonari用户的隐私。我们需要它有强大的性能,让通话过程尽可能的人性化,实时流畅。此外,随着新员工的不断加入,他们需要从我们现有的工作中学习,并在此基础上扩展。我们需要使代码更加成熟,具有良好的可维护性。

我们讨论并排除了一些选项:

安全性: C和C的内存和并发是不安全的,它们复杂的系统让我们很难获得一致简单的开发体验。

性能: Java、C#和Go的内存管理是不透明的。在延迟敏感的应用程序中,如果你想完全控制内存,它们可能很难使用。

可维护性:哈斯克尔、尼姆、D等定制语言在工具链、社区、人力资源等方面都不尽如人意。

事实上,在我们看来,Rust是唯一能够满足这些需求并用于生产的语言。

2

从铁锈开始

Rust的优势来自于开发社区做出的无数决策。

其构造体系固执己见,设计简单。它本身就是一个完整的生态系统,你可以很容易的引导新的工程师进入项目,建立开发环境。

内存和并发安全性保证再好不过了。我们相信如果继续用C来开发的话,到现在还不能第一次部署——,可能还会卡在很多细节上。

我们通过CUDA等API与底层硬件交互的能力,通常是通过现有的板条箱(板条箱,Rust的代码基术语)来实现的,这使得我们能够为第一个生产版本的延迟设定更高的标准。

随着tonari的不断发展,我们现在选择的嵌入式微控制器的固件可以用Rust编写,不用在不安全编程系统的旧世界里建造天堂。

我们依赖的工具箱

这里就不说cat Cargo.toml了,重点讲一些精心挑选的工具箱。

“优于标准”工具箱

几乎在所有方面,横梁比std :3360 sync 33603360 pspsc更适合线程间通信,最终可能并入STD。

Parking_lot几乎在所有方面都有一个优于std:sync:3360互斥体的互斥体实现,有朝一日可能会并入标准库。它还提供了许多其他有用的同步原语。

与Vec相比,bytes是一种更健壮且通常性能更高的字节处理方法。

如果您想优化底层网络,socket2将是您的最终选择。

锦上添花

Fern是定制和美化日志输出的简单方法。我们用它来保持日志的可读性和内部标准化。

Structopt是你一直梦想的CLI参数处理方法。除非你的依赖很少,否则没有理由不使用它。

货运经典传奇

货物发布使得减少构建版本变得容易。

cargo-udep可以识别未使用的依赖关系,并最大限度地减少我们的构建时间。

Cargo tree(最近集成到Cargo中)显示了一个依赖树,它在很多方面都很有用,但主要是用来寻找最小化依赖的方法。

Cargo-geiger帮助我们快速评估外部依赖性,以解决可能的安全性(或正确性)问题。

Cargo-flamegraph在跟踪代码中的性能热点方面给了我们很大的帮助。

项目结构

Tonari代码库是单一架构。本质上,我们有一个货物工作区,里面有二进制板条箱和许多配套的图书馆板条箱。

我们将工具箱放在一个存储库中,这样就可以很容易地在我们的二进制文件箱中引用它,而不必将它发布到crates.io或在我们的Cargo.toml中指定git依赖项。当这些库需要以开源方式发布时,很容易将它们分解成单独的存储库

库,二进制,为什么不用两者?

我们有一个主库,它包含一个统一的应用编程接口,用于与硬件、媒体编解码器、网络协议等进行通信。除了这个私有API之外,我们在工作空间中还有单独的板条箱,我们将其视为开源候选。例如,我们编写了适用于长期高吞吐量参与者的参与者框架,以及用于可靠、高带宽和低延迟媒体流的网络协议。

我们对tonari系统的不同部分使用不同的二进制文件,每个二进制文件都位于二进制文件中。它的库模块包含一组可重用的参与者,将我们的私有API与参与者系统相结合,然后是一组单个二进制文件,这些文件使用这些参与者并定义它们之间的管道。

视线内的标志

我们广泛使用功能标记在不同的操作系统(如旧的MacBook Pro)或不同的硬件配置上开发项目。这让我们可以轻松更换相机硬件,而无需额外的运行时检查或糟糕的sed编程技能。

比如Linux使用v4l2(视频For Linux.2)访问大多数网络摄像头,但其他网络摄像头可能有自己的SDK。要为不使用v4l2或特定操作系统中不可用的SDK的平台编译,我们可以将这些SDK放在函数标志后面,并导出一个公共接口。

举一个(简化的)具体例子,假设我们有一个定义为特性的通用相机接口:

假设我们有三个不同的相机接口:v4l2、corevideo和拍立得。我们可以使二进制文件特别针对这一特性变得灵活,并且我们可以使用函数标志来切换不同的捕获实现。

如果我们的代码与实现捕获特性的东西相匹配,而不是特定的类型,我们现在可以简单地切换功能标志,在各种目标平台上编译。例如,我们可以有一个带有field video_Capture: Box的结构,它使我们能够存储任何类型的可以从相机拍摄的捕捉。

支持我们上面编写的捕获实现的Cargo.toml文件示例可能如下所示:

这样,我们可以避免构建和链接到特定于平台的库,比如v4l2,它不是一个完全流行的选项。

工作中学习Rust

转行Rust一年后,我们的第四个工程师加入了团队,他在Rust或者系统工程方面没有太多经验。虽然学习曲线不可否认,但我们发现Rust为刚接触底层编程的初学者提供了惊人的动力。

如前所述,语言的内置内存和并发安全性意味着很多问题无法编译,编译器本身往往是你唯一需要的老师,因为它给出的警告解释的很清楚。关于Rust优秀的编译器消息和优秀的文档有很多文献(参见关于字符串的长篇讨论)。这些对我们来说都是非常有用的资源。

https://doc.rust-lang.org/stable/book/

https://doc.rust-lang.org/book/ch08-02-strings.html

不像其他很多语言,Rust里做各种事情总有一个很明显的“正确的方法”。没有以“正确的方式”书写的代码往往很显眼,很容易在审计中被发现,并且经常被货物剪贴自动识别。

事实上,这意味着新工程师可以快速开始贡献可用于生产的代码。代码审查可以继续关注实现,而不是花费更多的精力手工检查正确性。

IDE普查

在IDE部门,我们发现Rust相对于一些前辈来说是比较不成熟的。尤其是今年,我们取得了很大的进步,大家都找到了一个非常舒适的开发环境。

我们一个人用macOS,三个人用Linux(Arch,Ubuntu,Pop!_OS,我们可以看到各自的受虐倾向)

我们中的两个人使用带有铁锈分析器插件的VS代码,我们中的两个人使用带有铁锈增强的崇高文本。

我们经常分享设置,尝试对方的环境(布莱恩除外,他从29岁开始就停滞不前),我们一直在关注能帮助我们更好协作的新开发工具。

代码风格指南已死,鲁斯特万岁

你经历过狂野的发展生活吗?在提交代码之前,我们没有代码样式指南文件可供阅读。我们不需要这种东西。我们只是执行rustfmt。说实话:这确实是代码评审的前沿。

我们如何检查代码

我们的代码审查非常简单,因为到目前为止我们只有四个人,我们很幸运地赢得了彼此之间的信任。我们的主要目标是至少有两对眼睛盯着每一行代码,不要妨碍对方,这样我们才能活下去。

连续测试

我们使用谷歌的云构建器来运行配置项构建,因为我们的基础架构堆栈主要基于GCP构建,并且可以轻松调整构建机器规格和定制构建映像。每次提交都会触发它,并运行cargoclippiy和cargo build。我们将-D警告传递给编译器,将警告升级为错误,以保证我们的修改在下一次可怜的同事拉修改时,不会面临大量的rush c警告。

为了缩短配置项的构建时间,我们缓存了目标和。云存储中的货物目录,以便下次可以下载和增量构建。

我们也听到了关于sccache的好消息,很快会进行评估!

与现有C/C库的集成

Rust生态系统很棒,但是有很多现有的项目需要很多时间才能移植到Rust。网络音频处理就是一个很好的例子。它提供的好处(清晰的音频,没有回声和刺痛)是显而易见的,短时间内不太可能移植到Rust(它包含大约8万行C和C代码)。

幸运的是,Rust可以轻松使用现有的C和C库。板条箱Bindgen完成了大部分繁重的工作。给它一个用C或者C写的头文件,它会自动生成(不安全的)Rust代码,可以调用头文件中定义的函数。到那时,由您决定是否创建一个更高级别的Rust板条箱来公开一个安全的API。

https://crates.io/crates/bindgen

对于具有简单或通用构建过程的库,这个过程的大部分是相当自动化的。但是,创建更高级别的安全API很重要。——bindgen提供的Rust API不适合直接使用,因为不安全,不符合习惯。幸运的是,一旦你有了一个更高级的应用编程接口,你最终可以用你自己的Rust版本替换C库,而板条箱消费者不会注意到这种变化。

这些特性使我们能够使用许多从来没有本机Rust API的API和硬件,或者需要几个月或几年才能重新实现。底层的OS库,大型的代码库(比如webrtc-音频处理),厂商提供的摄像头SDK,都可以在我们的Rust代码库中使用,不需要把整个应用语言转移到c,同时还能提供很好的性能。

有些C库很难直接从Rust对接。你必须把类型放在白名单上,因为bindgen无法处理一些导入的STD :3360 *类型,而且不适用于模板化函数和复制/移动构造函数,以及这里记录的很多其他问题。

https://rust-lang.github.io/rust-bindgen/cpp.html

为了解决这些问题,我们通常创建一个简化的C头文件和源代码包装器来导出对bindgen友好的函数。这种工作比较复杂,但是工作量比把整个库移植到Rust要少得多。您可以在这里看到这个包装器创建的示例。

https://github.com/tonarino/web RTC-audio-processing/tree/2 a 973929 C3 afbc 24 beea 75 aa 235 f 3341 a 7be 275 a/web RTC-audio-processing-sys/src

由于Rust和C/C项目的所有生态系统都只是对bindgen的调用,所以我们可以在不牺牲执行速度的情况下,以最高的质量轻松访问一些现有的软件包。

Rust的痛点

生锈也不是没有问题。这是一门相对较新的语言,并且在不断进化。在评估向Rust迁移的选项时,应该考虑它的一些缺点。以下是我们总结的一些棘手问题:

编译时间长.著名的xkcd漫画讽刺说,你可以一边喝咖啡休息,一边等待Rust代码编译,这是非常真实的。例如,在一台中等性能的笔记本电脑上以非增量方式编译我们的代码库大约需要8分钟,但实际情况可能更糟。Rust编译器要实现强大的语言保障还有很多工作要做,必须从源代码中编译出整个依赖树。增量构建更好,但是有些crates自带构建脚本,可以拉取和编译非Rust依赖代码,在升级版本和切换分支时可能需要清除构建缓存。

库覆盖率.Rust的图书馆生态系统已经相当成熟,但相对于C/C而言,覆盖面还是有限的。我们最终实现了自己的抖动缓冲区,并用Rust的bindgen包装了几个C/C库,这意味着我们的Rust代码中存在不安全的区域。不寻常的项目往往会有少量不安全的代码,这使得学习曲线恶化,带来更多的内存错误的机会。

鲁斯特问你,首先编写正确和明确的代码.如果你犯了一个错误,编译器不会错过的。如果不太在意并发和内存保障,开发的时候会觉得慢但没必要。然而,Rust开发人员一直在努力改进错误消息。它们友好且可操作,通常包含修复建议。一个好的内存和并发的基本模型也有助于更快地克服最初的驼峰,所以我们建议你花一些时间来真正理解这种语言及其保证。

Rust 的类型推断器是如此强大,这让你觉得你有时在使用一种动态的语言。也就是说,有时它并不完全按照您希望的方式工作,尤其是在泛型和deref强制方面,您最终必须四处摸索才能满足这个推断者。这可能会带来挫败感,如果团队中有人经历过这个学习阶段会有帮助。只要有足够的耐心,这种挫败感往往会变成一个令人惊叹的瞬间,可以加深我们对语言设计及其使用的理解,避免可能出现的错误。

语言进化.Rust语言在不断发展。一些语言结构(比如异步)仍然是脆弱的,你可能会发现坚持线程和标准库更好。

选择了Rust之后,情况到此为止

到目前为止,还没有出现与软件相关的宕机,这既让人吃惊,也证明了Rust提供的安全性。Rust也让我们很容易利用高效的资源写出高性能的代码——。我们的CPU和内存使用是可预测和一致的。因为没有垃圾收集器,所以我们可以保证一致的延迟和帧率。

我们维护Rust代码库的经验也很棒。在对我们的代码库做了大量更改后,我们可以安全地对延迟部分进行重大改进。干净的编译并不总是意味着一切都会正常工作,但说实话,这是很常见的。

我们最终的结果是一个可靠的产品,不需要噩梦般的维护工作,能够满足我们高标准的帧率、延时和资源效率。同样,很难想象没有Rust我们会是什么样子!

到目前为止,我们已经打开了一个FFI机箱,即web RTC-音频处理。这是我们过去库中最先进的板条箱之一,在开源的过程中类似的板条箱更多。

https://doc.rust-lang.org/nomicon/ffi.html

在未来,随着我们发布更多的代码,关于这个主题的内容会越来越多。但是有一点是一样的:在开源之前,我们私下创造的每一个箱子都是为开源做好准备的。这个想法使我们的板条箱之间的界限更加清晰,并鼓励我们更顺利地决定代码库的哪些部分应该是开源的。

https://blog.tonari.no/why-we-love-rust

标签: 代码 语言 板条