本次参赛选手是…
本次将通过基本的路由和中间件,着重评测二位的性能和上手体验。测试设计很草率,并不代表普遍情况!
选择理由
有读者老爷可能会问了,明明 xxx 性能更强/更成熟/更优雅,为什么不选它?
Golang 方面, Gin 毋庸置疑是最为成熟稳定、生态最丰富的 Web Server Framework.
其 func(*gin.Context)
吃遍天的良好设计极其优雅高效。其他的 Framework 相比它
并没有决定性的性能优势,反倒是接口千奇百怪,难以理解。
Rust 方面,本来我是要选 Actix 的,然而它在本机上的性能表现比 Axum 差 20% 以上,而且 它的接口个人不太喜欢,尤其是其奇怪的 Response 构造和 path macro。Axum 的接口是最 对我胃口的,且性能在第一梯队。
当然,有测试表明,Actix 最大的优势是系统性能友好——它相比 Axum 能省 30% 以上的 CPU、 50% 以上的内存,而性能差距不到 10%。(虽然和我自己测出来的不一样)
Golang 代码
直接依赖: github.com/gin-gonic/gin
代码:
main.go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.New()
r.Use(Counter())
r.GET("/count", Count)
r.NoRoute(NotFound)
r.Run("localhost:8081")
}
func NotFound(c *gin.Context) {
c.JSON(200, gin.H{
"code": 404,
"message": "not found",
})
}
func Count(c *gin.Context) {
msg, _ := c.Get("example")
c.JSON(200, gin.H{
"code": 200,
"message": msg,
})
}
func Counter() gin.HandlerFunc {
n := 0
return func(c *gin.Context) {
c.Set("example", n)
n += 1
c.Next()
}
}
超级优雅的 Go 风格代码!仅需 35 行!
Rust 代码
Cargo.toml
tokio = {version = "1", features = ["full"]}
axum = "0.7"
serde_json = "1"
main.rs
use axum::{
extract::{Request, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Json,
};
use ctx::Ctx;
use serde_json::json;
#[tokio::main]
async fn main() {
let ctx = Ctx::new();
let app = axum::Router::new()
.route("/count", get(count))
.fallback(fallback)
.layer(from_fn_with_state(ctx.clone(), counter))
.with_state(ctx);
let listener = tokio::net::TcpListener::bind("localhost:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn fallback(_: Request) -> impl IntoResponse {
(StatusCode::OK, Json(json!({
"code": 404,
"message": "not found"
})))
}
async fn count(State(state): State<Ctx>) -> impl IntoResponse {
(StatusCode::OK, Json(json!({
"code": 200,
"message": state.get()
})))
}
use axum::middleware::{from_fn_with_state, Next};
async fn counter(State(mut state): State<Ctx>, request: Request, next: Next) -> Response {
let response = next.run(request).await;
state.inc();
response
}
mod ctx {
use std::{cell::Cell, sync::Arc};
#[derive(Clone)]
pub struct Ctx(Arc<Cell<i32>>);
unsafe impl Sync for Ctx {}
unsafe impl Send for Ctx {}
impl Ctx {
pub fn new() -> Self {
Self(Arc::new(Cell::new(0)))
}
pub fn get(&self) -> i32 {
self.0.get()
}
pub fn inc(&mut self) {
self.0.set(self.0.get() + 1)
}
}
}
其实核心代码和 Golang 差不多,但是总共却要 64 行代码。主要繁琐在
- 需解析的参数要手动填写,而非从一个 gin.Context 中全部获得
- State 类型需手动实现
Sync
Trait 的约束使我们要手动绕过它(当然这是我的问题不是 Rust 的问题)- crate - module 组织结构太丑陋,不得不手动
use
性能测试
到了最激动人心的环节了!
我们采用 wrk 在 Ryzen 6800H 的 Linux(5.10.16.3-microsoft-standard-WSL2) 上,分别测试 1, 10, 100, 1000 连接的 loopback 内 close 和 keep-alive 连接 QPS。数据采用 五次测试取中位数的方式。
命令: wrk -d3s -t8 -c$CONNS
Connection: Close QPS:
Backend | 单连接 | 10 连接 | 100 连接 | 1000 连接 |
---|---|---|---|---|
Axum | 5713.87 | 37406.69 | 67543.18 | 64061.99 |
Gin | 5374.02 | 30646.14 | 86524.36 | 80722.98 |
Connection: Keep-Alive QPS:
Backend | 单连接 | 10 连接 | 100 连接 | 1000 连接 |
---|---|---|---|---|
Axum | 15211.09 | 85662.56 | 206834.91 | 567978.31 |
Gin | 9830.07 | 64874.18 | 214375.67 | 332513.46 |
可见,Gin 对短连接、高并发情形有蜜汁优化。
值得一提的是,在任何场景中,axum 的 CPU 占用都在 gin 的 75% 以下,在 Keep-Alive 场景中 甚至可以达到 50%。可见 axum 仍未充分利用 CPU,有提升空间;而 Golang 存在一些 Overhead 妥协, 在 CPU 性能受限时表现较差。
当然我还顺带测了测 nginx:差距太大啦!低并发下 QPS 差快一倍啦!果然还是不能跟专业的比啊(笑)
总结
Gin 的表现令人惊喜。在测试之前我以为 Rust 既然编译那么久,各种传参 数据流都做了充分的优化,性能肯定碾压并非 Golang 阵营最强的 Gin。然而架构和成熟度的差异 使得 Gin 面对更贴近现实极端情况的高并发短连接情形,性能极其优异。
Axum 在长连接的情形下性能远超 Gin,在高并发短连接下也不会和 gin 差太远。毕竟它还是个 beta 库,未来可期。
考虑到编码和编译复杂度,我想,如果只是写一个网络中间件,例如简单 CRUD 的项目,Golang 和 Gin 一定是更好的选择;它成熟稳定,性能优异,代码优雅,编译快速,线上占用较低。 而如果要写一个功能丰富、CPU 密集型的后端,我会选择 Rust 和 Axum。因为 Rust 有着 Cargo 这个 活跃的社区和强大优雅的模块系统设计。