发布于 2026-01-05 4 阅读
0

我们从用 Rust 构建 SaaS 产品中学到了什么🦀

我们从用 Rust 构建 SaaS 产品中学到了什么🦀

在这篇文章中,我们不会回答每个人在开始一个新项目时都会问的问题:我应该用 Rust 来做吗?

相反,我们将探讨在自信地回答“当然! ”并开始主要使用 Rust 构建业务的旅程之后,我们遇到的陷阱和见解。

本文旨在对我们的经验进行概述,我们将在接下来的系列文章中深入探讨细节。

(请在评论区投票选出我们下一篇帖子的主题🗳️)


为什么生锈

为项目选择合适的语言从来都不是一个一成不变的决定。

简单介绍一下我们的团队和应用案例:

  • 我们是一个六人团队,几乎没有 Rust 开发经验,但拥有丰富的 Scala/Java 背景,擅长构建数据密集型应用程序。
  • 我们的 SaaS 是一个计费平台,非常注重分析、实时数据和可操作的见解(可以想象成 Stripe Billing 与 Profitwell 的结合体,再加上一点 Posthog 的功能)。
  • 我们的后端完全用 Rust 编写(分为 2 个模块和几个 worker),并通过 gRPC-web 与我们的 React 前端通信。

我们是开源的!
您可以在这里找到我们的代码仓库:https://github.com/meteroid-oss/meteroid
我们非常期待您的支持⭐和贡献

因此,我们有一些不容商榷的要求,而这些要求恰好与 Rust 非常契合:性能、安全性和并发性。Rust
几乎完全消除了与内存管理相关的各种 bug 和 CVE,而它的并发原语也相当吸引人(而且确实没有让我们失望)。

在 SaaS 中,所有这些功能在处理敏感或关键任务时都特别有价值,例如我们案例中的计量、发票计算和交付。

内存使用量的大幅降低也是构建可扩展和可持续平台的一大优势,许多大型企业(包括微软)最近也承认了这一点。

我来自一个充满戏剧性且有时充满毒性的 Scala 社区,而Rust友好包容的生态系统也对我产生了很大的吸引力,促使我去探索这片新领域。

怀着这些美好的愿望,让我们开始我们的旅程吧!


第一课:学习曲线是真实存在的。

学习 Rust 并不像学习其他编程语言那样简单。所有权、借用和生命周期等概念一开始可能会让人望而生畏,使原本简单的代码编写变得极其耗时。

尽管生态系统非常宜人(稍后会详细介绍),但你有时不可避免地需要编写底层代码。

例如,考虑一下我们 API 的一个相当基本的中间件(Tonic/Tower),它只是简单地报告计算持续时间:

impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for MetricService<S>
where
    S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = BoxError>
        + Clone + Send + 'static,
    S::Future: Send + 'static,
    ReqBody: Send,
{
    type Response = S::Response;
    type Error = BoxError;
    type Future = ResponseFuture<S::Future>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
        let clone = self.inner.clone();
        let mut inner = std::mem::replace(&mut self.inner, clone);
        let started_at = std::time::Instant::now();
        let sm = GrpcServiceMethod::extract(request.uri());

        let future = inner.call(request);

        ResponseFuture {
            future,
            started_at,
            sm,
        }
    }
}

#[pin_project]
pub struct ResponseFuture<F> {
    #[pin]
    future: F,
    started_at: Instant,
    sm: GrpcServiceMethod,
}

impl<F, ResBody> Future for ResponseFuture<F>
where
    F: Future<Output = Result<Response<ResBody>, BoxError>>,
{
    type Output = Result<Response<ResBody>, BoxError>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();
        let res = ready!(this.future.poll(cx));
        let finished_at = Instant::now();
        let delta = finished_at.duration_since(*this.started_at).as_millis();
        // this is the actual logic
        let (res, grpc_status_code) = (...) 

        crate::metric::record_call(
            GrpcKind::SERVER,
            this.sm.clone(),
            grpc_status_code,
            delta as u64,
        );

        Poll::Ready(res)
    }
}
Enter fullscreen mode Exit fullscreen mode

是的,除了泛型类型、泛型生命周期和 trait 约束之外,你最终还需要为一个简单的服务中间件编写自定义的 Future 实现。
请记住,这是一个略显极端的例子,旨在展示 Rust 生态系统中存在的不足之处。在很多情况下,Rust 最终可以像任何其他现代语言一样简洁高效。

学习曲线会因个人背景而异。如果你习惯了 JVM 处理繁重的工作,并且像我们一样使用过更成熟、更完善的生态系统,那么理解 Rust 独特的概念和范式可能需要花费更多精力。

然而,一旦你掌握了这些概念和基本原理,它们就会成为你强大的工具,即使偶尔需要编写一些样板代码或宏,也能显著提高你的工作效率。
值得一提的是,谷歌已在相当短的时间内成功地将团队从 Go 和 C++ 过渡到 Rust ,并取得了积极的成果。

为了降低学习难度,请考虑以下几点:

  • 请从头到尾阅读官方的Rust 书籍,不要跳过任何章节。这样理解这些复杂的概念就会容易得多。
  • 练习,练习,再练习!认真完成Rustlings 的练习,以建立肌肉记忆并培养 Rust 思维模式。
  • 加入Rust 社区吧!他们是一群非常棒的人,总是乐于助人。
  • 利用 GitHub 的搜索功能查找并学习其他项目。GitHub 生态系统仍在不断发展,与他人协作至关重要(但请注意许可证,并始终为项目做出贡献)。我们将在下一篇文章中探讨一些启发过我们的项目。

第二课:生态系统仍在发展成熟中

Rust 的底层生态系统非常出色,拥有众多设计精良、维护完善的库,并被社区广泛采用。这些库为构建高性能、高可靠性的系统奠定了坚实的基础。

然而,随着你向上移动到堆栈的更高层级,事情可能会变得稍微复杂一些。

例如,在数据库生态系统中,虽然存在像sqlx和 这样diesel的优秀关系型数据库库,但对于许多异步或非关系型数据库客户端来说,情况就复杂得多。这些领域的高质量库,即使被大型公司使用,通常也只有一位维护者,这会导致开发速度较慢,并带来潜在的维护风险。
对于分布式系统原语来说,挑战更为突出,因为您可能需要自行实现解决方案。

这并非 Rust 独有的情况,但与一些更古老/更成熟的语言相比,我们发现 Rust 经常会遇到这种情况。

从好的方面来看,Rust 的生态系统对安全问题反应迅速,补丁能够迅速传播,从而确保应用程序的稳定性和安全性。

到目前为止,Rust 开发相关的工具也相当出色。

我们将在以后的文章中深入探讨我们选择的库以及我们做出的决定。

生态系统在不断发展演变,社区积极努力填补空白并提供有效的解决方案。做好准备应对未知挑战,合理分配资源以协助维护工作,并回馈社区。


我有没有提到我们是开源的?

Meteroid是一个现代化的开源计费平台,专注于商业智能和可操作的洞察。

我们需要您的帮助!如果您有时间,

您的支持对我们意义重大❤️⭐️
请在GitHub上给我们点赞⭐️


第三课:文档就在代码里

深入了解 Rust 生态系统后,你会很快发现,相关的文档网站有时会显得有些……简略。
但别担心!真正的宝藏往往就隐藏在源代码中。

许多库都提供了非常完善的文档,代码注释中也包含丰富的示例。如有疑问,不妨深入研究源代码并进行探索。您通常会找到所需的答案,并对库的内部运作机制有更深入的了解。

虽然带有使用指南的外部文档仍然很重要,可以节省开发人员的时间和精力,但在 Rust 生态系统中,做好在必要时深入研究代码的准备至关重要。

像docs.rs这样的网站可以轻松地为公开的 Rust crate 提供基于代码的文档。或者,您也可以使用 cargo doc 在本地为所有依赖项生成文档。这种方法一开始可能会让人感到困惑,但花些时间学习如何使用这个系统,从长远来看会非常有效。

毋庸置疑,另一个有用的技巧是寻找示例(大多数库/examples在其代码仓库中都有一个文件夹)和其他使用该库的项目,并积极参与这些社区。这些示例和项目总能提供关于库正确使用方法的宝贵指导,并可作为您自身实现的起点。


第四课:不要追求完美

刚开始学习 Rust 时,人们很容易想要编写出最符合惯用语法且性能最佳的代码。
然而,大多数情况下,为了简洁性和生产力而做出一些取舍是可以接受的。

完成比完美更重要

例如,使用线程间clone()共享Arc数据可能并非最节省内存的方式,但它可以极大地简化代码并提高可读性。只要你意识到性能影响并做出明智的决策,优先考虑代码简洁性完全可以接受。

记住,过早优化是万恶之源。首先要专注于编写简洁、易于维护的代码,并在必要时再进行优化。不要尝试进行微优化 ¹(除非你真的需要)。Rust 的强类型系统和所有权模型已经为编写高效、安全的代码提供了坚实的基础。

当需要优化性能时,请关注关键路径,并使用诸如 `router` 和 `router` 之类的性能分析工具perfflamegraph识别代码中真正的性能瓶颈。如需全面了解这些工具和技术,我推荐《Rust 性能优化手册》


¹ 这适用于您创业历程的方方面面,包括融资。


第五课:错误其实也挺好的

Rust 的错误处理机制非常优雅,其Result类型和?运算符鼓励显式地进行错误处理和传播。然而,它不仅仅关注错误处理;它还关注提供清晰明了的错误信息以及可追踪的堆栈跟踪,而
无需编写大量样板代码来转换错误类型。

thiserror像 `std::error_type`或`std::error_type`anyhow这样的库snafu对于此目的来说非常宝贵。我们最终决定使用 `std::error_type` thiserror,它简化了创建带有信息丰富错误消息的自定义错误类型的过程。

在大多数 Rust 使用场景中,你并不太关心底层错误类型的堆栈跟踪,而是更喜欢将其直接映射到你的领域内的信息丰富的类型错误。

#[derive(Debug, Error)]
pub enum WebhookError {
    #[error("error comparing signatures")]
    SignatureComparisonFailed,
    #[error("error parsing timestamp")]
    BadHeader(#[from] ParseIntError),
    #[error("error comparing timestamps - over tolerance.")]
    BadTimestamp(i64),
    #[error("error parsing event object")]
    ParseFailed(#[from] serde_json::Error),
    #[error("error communicating with client : {0}")]
    ClientError(String),
}
Enter fullscreen mode Exit fullscreen mode

花时间编写清晰明了的错误信息,能够极大地提升开发者的体验,并简化调试过程。这虽是一项小小的投入,却能带来显著的长期收益。

然而,有时,尤其是在 SaaS 用例中,日志不在用户范围内,保留完整的错误链(可能还包括其他上下文)就很有意义了。

我们目前正在试验error-stackhash.dev 维护的一个库,它能够实现这一点,即附加额外的上下文并将其保留在整个错误树中。它作为底层组件使用效果很好thiserror

它提供了一个惯用的 API,实际上是将错误类型封装在一个 Report 数据结构中,该数据结构保存了所有错误、原因以及您可能添加的任何其他上下文的堆栈,从而在发生故障时提供大量信息。

我们遇到了一些小问题,但这篇文章已经太长了,更多内容将在后续文章中介绍!

总结

使用 Rust 构建我们的 SaaS 产品一直是一段旅程(现在仍然是)。起初是一段漫长而充满挑战的旅程,但同时也是一段充满乐趣和成就感的旅程。

  • 如果我们用 Scala 开发产品,速度会更快吗?
    当然会。

  • 它会同样有效吗?
    也许吧。

  • 我们还会像今天这样充满热情和兴奋吗?
    可能不会。

Rust 促使我们以不同的视角思考代码,拥抱新的编程范式,并不断追求进步。
诚然,Rust 也并非完美无缺。它的学习曲线可能比较陡峭,生态系统也仍在不断发展。但这正是它的魅力所在。
除了技术层面,Rust 社区也令人无比欣喜。友好的氛围、乐于助人的社区精神以及对这门语言的共同热情,都让这段学习之旅更加愉快。

所以,如果您有时间和意愿去探索一个新兴的、蓬勃发展的生态系统,如果您愿意接受挑战并从中学习,如果您需要高性能、高安全性和高并发性,那么Rust 可能就是适合您的语言

至于我们,我们很高兴能继续使用 Rust 构建我们的 SaaS 产品,不断学习和成长,并期待这段旅程将带我们走向何方。敬请期待更多深度文章,或者在第一条评论中投票告诉我们接下来应该写什么。

如果您喜欢这篇文章并觉得它对您有所帮助,请不要忘记给我们的代码库点个赞!您的支持对我们意义重大。

⭐️ 星状流星体 ⭐️

下次再见,祝你编程愉快!

文章来源:https://dev.to/meteroid/5-lessons-learned-building-our-saas-with-rust-1doj