Rust开发Web后端效率如何?

关注者
170
被浏览
520,913

35 个回答

前言

问题是 2020 年的,现在是 2023年 11月 29日。

2020 年的时候,这个问题肯定不那么好回答,但是到了今天,我对这个问题也有了比较明确的答案:

效率够高,而且还在进步中!

我是内部推动 Rust 作为后端开发的动力源之一,以下这篇文章是第一次在知乎分享的文章,到现在也1年半时间过去了,参与了更多 rust 的 web 项目,今天在这个问题下面再分享一些自己对 rust web 开发的一些想法。

抛出一个问题

我们需要一个什么样的 web 框架?

Java Spring Boot 给我带来了什么:

  • 方便的路由绑定(通过注解)
  • 灵活的 http 参数提取 (@PathVariable @RequestBody @RequestParam等等)
  • 灵活的返回类型(前后端分离时代,基本 Json 为主)
  • 错误处理(全局,类型转变)
  • ORM/数据库集成 (Jpa ,MyBatis,jdbc template 等等)
  • 输入校验 (Bean Validation)
  • 中间件扩展机制 (Interceptor)
  • 灵活的其他模块集成(Sessoin ,权限,Redis ,JMS,MQ 等等)

如果以上需求都满足了,并且功能稳定了,同时行业还有不错的最佳实践参考,那么用这个框架开发项目肯定是能保证效率和效果了(前提是开发者对语言和框架都相对熟悉)。

那么 Rust 可以带来相同的东西吗?

我对主流 rust web 框架都做过技术调研,其实差异都不大,而且都能满足上述需求。(要熟练使用,你对 rust 必须比较了解,文章最后再来谈一谈这个学习曲线的问题)

功能actix-web 4axum 0.7rocket 0.5
方便的路由绑定宏+显示绑定显示绑定宏+显示绑定
灵活的 http 参数提取*支持支持支持
灵活的返回类型*支持支持支持
错误处理支持支持支持
ORM/数据库集成*第三方第三方第三方
输入校验第三方第三方内置
中间件扩展机制支持支持支持
灵活的其他模块集成自身生态+第三方自身生态+第三方自身生态+第三方

数据/内容提取

针对请求提取,三个框架都抽象了 Extrator (每个框架名字可能不一样,下同)机制,通过实现 FromRequest trait , 很方便实现 T 类型的提取,不仅仅可以从 http 上下文提取,还可以提取 State (web::Data) 这类中间数据,也可以配合自己的 Middleware 实现自定义类型的提取。所以很灵活, 代码写起来也很优化,这里我展开对比一下 golang , PS: 我很早关注 golang, 因为它也可以二进制分发,最早使用 revel 这个框架,因为他和 spring boot 很像,奈何后面发展停滞了,自己也就放弃 golang 了,所以对于 golang 最新的框架仅限于,这几天对这个框架文档的学习和理解。

golang 框架,我参考

选了 `gin` , beego , `revel` 作参考

gin: Parameters in path

func main() {
	router := gin.Default()

	// This handler will match /user/john but will not match /user/ or /user
	router.GET("/user/:name", func(c *gin.Context) {
		name := c.Param("name")
		c.String(http.StatusOK, "Hello %s", name)
	})

	// However, this one will match /user/john/ and also /user/john/send
	// If no other routers match /user/john, it will redirect to /user/john/
	router.GET("/user/:name/*action", func(c *gin.Context) {
		name := c.Param("name")
		action := c.Param("action")
		message := name + " is " + action
		c.String(http.StatusOK, message)
	})

	router.Run(":8080")
}

beego:Welcome to Beego | Beego

type MainController struct {
	web.Controller
}

func (ctrl *MainController) Post() {
	name := ctrl.GetString("name")
	if name == "" {
		ctrl.Ctx.WriteString("Hello World")
		return
	}
	ctrl.Ctx.WriteString("Hello " + name)
}

revel:revel.github.io/manual/

// path = /book/:author/:book
author := c.Params.Route.Get("author")
book := c.Params.Route.Get("book")

// url = /foo?sort=asc&active=1
s := c.Params.Query.Get("sort")
act := c.Params.Query.Get("active")

再看看 Rust 的框架

actix-web:actix.rs/docs/extractor

use actix_web::{get, web, App, HttpServer};
use serde::Deserialize;

#[derive(Deserialize)]
struct Info {
    username: String,
}

// this handler gets called if the query deserializes into `Info` successfully
// otherwise a 400 Bad Request error response is returned
#[get("/")]
async fn index(info: web::Query<Info>) -> String {
    format!("Welcome {}!", info.username)
}

axum:axum - Rust

use axum::extract::{Path, Query, Json};
use std::collections::HashMap;

// `Path` gives you the path parameters and deserializes them.
async fn path(Path(user_id): Path<u32>) {}

// `Query` gives you the query parameters and deserializes them.
async fn query(Query(params): Query<HashMap<String, String>>) {}

// Buffer the request body and deserialize it as JSON into a
// `serde_json::Value`. `Json` supports any type that implements
// `serde::Deserialize`.
async fn json(Json(payload): Json<serde_json::Value>) {}

Axum 的文档不是很完善,看方法签名也可以略知一二,和 actix-web 差不多的, rust 可以实现这个是通过 magic function params 的设计实现的,感兴趣可以参考一下:

光从代码层面看, rust 实现的是更优雅,而且更符合 DRY 原则。

ORM

rust 的 orm 框架有 diesel , sea-orm ,rbatis 等, 更地层操作可以使用 sqlx (类似 jdbc template)

个人评价,肯定够用, 但是成熟度比不上 java 生态, golang 不了解。

这里有 rust 没有反射机制的原因, java 和 golang 有运行时反射,这样依赖于动态生成的 orm 可以比较优雅(但是损失性能), rust 依赖宏,编译时生成`新`的东西,所以使用上就会引入`新`的 struct 或者 trait , 这个和 java 的 querydsl 一样。

其他各部分的内容,不展开讲了 , 总体来讲 Rust web 开发通用需求下(微服务还没深入应用)没有什么坑了,各主流框架实现的功能都差不多,不会有很大的功能上的差异,所以如果你问我,推荐 actix-web/axum/rocket 哪个? 我的答案是都可以,因为他们的设计都很接近,这也是 web 框架发展这么多年了,抽象的差不多了,生态上其实更需要的是企业级的应用框架:没错就是类似 spring boot!. 给出最佳实践参考,第三方模块积极整合集成,那时候 rust web 开发应该就更上一层楼了。

Rust 后端应用开发的思考

最后来点喜闻乐见的讨论:

rust 的学习曲线影响 rust web 开发的推广吗? 答案是肯定的。

对于一门语言初学者或者已经有其他语言经验,初学新语言的人,以往的经验可以借用是很好的加分项, c# , golang, java , python 任意一门传统语言,你有一定的编成经验的话,看看项目,改改局部代码,是很容易的,没有什么大的负担,这也是我一直以来推崇的“实践中学习”的方法。

但是到了 rust 这里, 如果你没有比较好的 函数式编成经验或者cpp 经验(本人就是无这方面经验),那么你要去理解一个框架级别的代码,是需要你花时间先去补上 rust 的基础的,这里分为阅读和`局部修改`两方面,比如上面的 magic function params ,这个概念,我最早是通过写 bevy 的 demo 的时候遇到的疑惑, bevy systems 怎么能自动给我自定义的函数注入参数的,带着这个疑问找到了 magic function params 的概念,但是要深入了解你又得理解 rust 的 trait ,带着各种泛型参数 T,P,Q,Z 的花式代码看着眼花缭乱。这不得要先入门 rust!

再比如

#[get("/")]
async fn index(info: web::Query<Info>) -> String {
    format!("Welcome {}!", info.username)
}

magic function params 了解了, 那么这个 Query<Info> 怎么又能直接用 info.username 呢? 这又是 rust Deref trait 的能力,这些基础没有理解的话,很那要去深入理解这些代码的设计是挺难的,但是表面读懂,大家应该都没有问题,只是不知道其所以然!

如果涉及到修改,你第一个就会遇到所有权/move 的问题,如果纯新手,肯定要疑惑,这点你学习理解 rust 的所有权机制后,就会迎刃而解,同样如果遇到生命周期的报错,你得先去深入了解 rust 的引用机制,才能熟练掌握, 要解决 move 报错,最简单可以用引用解决,而这个引用可能又会带来生命周期约束的新问题, 真是一环扣一环,让 rust 没有新手的生存余地!只能历练成长走出新手村!

再来聊一聊一些最佳实践能:

全局数据共享的实现机制

web 应用中,我们需要在不同模块中共享数据,比如数据库连接,全局上下文对象等等, actix/axum/rocket 都提供了 State(web::Data) 机制来给开发者使用,通过注入对象,就可以在请求响应中通过 extrator 机制自动提取对应的类型去使用, web 层 也要通过显示传递给 service 或者 dao 层。 这是目前 rust web 框架推荐的实践方案, spring boot 因为有bean 容器和自动注入,所以这个开发者都是可以不用管理, golang 这几天看的文档,也是使用显示注入或者全局变量的方式实现。

rust 的全局变量这两年也有不错的发展,主要通过 once_cell ,lazy_static 这几个 crate 实现,1.70后 once_cell 的几个功能也进入了标准库, OnceLock 和 OnceCell ,后续还有新的要并入,所以后面 rust 全局变量也会更容易使用了。

Rust 宏和其他语言的运行时反射

Java 和 Golang 的注解可以在运行时通过反射实现动态功能,这个可以让开发者很灵活的应用,但是也会有不小的性能损失,特别是一些性能要求高的场景要避免,灵活应用可以带来很好的开发体验。

Rust 没有运行时反射,所以类似的场景会通过宏实现编译时解决需求,这个对性能没有很大的影响,也可以通过 cargo-expand 等工具查看宏展开后的代码,这点对于一些有代码入侵洁癖的人挺友好的,有了编译时实现就让代码入侵分离成为了代码级别入侵和运行时入侵,比如 java 的 openapi 的很多库实现就是运行时入侵,这点我就接受不了,所以选择了了离线通过注释提取 openapi 的库。 到了 rust 都只有代码级别入侵了, 而且这类工具库,只是提取数据,而不修改代码,这样就对代码运行效率没有任何影响,所以我现在基本不拒绝好用的宏工具。

以上就是个人对 rust web 开发的一些看法,欢迎讨论。

PS: 知乎 markdown 支持真`烂`

最近正在体验 Rust 写网站,大致是 axum 作为框架,tera 搞服务器端渲染,SeaORM 处理数据库。产物静态链接,再把所有静态资源编码进二进制,最后一个二进制一个配置文件丢到服务器上跑。

整个开发体验个人感受还是比较舒适的,反正就是 CRUD。比较好的点是一些性能瓶颈可以借助 Rust 离谱的性能自己优化,并发能干到很高。

至于生命周期的问题,我比较习惯最开始 Arc<Mutex>> 一把梭,等功能逻辑通了以后再回来优化。反正写 CRUD 的时候遇到麻烦的生命周期问题也很少。印象里只有一次想搞个复杂的花活,接收几个闭包作为参数,最后整个函数签名复杂到炸。这里其实也是想装逼了,非必要。

框架的成熟度可能还不如其他后端语言。比如以前用 Actix 的时候在流这里踩过坑,但熟悉 HTTP 和 Rust 的话也问题不大。

总而言之,该有的基本都有,写起来效率也还行,开新坑可以一试。