三个Rust代码库的经验分享:何时开始使用Rust? - convex


什么时候是开始使用 Rust 的好时机?
Convex 的创始团队有幸领导了世界上一些最常用的基于 Rust 的系统的开发:

  • Magic Pocket,Dropbox 的地理分布式数据存储系统。该系统已在近百万个并发存储节点上运行,并直接管理 EB 的磁盘存储,所有这些都没有发生任何重大事件。
  • Nucleus,Dropbox 同步引擎的全新改写。该代码库在超过 50 亿台设备上运行,管理数万亿个文件,应对各种奇怪的最终用户文件系统配置,并具有无可挑剔的可靠性记录。
  • Convex,零设置、无限可扩展的后端,专为响应式应用程序开发人员的需求而设计。好的,这还不是最常用的代码库之一(目前)。这是我们现在正在建设的。

实际上,在 Rust 1.0 发布之前,我们在 2014 年就在积极使用它!该语言已经走了很长一段路,现在采用的障碍显着降低。然而,尽管我们在使用 Rust 方面有着非常积极的经验,但我们并不总是向每个开发团队推荐它。在这篇文章中,我们希望为 Rust 非常适合项目的条件提供一些参考。
 
为什么要切换?
如果您正在阅读本文,您可能不需要对 Rust 的优势有太多的说服力。Rust 一直是我们所从事的一些项目取得成功的重要贡献者。
  • 性能

像魔术师口袋里的项目在运行,认真大而昂贵的舰队服务器。我们在该项目中的目标之一是尽可能地降低成本开销,其中“开销”通常定义为“任何非硬盘驱动器”。切换到 Rust,由于缺乏运行时和对内存分配的严格控制而具有可预测的利用率,这使我们能够显着减少存储节点中的 CPU 和 RAM 量,而不会像我们在早期 Go 代码库中遇到的 OOM 问题的持续威胁。最终,Rust 在一个更安全、更符合人体工程学的开发环境中为我们提供了 C++ 级别的控制和效率。
  • 正确性

为数亿用户无缝迁移 EB 数据或同步状态是可怕的。像这样的项目对正确性的要求非常高。Rust 的强类型系统和借用检查器是用于开发正确的多线程代码的巨大的开箱即用资源。
Nucleus 项目大量利用代号为 Trinity 的测试框架,该框架确定性地验证了执行线程复杂交错的不变量。Rust 对期货的支持以及实现我们自己的对抗性调度程序的自由使这个项目成为可能,而在另一种语言中,尤其是具有语言运行时的语言,它会困难得多。
我们将继续推进 Convex 测试分布式和并发系统的最新技术。
  • 生产率

虽然 Rust确实有一个陡峭的学习曲线,但几乎所有与我们合作过的工程师都经历过使用它后生产力的提高。避免类型错误和数据竞争是复杂项目的巨大生产力提升。
Rust 库也往往具有极高的质量,Cargo 提供的构建系统使处理大型 单体仓库monorepos 变得轻而易举。我们承认,我们已经浪费了无数时间来处理 npm 中的构建系统异常或与 Bazel 搏斗,而 Cargo 根本不是这种情况。
用 Rust 编程的一个潜在的意想不到的好处是,它实际上让我们成为了更好的原型!过去,在用 Go 重写用于生产之前,我们已经用 Python 等语言构建了分布式系统的原型。拉斯特重构是如此容易得多,也使我们能够发展成原型完全成熟的生产系统,而无需重写。重构通常就像进行更改然后追踪编译器错误的尾部一样简单,直到它完成。Rust 拥有我们使用过的任何语言中最有用的编译器建议。
  • 多功能性

Rust 不依赖于语言运行时的事实使其成为需要嵌入其他系统的代码的绝佳选择。例如,Unicode 规范化是一个非常复杂且容易出错的过程。当我们需要跨 Python、Go 和 Rust 实现相同的规范化算法时,我们为开源 Rust 实现做出了贡献,并在所有三种语言中嵌入了相同的代码。
在其他语言中嵌入 Rust 很容易,但在 Rust 中嵌入其他语言也很容易。Convex 允许客户将数据库查询表达为成熟的 JavaScript/TypeScript 函数,这些函数在我们的 Rust 后端的 V8 隔离中执行。我们的团队对WASM以及 Rust 与 WASM 团队之间的密切联系感到特别兴奋。我们期待着在 Convex 中安全嵌入其他语言的未来机会。
 
什么是工作的最佳工具
大公司经常发生的争论之一是对一致性的渴望,还是让工程师使用最好的工具来完成工作。想要使用可以让您个人更有效率的语言或加入热门新事物是很容易的。不幸的是,这通常以组织成本为代价。本地决策可能对工程产生广泛的影响,通常您的代码在您离开后仍然存在很长时间。可维护性通常也比大型项目的初始开发工作更重要。这种紧张关系一直是我们团队对以前项目的争论的根源。
  • 开发者可替代性

Rust 是一门难学的语言。根据我们的经验,开发人员通常可以在 Python、Go、Java 等之间跳转,并通过模式匹配他们的成功之路,即使他们不精通该语言。开发人员第一次与 Rust 中的借用检查器发生冲突或不得不对 trait 对象进行推理时,需要一些实际的专门学习时间。
入门问题的一个诱人替代方案是分拆一个由崭露头角的 Rust 专家组成的小型子团队,负责处理需要 Rust 性能或类型安全级别的关键任务组件。这是我们在 Magic Pocket 项目中犯的一个错误,少数工程师用 Rust 重写了存储引擎,而团队的其他成员继续用 Go 构建系统的其余部分。当 Rust 组件是一个小型 skunkworks 项目时,这种拆分是必要的,但它在此后持续了多年。
Rust 组件是他们自己成功的牺牲品——它们运行良好,不需要太多维护,当最初的作者离开团队去开发 Nucleus 时,已经没有多少工程师留下来成为这些方面的专家——大型系统。这在一段时间内减缓了 Rust 组件的创新,并让剩下的团队对 Rust 的使用感到复杂。
当我们开始 Convex 时,我们发誓不再重复这个错误,并确保所有工程师都在 Rust 上工作并掌握该语言。
  • 作为组织负担的库包支持

有了 Nucleus(Dropbox 的下一个 Rust 主要项目),我们确保整个团队都流畅并积极地为代码库做出贡献。在 Nucleus 团队中,Rust 取得了明确的成功。桌面客户端与后端代码库相对隔离,开发团队已经习惯了与多种语言的互操作。
在项目的边界上,事情稍微复杂一些。大公司有数百个相互通信的内部系统:监控、RPC 框架、发布过程、身份验证等。工程师需要将这些现有子系统移植到每种新的支持语言。Nucleus 的原型 API 服务器 Tomahawk 最初是用 Rust 重写的。Nucleus 团队承担了在 Tomahawk 内部实施内部系统支持的重担。最终,移植整个生态系统的工作量太大,所以我们用 Go 重写了 Tomahawk,Go 是公司事实上的后端语言。有时,移民的障碍实在是太高了。
在 Convex,我们可以从一开始就让事情变得干净:所有后端代码都在 Rust 中;所有前端代码都在 Typescript 中;操作工具主要使用 Python。作为一家初创公司,我们需要专注于完成工作,而不是支持多种语言。
  • 迁移后

也许引入新语言的最大缺点之一不是它需要在新代码库上做更多的工作,而是它减少了对现有代码库的投资。我们将 Dropbox 的后端堆栈从 Python 切换到 Go,然后将一些备受瞩目的项目从 Go 切换到 Rust。每次我们在性能、类型安全和生产力方面都获得了很多。然而,随着高级工程师转向这些令人兴奋的新项目,在现有代码库上完成的前沿工作越来越少,因此对库的持续投资也越来越少。
当一个团队对另一个团队提出功能请求时,他们自己介入并进行更改也很常见。通过简单的代码审查而不是一系列 Jira 任务来协调更改通常要快得多,而且希望该功能的团队投入时间来实现它是公平的。当团队之间存在(编程)语言障碍时,这是一个重大挑战。
初创公司不能在团队之间建立人为的障碍。我们在 Convex 做出了有意识的努力,使代码库对所有开发人员来说尽可能平易近人,并确保所有开发人员都拥有足够的专业知识,可以在需要工作的任何地方工作。
 
引入Rust注意点
我们是 Rust 的大力倡导者,并从该语言中受益匪浅,但必须谨慎地将新语言引入组织。

对于新团队

  • 首先决定你是否真的想使用 Rust。你会从类型安全和性能中受益吗?惊人的。您是否主要是一个前端商店,其 IO 绑定的工作负载无论如何都会执行大致相同的工作?也许你最好使用像 Go、Java 或 Kotlin 这样的垃圾收集语言。
  • 接受会有加速的时间。不管人们告诉你什么,像 Rust 这样的语言需要相当长的时间才能变得富有成效。确保你的团队中已经有一些专家可以帮助其他人提升而不是同时放慢整个团队的速度。当没有现有水平的 Rust 专业知识作为指导时,我们已经看到项目进展不佳。
  • 决定您将拥有多少种语言,并负责长期支持它们。意识到以多种语言维护库和操作系统的开销。
  • 愿意说不。有时,正确的解决方案就是凑合你已经拥有的东西。

对于现有团队
  • 决定是否值得重写或使用新语言。仅仅重构现有的代码库就足够了吗?您是否确定了真正重要的热点并尝试优化它们?您对您想要进行的更改为客户和开发人员带来的好处有信心吗?
  • 从一个不在关键开发路径上的较小的子项目开始。这将使您能够灵活地推迟时间表并适应意外的互操作性问题。
  • 一旦较小的项目取得成功,请确保整个团队都在努力——您不希望专业知识分散在少数工程师之间。
  • 如果一切顺利,承诺在整个公司更广泛地传播这种语言。
  • 承担建立邻近系统支持的个人责任。仅仅告诉监控团队为您构建一套全新的客户端绑定可能是不行的——您可能必须自己引导这项工作。
  • 确保核心系统的持续开发。核心系统需要一支工程师团队,他们能够在出现问题时进行创新或干预。即使没有紧迫的功能需求,也总是有改进的地方。