为什么后端开发应该使用Rust
随着Rust被评为Stack Overflow 2024调查中最受欢迎的语言,越来越多的开发者开始学习Rust。尽管Rust是一种多功能的语言,但许多新的Rust开发者特别喜欢用它来进行后端Web开发。在本文中,我们将讨论为什么Rust相比其他语言更适合后端Web开发。
Contents
Rust Web服务是什么样的?
在我们深入探讨Rust在后端的优势之前,让我们先看看Rust Web服务是什么样的——以Axum作为我们的Web框架为参考。
下面是一个将sqlx
的数据库连接池添加到共享应用程序状态的示例。我们将在路由器中使用它,并设立一个端点来获取数据库记录并返回JSON响应:
use sqlx::PgPool;
use axum::{Router, routing::get};
use tokio::net::TcpListener;
// 我们在这里自动实现Clone特征,以便框架在请求时可以克隆这个结构体
#[derive(Clone)]
struct AppState {
db: PgPool
}
#[tokio::main]
async fn main -> Result<(), Box<dyn std::error::Error> {
// 这假设我们已经设置了数据库连接函数
// 使用`?`运算符允许我们自动传播错误
let db = connect_to_db().await?;
let state = AppState { db };
// 在这里设置路由
let router = Router::new()
.route("/users", get(get_users))
.with_state(state);
let tcp_listener = TcpListener::bind("localhost:8000").await?;
axum::serve(tcp_listener, router).await?;
Ok(())
}
我们还会添加我们的端点代码如下:
use serde::Serialize;
use axum::{extract::State, response::IntoResponse};
// 在这里我们自动派生`sqlx`的FromRow特征
// 和`serde`的Serialize特征,以便将结构体序列化为JSON
// 以及自动将检索到的Postgres行转换为我们的结构体
#[derive(sqlx::FromRow, Serialize)]
struct User {
id: i32,
name: String,
email: String,
}
// 在这里,我们将函数设置为返回实现了IntoResponse的非具体类型
async fn get_users(State(state): State<AppState>) -> impl IntoResponse {
let query = sqlx::query_as::<_, User>("SELECT * FROM users")
.fetch_all(&state.db)
.await
.unwrap();
Ok(Json(query))
}
虽然语法可能有些不熟悉,但惯用的Rust代码相对容易理解。虽然我们在主函数中设置了应用程序逻辑的核心,但将其提取到另一个函数或本地crate包中以便于测试是很容易的。
Rust的优势
低内存占用
Rust的借用检查器系统允许在运行时大大降低内存占用。在编译时进行的优化不仅使内存占用较小,还提供了内存安全保证。这使得Rust成为公司(和个人!)在提高绿色环保信誉和降低运行成本方面的绝佳选择。更快的执行速度也可以降低无服务器函数的成本!
这对于Web服务比其他类型的应用程序更重要。如果你不是在VPS上运行Web服务,那么很可能会为计算和CPU使用付费。能够使用更高效的语言编写后端可以显著降低长期成本,尤其是对于那些可能从Java、Python或Ruby等语言转换过来的大公司。使用VPS时,这意味着你可能可以缩小所使用的虚拟机,尽管存在一定的约束。
这一优势的另一个主要结果是,在物联网设备上运行Rust Web服务器变得更加容易。例如,在Raspberry Pi上运行Actix Web服务器。技术上,你可以用Django、Flask甚至Ruby on Rails来做到这一点。然而,随着应用程序的规模变大,可能没有太多空间运行其他东西。至少,你可以在你的小机器上再运行几个程序,这取决于你的用例,可能非常有用。
当然,将这么多前置到编译器确实会增加编译时间。即便如此,你的应用程序通常会在生产中运行更长时间。使用crane和mold linker也可以显著减少编译时间。虽然首次编译可能需要几分钟,但通常在随后的编译中,时间会少得多。
并发
并发一直是一个难以解决的问题。设计语言时为了抽象复杂性,往往会导致并发问题。例如,Python的全局解释器锁(GIL)在开发和运行C扩展时无疑带来了好处。然而,它也显著增加了多线程应用程序的复杂性,主要通过锁获取和线程争用。因此,并行性更难以表达。
相比之下,Rust不会隐藏编写并发程序的复杂性。这使得入门有些令人望而生畏。然而,目前的生态系统显著扩展了标准库原语,提供了更易用的抽象,使得大规模处理并发变得更加容易。例如:dashmap,一个并发的HashMap
实现,以及parking-lot,主要提供线程的停车和启动,并为互斥锁和其他同步原语提供死锁检测。
如果你使用Tokio运行时进行异步Rust开发,使用Tokio的同步原语也很容易,这些原语可以跨线程使用。如果你想在Rust后端服务中实现基本的并发而无需全面投入,利用这些原语是一个很好的方法:
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
#[tokio::main]
async fn main() {
let locked_map: Arc<RwLock<HashMap<String, String>>> = Arc::new(RwLock::new(HashMap::new()));
let mut writer = locked_map.write().await.unwrap();
writer.insert("Hello".to_string(), "world!".to_string());
// 手动释放Writer锁以便我们能重新获得访问权限
drop(writer);
let reader = locked_map.read().await.unwrap();
println!("{}", reader.get("Hello".to_string()));
}
Rust与其他语言的区别在于实现细节。由于Rust的类型系统(例如,RwLocks和Mutexes封装类型),同步原语不会锁定线程——它们锁定资源。这是一个重要的细节,因为它仍然允许其他事情运行。锁也是有范围的:一旦函数运行完并且超出范围,锁会自动释放并销毁,允许你再次锁定资源。在C++等语言中,互斥锁本身是一个资源,会锁定整个线程,这本身可能导致问题。一个简单的例子是忘记解锁互斥锁,或者锁定顺序不正确。当然,这并不意味着Rust免疫于竞争条件——资源仍然可能因尝试同时访问而被锁定,以及锁中毒等问题。
有些编程语言专门用于处理并发:Erlang和BEAM(以及可以在BEAM虚拟机上运行的其他语言)就是一个例子。虽然Rust并没有在每一个语言设计选择中都嵌入并发,但如果你想在Rust后端Web服务中完全实现并发,你会得到能够安全高效地做到这一点的工具。
内存安全
今年早些时候,白宫倡导使用内存安全语言,这对Rust来说是一个巨大的胜利。虽然Rust是讨论中的几种语言之一,但信息基本相同:远离那些没有足够保护措施防止引入内存错误的语言,无论是内存访问违规、泄漏还是简单的使用后释放(use-after-free)。
Rust并不是完全免疫于内存问题。然而,它确实提供了足够的保护措施,以至于你需要故意使用unsafe
标记来绕过编译器强制的内存安全保证。例如,这段代码是 Vector 类型(Vec
)中删除给定索引元素的方法:
pub fn remove(&mut self, index: usize) -> T {
// 注意:这里 `<` 是为了防止所有内容之后再删除
assert!(index < self.len, "index out of bounds");
unsafe {
self.len -= 1;
let result = ptr::read(self.ptr.as_ptr().add(index));
ptr::copy(
self.ptr.as_ptr().add(index + 1),
self.ptr.as_ptr().add(index),
self.len - index,
);
result
}
}
注意,该函数是“安全”使用的,因为它没有标记为unsafe,但有一个unsafe块。这意味着代码的健全性由库来保证,而不是用户!
虽然这对许多人来说可能令人望而生畏,但许多库通过使用上述方法创建了安全的抽象来解决这个问题。一个具体的例子
是许多Rust库是C库的绑定。它们在上面有安全的抽象(如image-rs
和rust-rdkafka
),允许用户安全地使用底层技术而没有内存问题。标准库的许多部分也使用unsafe代码,同步示例和低级数据结构如Vec
(Vector)和hashmap。The Rustonomicon实际上有一个从头开始重新实现Vec
类型的指南,这是任何想深入学习如何安全使用unsafe Rust的人的绝佳读物。
虽然内存安全不是大多数行业和用例的硬性要求,但内存问题通常耗时解决,如果与基础设施相关,可能成本高昂。实际上,对于大多数开发人员来说,犯内存错误几乎是不可避免的——这就是为什么添加编译时内存安全检查是Rust的一个重要优势。当然,有些东西能通过编译器,但如果你在一个有更多初级工程师的团队中,你绝对不希望他们在生产中引入内存错误。Rust使得确保这种情况不发生变得容易得多。
谁在使用Rust?
当然,目前有不少大公司在使用Rust:Amazon、Google和Microsoft(正在将他们的Office 365后端迁移到Rust)只是其中一部分。许多注重安全的公司也在使用Rust:1Password已经为使用密码键制作了他们自己的Rust库集合,cryptee专注于加密文档存储和照片管理服务,还有越来越多的Rust公司和咨询公司涌现,帮助大公司创建高质量的Rust代码。
DARPA最近也宣布,他们正在将所有的C代码转换为Rust,目标是代码库最终像熟练的Rust开发人员写的一样高质量。虽然这是一个不小的工程,但如果成功,将是Rust未来发展的巨大胜利。
总结
感谢阅读!有很多理由可促使你在后端使用Rust,希望本文能帮助你理解其优势背后的一些基本细节。
原文:https://www.shuttle.rs/blog/2024/07/31/rust-on-the-backend