在生产环境中使用Rust 一年之后感想

文摘   2024-09-27 16:23   美国  

在过去的几个月里,我几乎所有的空闲时间都在为我的副业 -  JustFax[1]  工作。这一切都始于从LemonSqueezy迁移到Stripe(想知道为什么吗?订阅我 另一个博客[2] 的通讯,因为我计划详细分析为什么你可能想选择支付网关而不是商户记录)。但就像每次重构或重写一样,它的规模远远超出了我的预期。一个简单的支付提供商变更要求我在SQL之上实现一个作业处理队列,以及做一个小型会计系统。当然,全部用Rust实现。这导致了我合并过的最大的合并请求之一。

这个合并请求来得正是时候,它提醒我大约一年前(也许是11个月前)开始了JustFax。此外,我在$MAIN_JOB中也在生产环境中使用Rust,所以我决定概述一下在生产环境中使用Rust是什么感觉。

如果编译通过,就能运行

我在 最初用Rust编写Web应用程序的印象[3] 中简单提到过这一点,我想再次强调一下。

通常情况下,我不会在每次修改一行代码后就运行代码(除非我在做烦人的CSS调整)。大多数时候,我会完成一大块重构或功能实现,然后运行所有内容进行测试。此外,使用Rust,你无法运行不能编译的代码,所以如果你的代码有错误,你需要修复它们,这与JavaScript不同。因此,我被重构工作带走了,这跨越了无数个夜晚,持续了几个月。一件事接着一件事,代码一直处于无法编译的状态,直到最后一刻一切都完成了。

我很害怕运行所有内容,但在设置好系统的每个组件并发出第一个(本地)请求后,我惊讶地发现一切都能正常工作!我知道我是一个优秀的开发人员,但我不会自欺欺人地认为我可以写出没有错误的代码。除了代码动态方面的一些小问题,比如错误的序列化/反序列化格式,代码运行得很完美。

这种成功的第一层是Rust的类型安全和编译特性。你不能意外地将i64赋值给uuid,Rust编译器不会让你这么做。你也不能像JavaScript那样访问结构体中不存在的字段。当然,有人声称从未遇到过cannot access property "foo" of undefined这样的运行时错误,但我和我同事的经历并非如此。我们在$MAIN_JOB中使用Rust的原因之一,是因为现有的用动态语言编写的后端变得难以管理。当然,你可以在头脑中保持一个小应用程序的上下文,但在某个时候它会超出你的记忆范围。我记得在以前使用PHP的日子里,到了某个时候我们不得不在屏幕上转储数组的内容,以便知道它有哪些键,因为有些键是snake_case,而其他的是camelCase,等等。

第二层,它某种程度上是第一层的延伸,是Rust社区对类型安全的痴迷。这是一种好的痴迷,因为它催生了像 sqlx[4] 这样的工具 - 一个编译时、类型安全的SQL包装器,它会针对真实的数据库运行你的查询,如果你的查询在语法上有错误,或者你试图将i32插入到TEXT列中而没有明确转换,它就不会让你的代码编译。我曾经害怕SQL,因为它通常是每个应用程序的重要组成部分,而且很容易出错。拼错一个查询,或者搞乱参数,在生产环境中你就麻烦了。许多人会为SQL查询编写单元测试,而其他人则会求助于使用ORM或从你的模式生成代码。但有了sqlx,我不再需要担心。我获得了SQL的全部功能,而不需要使用高度抽象的ORM,也不需要从数据库模式生成代码。当然,也有缺点,我会在博文的后面提到一些。

我可以把另一组工具放在这个类别中,那就是像 askama[5]maud[6] 这样的类型安全模板引擎。虽然我尝试过它们,但它们并没有在我的项目中找到位置,不过它们确实从你的代码中消除了另一个不可预测的元素。让我发笑的事情之一是,当我收到一封以Hello {{userName}}开头的电子邮件时。从一个无名服务收到这样的邮件很有趣;从银行收到这样的邮件则很尴尬。使用类型安全的编译模板,这种错误是不存在的。

我唯一怀念的是类型安全的第三方API接口。当然,你可以从OpenAPI/Swagger规范生成OpenAPI客户端,但你仍然受制于第三方API提供商,而且他们有规范并不一定意味着他们会遵循它并且不会破坏他们的API,所以我不确定我们能解决这个问题。

当它运行时,它非常稳定

我从未经历过Rust进程崩溃。我确实经历过Node进程崩溃。除非你在代码中滥用.unwrap()(这基本上是说"如果结果是错误,就崩溃"),否则你的进程很可能永远不会崩溃。我在代码中有几个.unwrap()调用,主要是在初始化期间,所以如果配置文件丢失,或者环境变量未定义,我除了崩溃进程外别无选择。但总的来说,Rust要求你明确处理错误。所有返回Result<Something, Error>的东西,都要求你通过?运算符将错误冒泡,或者对其执行match语句。在大多数情况下这是有意义的。在某些情况下,它有点烦人(例如,当你知道从i64i32的转换会成功,因为它包含的数字完全在i32的边界内,你仍然需要做try_from(),它返回一个Result)。但即使在烦人的情况下,我也倾向于不使用.unwrap(),而宁愿记录一个警告并返回一些合理的默认值。编程是不可预测的,所以至少这样我的程序可以继续运行,同时记录我可能愚蠢的位置。

JustFax不仅仅是Rust,它还有用TypeScript和其他JS框架编写的内部系统。我发誓每次我创建一个新的TypeScript项目时,总有一些东西会改变。要么某个工具的配置文件发生了变化(看着你,eslint);要么typescript有了新的样板代码;要么express不再酷了,现在每个人都使用fastify;要么express 又酷了[7] ;要么ts-node不好,现在我们使用tsx;要么这个模块只支持esm,而那个是cjs,所以你自己想办法吧。TypeScript总是有一些问题。你工作的方式,你lint代码的方式,原生JS中工作空间的工作方式与 TypeScript[8] 相比。

但Rust不是这样。cargo init - 砰,你得到了一个新项目。想要工作空间?没问题,完美运行。代码检查?clippy为你服务。它就是如此少的样板代码和精神疲劳。

编译时间仍然是个痛点

毫无疑问,Rust最大的缺点是它的编译时间。特别是当你开始使用像sqlxmaud这样严重依赖宏的工具时,开发过程中的LSP和编译器都开始变得吃力。随着项目的增长,我收集了更多的第三方依赖。虽然我试图通过将多个包之间的共同依赖放在根Cargo.toml中,以及积极地只选择需要的依赖特性来优化它们,但我仍然在编译时间上挣扎。我提到过初始编译在CI/CD中花了大约6分钟,现在需要大约20分钟。根据网上的不同博客,可以对其进行优化,但这需要一个类似外科手术的程序,在多阶段docker容器中积极缓存依赖。我现在没有时间做这个,需要更多地关注业务方面,但我以后会处理的。

在Mac M2上的本地编译是可以管理的,特别是使用增量构建,但它的代价是存储空间。我偶尔运行cargo clean,这会导致数十GB的缓存被删除。然而,即使使用增量编译,它仍然远远不及你在JS世界中获得的即时热重载。所以回到我的第一点,一个"改变一小段代码,立即测试"的开发周期有点被打破了。你无法获得像JS/TS那样的即时反馈,这就是为什么JustFax的一些内部和外部工具仍然用TypeScript编写的原因。Rust某种程度上引导你走向"写大量代码,编译,检查"的流程,而不是Node/JavaScript中的"改变一行,alt-tab浏览器"工作流。我大多数情况下都可以接受。我得到了很多回报,比如真正的类型安全。我相信CI管道可以修复,我只是需要更多的时间来处理它。

Rust在某些方面表现出色,比如纯后端。像axum这样的框架提供了构建API服务器所需的许多部分,大多数(流行的)外部API在Rust中都有客户端。当然,它们通常不是由构建这些API的公司维护的,但至少我们可以使用它们。然而,网上的大多数例子都不包括Rust。所以你只能自己想办法如何与这个或那个API集成。

这是Rust开发中一个反复出现的主题,至少对于Web来说是这样,我无法评论其他类型的应用程序。我经常发现自己在阅读源代码,或者在GitHub问题中寻找与我遇到的问题类似的问题。LLM很少能提供适当的解决方案,因为大多数包都有点小众。Rust社区非常注重他们包的文档和示例,这很棒,但不可避免地你会遇到一些既没有文档也没有示例的边缘情况,很可能你将不得不求助于阅读源代码来弄清楚。

有些应用程序不太适合Rust,特别是如果你来自快速原型环境。我仍然更喜欢使用Astro或Svelte用TypeScript编写前端。Rust的开发体验在这方面就不太好。由于每次变量更改都需要重新编译代码,这使得前端开发的快速迭代变得太慢。

总之,我很高兴一年前选择了Rust。它不仅帮助我获得了一份我真正喜欢的$MAIN_JOB,还帮助我构建了更好的软件。我期待着第二年的Rust开发。

参考链接

  1. JustFax: https://justfax.online/
  2. 另一个博客: https://thesolopreneur.blog/
  3. 最初用Rust编写Web应用程序的印象: https://yieldcode.blog/post/building-a-webapp-in-rust/
  4. sqlx: https://github.com/launchbadge/sqlx
  5. askama: https://github.com/djc/askama
  6. maud: https://maud.lambda.xyz/
  7. 又酷了: https://github.com/expressjs/express/blob/5.0/History.md
  8. TypeScript: https://yieldcode.blog/post/npm-workspaces/

幻想发生器
图解技术本质
 最新文章