编译 | 言征
去年,Web开发公司MAInmatter对Web版 Rust 进行了战略押注,并发起了 EuroRust 会议,加入了 Rust 基金会,同时正在内部以及开源领域从事许多 Rust 项目。
Mainmatter非常乐观地认为 Rust 将在未来几个月和几年内在Web和云空间中起飞,并认为Rust 是迈向 Web 开发新时代的第一步,开发人员可以利用这项技术,在不放弃开发人员经验和生产力的情况下,达到更高的、以前难以想象的效率、稳定性、可靠性和可维护性水平。
这篇文章意在分享为什么Mainmatter有信心作这一押注,以及为什么我们相信 Rust 在Web和云领域拥有美好的未来。
Rust 自从大约十年前登台以来,就受到了很多开发人员的关注和喜爱。不仅开发人员喜欢这门语言,大公司的决策者也认为 Rust 是一项伟大的技术,并且在过去几年里,该语言在整个行业得到了广泛采用。
AWS在其平台上大量使用 Rust ,google在 Android 中使用它,Microsoft在 windows 中使用它。从本质上讲,Rust 有望在以前使用的许多领域取代 C 和 C++:系统编程、操作系统、各种嵌入式系统、低级工具以及游戏和游戏引擎。
当然,除了以上这些,未来具有更大潜力的领域是Web和云。Rust给这两个领域带来了无限想象的后端提升空间。一旦后端的开发提升到一个新的水平,就能让团队能够访问以前无法实现的功能。
尽管 Rust 还很年轻,但已经看到很多公司在Web和云中成功使用 Rust,比如:Truelayer、Discord、Temporal、Nando's、svix、Wingback等等。
值得一提的是,谷歌多年来也一直大力采用 Rust,最近表示,与他们使用的任何其他语言相比,他们并没有真正看到 Rust 的生产力损失。
虽然相对年轻,毕竟距离1.0 发布,Rust的生态系统也只走过了 8 年。但Rust以及其Web生态已经达到了一定的成熟度,足够使其成为构建真实应用程序的可行选择。
正如arewewebyet.org所证实的,Rust 显然已经为Web做好了准备:
首先,有tokio,一个异步运行时,它是Web应用程序的坚实且高性能的基础;其次,最重要的是,Rust已经有了成熟且维护良好的 Web 框架,例如axum和actix-web。所有相关数据存储以及 ORM 都有成熟的驱动程序;最后,可以找到涵盖构建 Web 应用程序的所有其他相关方面的库,例如(反)序列化、国际化、模板化、可观察性等。
总的来说,Rust的雄心勃勃,构建 Web 后端提供了坚实而稳定的构建块。
当然,有人可能会问:我为什么要换Rust?对于已经使用 Ruby、JAVA、Elixir、TypeScript、Go 或其他任何语言的团队而言,换 Rust有哪些好处吗?
有两个关键方面使 Rust 成为 Web 构建的绝佳选择:一是它的效率和性能;二是其类型系统带来的可靠性和可维护性带来的好处。
1.效率和性能
Rust 以其高效和高性能而闻名。它将轻松超越 Web 应用程序常用的 JavaScript、Ruby、Python/ target=_blank class=infotextkey>Python 等语言几个数量级。其他语言可能具有更高的性能上限(例如 Java 或 C# 或 Go),但你需要投入大量的工程精力才能接近 Rust 工具包开箱即用提供的性能水平。
此外,Rust 还有一个关键优势:它不捆绑垃圾收集器。垃圾收集语言可以很快,但它们不能始终一致性地表现出色。垃圾收集器将引入暂停(pause)以释放未使用的内存,从而对应用程序的尾部延迟产生负面影响。而Rust 不存在这个问题:它可以提供一致的性能,而不会出现这些峰值。
C 和 C++ 是唯一能够实现如此稳定和一致性能的其他语言。不幸的是,这两种语言往往搬起石头砸自己脚,处处是陷阱,特别是在手动内存管理时。正如 linux 的创始人 Linus Torvalds 所说:
“它离硬件太近了,你可以用它做任何事情。这很危险。就像玩杂耍电锯一样。我还发现它确实有很多陷阱,而且很容易被忽视。
由于 C 和 C++ 的这些危险,除了这两种语言的专家或拥有专家团队时才能使用。否则,你得到的就是一个不稳定且充满安全漏洞的系统。
同时,别忘了,在 Web 领域,很少人具备这种专业知识,因为每个人大多都使用非常不同的语言,如 JavaScript、Python、Ruby、Elixir 等。反而Rust 就不会遇到同样的陷阱,使开发人员能够以以前的效率水平构建软件。
Rust 通常会比用于构建 Web 后端的其他技术的性能好几个数量级,同时保持显着较低的内存占用。
当然,如果与其他技术相比, Rust Web 服务器可以在一小部分时间内响应请求,这也意味着它可以用更少的服务器响应相同数量的请求,这又意味着更少的托管成本。
这对于中小型产品和公司来说,减少托管的云服务器数量,就意味每月就可以轻松减少不菲的费用。
我们的 Python 服务平均约为 50 个请求/秒,NodeJS 约为 100 个请求/秒,Rust 约为 690 个请求/秒。我们可以在通常托管单个 Python 服务的 k8 EKS 节点上安装 4 个 Rust 服务。——Reddit某用户
然而,成本节省还只是好处之一,使用更少的服务器也意味着使用更少的能源。尽管使用可再生能源运行数据中心固然很好,但最绿色的能源仍然是我们不使用的能源。Rust 或许不能解决气候危机,但这里要承认的是,运行我们编写的软件,也会真真切切地消耗资源,从而对现实世界产生影响。软件行业往往会忘记这一点——如果我们能够更有效地利用资源,并以更少的投入获得相同的产出,这是选择技术时的一个重要考虑。
2.可靠性和可维护性
虽然性能和效率很重要,但在许多情况下,可能更相关的原因则是 Rust 的强类型系统所带来的可靠性和可维护性收益。
像这样的代码片段对于 Web 应用程序(Ruby on Rails)来说是相当典型的:
复制
class User
attr :name
attr :active
attr :activation_date
def activate(activation_date)
self.active = true
self.activation_date = activation_date
save
end
def save
…
end
end
…
user.activate(Time.now)
虽然这段代码非常简洁且易于阅读,并且编写这样的代码可以让你快速实现目标,但也存在问题。在这个的示例中,虽然我们可以看到用户的属性,但我们不知道这些属性周围可能有什么规则(例如,如果active是true,则activation_date可能也必须设置?如果active是false,则大概activation_date应该是nil?)。为了验证这些假设,我们必须研究该activate方法的实现,这意味着需要付出相对较高的努力才能获取信息。
查看该activate方法的调用,我们无法知道它是否会引发错误,或者我们应该在哪个时区中度过时间。虽然 Ruby 可能有点极端,但考虑到其众所周知的灵活性,许多这些问题在其他语言中也存在。让我们以 Java 为例。我们仍然无法在类型系统中对围绕active和activation_date属性的规则进行编码,即使可以null,我们也有NullPointerException在运行时获取 s 的风险。
随着代码库的增长和开发团队的壮大,或者只是随着一些人离开和加入而发生变化。从事代码库工作的每个人都对整个应用程序以及整个代码库中所做的所有隐式假设都有一个完美的心智模型,但这很难做到,相反,理解这些概念需要人们认真阅读遗留代码。这不仅降低了效率,而且还可能导致生产中的错误率增加。
与上面相同的代码片段,但在 Rust 中更加清晰和富有表现力:
复制
enum User {
Inactive {
name: String,
},
Active {
name: String,
active: bool,
activation_date: DateTime<Utc>,
},
}
impl User {
fn activate(&self, activation_date: DateTime<Utc>) -> Result<(), DBError> {
match self {
User::Inactive { name } => {
let new_user = User::Active {
name: name.clone(),
active: true,
activation_date: activation_date,
};
new_user.save()
}
User::Active { .. } => Err(Error::default()),
}
}
fn save(&self) -> Result<(), DBError> {
…
}
}
首先,对于用户模型,我们可以使用 Rust 的enum关联数据。这样,就可以完全清楚非活跃用户和活跃用户是什么样子,以及在什么场景下可以设置哪些属性——事实上,活跃用户和非活跃用户甚至不具有相同的属性,但每个用户都只具有对其有意义的属性。它们代表各自的用户状态。此外,属性的类型也被明确定义——不仅 Rust 是类型化的,而 Ruby 显然是非类型化的,而且类型也非常精确,例如对于字段,activation_date预期的时区在类型中也是正确的。
该函数的签名activate还显式地编码了 Rails 示例中隐含的许多信息。同样,预期的时区activation_date在类型中是正确的,并且该函数返回Rust 的时区Result,这清楚地表明调用它时可能会发生错误。Result事实上,Rust 编译器将要求处理的成功和错误变体,以便不会发生未处理的运行时异常。
此外,activation_date当调用函数时,函数的参数总是保证有一个值,因为 Rust 没有隐式可空性的概念(与 Java 不同)。如果activation_date 可能在其计算位置没有值,则它可能无法Option<DateTime<Utc>>传递给函数activate,因为它的类型与预期的不同DateTime<Utc>。Rust 编译器只允许Some其变体的代码路径Option导致方法的调用activate,以便activation_date保证在函数运行时有一个值。
虽然这显然是一个相当简单的示例,但它很好地说明了 Rust 的两个主要优点:
(1)Rails 示例中隐含的许多概念和规则都是通过 Rust 代码中的类型显式传达的。可以清楚地区分活跃用户和非活跃用户,对于日期字段,甚至预期的时区也被编码在类型中。这种表现力使代码更容易理解,特别是对于代码库的新手来说,从而提高了可维护性。
(2)Rust 还大大提高了可靠性,因为其他语言(包括 Java 或 Go 等类型化语言)中常见的整类错误将在编译时而不是运行时检测到。编译器保证函数activation_date的参数activate具有值以及要处理的函数可能返回的任何错误。
总体而言,当每个人都关注 Rust 的性能时,Rust 带来的可靠性和可维护性方面的改进常常被忽视。然而,这些好处对于项目的长期成功可能比纯粹的绩效数字更相关。
由于 Rust 的主要优点是可靠性、可维护性、效率和性能,因此该语言的用例显然是与这四个方面特别相关的用例。但是,好处的代价是需要考虑在内。
总体而言,Rust 仍然需要比其他技术更高的前期投资,特别是与 Web 项目中常用的技术相比:
虽然像 JavaScript 和 Ruby 这样的语言是为了快速获得结果而设计的,但 Rust 则没有留下太多的自由度,并且要求程序在获得工作结果之前通过所有编译器的检查。与这些语言相比,使用 Rust 就需要付出更多的初始工作。此外,人们在使用 Rust 之前还需要翻越一座山——那就是掌握 Rust 独特的所有权系统。
然而,当跨过项目的初始阶段并将视野扩展到更长的时间范围时,可维护性、可靠性和稳定性等方面变得极其重要,一开始使用 Rust 时进行的额外投资会随着时间的推移而带来回报——
Rust 应用程序更可靠,因此需要更少的时间投入到错误修复上,并且更易于维护,因此更容易与不断增长和变化的团队一起有效地工作。
最后就会呈现出:Rust工作量先大后小,先苦后甜。对于其他语言来说,情况往往是相反的:随着时间的推移,随着团队的成长,可靠性和可维护性挑战的影响变得更大、成本更高,工作量也会增加。
根据 Rust 的优势和投入曲线,每当评估是否针对特定情况选择 Rust 时,需要回答的主要问题是:
(1)团队是否已经具备 Rust 专业知识(许多不使用 Rust 的团队实际上已经拥有专业知识,因为很多开发人员在空闲时间使用 Rust 编写代码)?
(2)可靠性方面有哪些要求?
(3)长期维护计划是什么?
(4)系统构建的规模有多大,Rust 在托管方面可以节省多少钱?
(5)根据以上问题的答案,额外的初始投资值得吗?
虽然在某些情况下,结论是额外的初始投资不值得,但在某些情况下,评估显然对 Rust 有利。我们看到的一些典型用例包括:
(1)对于实现产品关键业务逻辑的核心业务系统来说,可靠性、长期可维护性等方面是首要考虑的问题。
(2)对于金融系统来说,通常对错误的容忍度很低,而 Rust 带来的稳定性的提高可能是一个决定性因素。另外,性能是一项关键要求,在特定场景(例如交易系统)中具有明显的财务影响。
(3)任何必须能够提供高吞吐量和性能的系统显然都会从 Rust 中受益。位于多个微服务前面的代理服务器等系统必须具有最小的开销和一致的性能。在这些情况下,垃圾收集语言及其不可靠的性能特征通常不是一个选择。
(4)最后,对于任何大规模运行的系统,在托管成本方面都有很大的节省潜力。
一旦做出了使用 Rust 的决定,就有两种主要的采用路径——要么用 Rust 从头开始(重新)编写整个应用程序,要么考虑与其他技术一起逐步采用。篇幅原因,就不再展开了。
原文链接:https://mainmatter.com/blog/2023/08/14/the-case-for-rust-on-the-web/#why