我们从用 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)
}
}
是的,除了泛型类型、泛型生命周期和 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` 之类的性能分析工具perf来flamegraph识别代码中真正的性能瓶颈。如需全面了解这些工具和技术,我推荐《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),
}
花时间编写清晰明了的错误信息,能够极大地提升开发者的体验,并简化调试过程。这虽是一项小小的投入,却能带来显著的长期收益。
然而,有时,尤其是在 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

