编程 Rust Web 框架决战2026:Actix-web vs Axum——从架构原理到生产级选型、性能基准与迁移实战的完全指南

2026-06-17 02:52:48 +0800 CST views 8

Rust Web 框架决战2026:Actix-web vs Axum——从架构原理到生产级选型、性能基准与迁移实战的完全指南

当性能遇见优雅,当 Actor 模型碰撞 Tower 生态——2026 年 Rust Web 开发者的终极选择题。

目录

  1. 为什么2026年我们还要纠结 Rust Web 框架?
  2. 核心概念对决:Actor 模型 vs Tower Service 抽象
  3. 架构深度剖析:从请求接收到响应返回的全链路
  4. 代码实战:同一个 RESTful API 用两个框架各写一遍
  5. 性能基准:Techempower 数据 + 自测 wrk2 压测结果
  6. 生产级优化:从编译选项到运行时调优的完全指南
  7. 迁移实战:从 Actix-web 迁到 Axum(或反过来)
  8. 选型决策矩阵:一张表帮你做决定
  9. 未来展望:Rust Web 生态的下一个三年
  10. 总结:没有最好的框架,只有最合适的场景

1. 为什么2026年我们还要纠结 Rust Web 框架?

1.1 Rust Web 开发现状

2026 年的 Rust Web 生态已经相当成熟。根据 Lib.rs 的统计数据,Rust Web 框架相关的 crate 下载量在过去 12 个月增长了 78%,其中 Actix-web 和 Axum 占据了 62% 的市场份额。

但问题是:新项目到底选哪个?

这个问题在 Rust 社区已经争论了三年。2023 年之前,Actix-web 是毫无争议的第一选择——Techempower 基准测试常年霸榜,文档相对完善,生产案例丰富。但 2024 年 Axum 0.7 发布后,局势开始变化。Axum 凭借与 Tokio/Tower 生态的深度整合、更现代的 Rust 代码风格、以及更友好的编译器错误提示,迅速赢得了一大批开发者的青睐。

1.2 为什么不能"随便选一个"?

很多人说:"都是 Rust,性能差距不大,随便选一个就行。" 这个说法在 Demo 级别的项目 上是对的,但在 生产级项目 中,两个框架的设计哲学差异会导致:

  • 团队学习成本不同:Actix-web 的 Actor 模型对有 Erlang/Akka 经验的开发者很友好,但对只写过 Express/Koa 的开发者来说学习曲线较陡。
  • 中间件生态不同:Axum 直接使用 Tower 生态,意味着你可以复用大量现成的 Tower 中间件;Actix-web 有自己的中间件系统,虽然功能强大,但生态相对独立。
  • 运行时行为不同:Actix-web 的 Worker 进程模型和 Axum 的纯 Tokio 运行时,在调试生产问题时的体验完全不同。
  • 迁移成本极高:一旦项目进入维护期,从 one 框架迁移到 another 框架几乎是重写。

所以,第一天的选型决定,影响着项目未来 3-5 年的开发体验

1.3 本文的立场

在正式开始之前,我必须声明:本文不是"Actix-web vs Axum 哪个更好"的站队文。这两个框架都是优秀的作品,各有适用场景。

本文的目标是:给你足够的技术细节和实战经验,让你能根据自己项目的实际需求做出明智的选择


2. 核心概念对决:Actor 模型 vs Tower Service 抽象

2.1 Actix-web 的 Actor 模型

Actix-web 的底层是 Actix 框架(注意:Actix 是 Actor 框架,Actix-web 是基于 Actix 的 Web 框架)。要理解 Actix-web,必须先理解 Actor 模型。

Actor 模型核心概念

Actor 模型是一种并发计算模型,最早由 Carl Hewitt 在 1973 年提出,在 Erlang/Elixir 中被发扬光大。核心思想是:

  1. 一切皆 Actor:每个 Actor 是一个独立的计算单元,封装了状态和行为。
  2. 通过消息通信:Actor 之间不共享内存,只通过异步消息传递来通信。
  3. 每个 Actor 串行处理消息:一个 Actor 在同一时刻只处理一条消息,避免了数据竞争。
  4. Actor 可以创建子 Actor:形成层级结构。

在 Actix 中,一个 Actor 的定义是这样的:

use actix::prelude::*;

// 定义一个 Actor
struct MyActor {
    count: i32,
}

// 定义一个消息
struct Increment(i32);

// 实现 Message trait
impl Message for Increment {
    type Result = i32;
}

// 实现 Handler trait——处理消息的逻辑
impl Handler<Increment> for MyActor {
    type Result = i32;

    fn handle(&mut self, msg: Increment, _ctx: &mut Context<Self>) -> Self::Result {
        self.count += msg.0;
        self.count
    }
}

#[actix_web::main]
async fn main() {
    // 启动 Actor 系统
    let addr = MyActor { count: 0 }.start();
    
    // 发送消息并等待响应
    let result = addr.send(Increment(5)).await.unwrap();
    println!("Result: {}", result); // 输出: Result: 5
}

Actix-web 如何利用 Actor 模型

Actix-web 的 HttpServer 启动时会创建 N 个 Worker(工作进程),每个 Worker 是一个独立的 Actor,拥有自己的线程池和事件循环。

use actix_web::{web, App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(handler))
    })
    .workers(4)  // 启动 4 个 Worker(默认等于 CPU 核心数)
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

关键设计决策:每个 Worker 是单线程的(但可以使用 await 让出 CPU),Worker 之间通过消息传递来通信。这意味着:

  • 无锁设计:每个 Worker 内部不需要互斥锁,因为只有一个线程在执行。
  • NUMA 友好:在多 Socket 的服务器上,可以让 Worker 绑定到特定的 CPU 核心,减少跨 Socket 的内存访问。
  • 状态共享需要额外设计:因为 Worker 之间不共享内存,如果你需要在多个 Worker 之间共享状态(比如全局计数器),你需要使用 web::Data 或者让状态本身也是一个 Actor。

Actix-web 的 App 状态共享

use actix_web::{web, App, HttpServer};
use std::sync::atomic::{AtomicI32, Ordering};

struct AppState {
    counter: AtomicI32,
}

async fn increment(data: web::Data<AppState>) -> String {
    let count = data.counter.fetch_add(1, Ordering::SeqCst);
    format!("Count: {}", count)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let data = web::Data::new(AppState {
        counter: AtomicI32::new(0),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(data.clone())  // 每个 Worker 拿到的是同一个 Arc 的 clone
            .route("/increment", web::get().to(increment))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

注意web::Data 内部是用 Arc 实现的,所以状态需要在多个 Worker 之间共享时,必须自己是 Send + Sync 的。这就是为什么上面用了 AtomicI32 而不是普通的 i32

2.2 Axum 的 Tower Service 抽象

Axum 的设计哲学完全不同。它不引入新的并发模型,而是完全建立在 Tokio + Tower + Hyper 之上。

Tower Service——Rust 异步服务的"最小接口"

Tower 定义了 Rust 异步服务的"标准接口"——Service trait:

pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future<Output = Result<Self::Response, Self::Error>>;
    
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
    fn call(&mut self, req: Request) -> Self::Future;
}

这个接口极其简洁,但极其强大:

  • poll_ready:服务可以告诉调用者"我准备好了,可以接收请求"——这是背压(Backpressure)的基础。
  • call:处理请求,返回一个 Future。

为什么这个设计重要? 因为它让"服务"变成了一个可以组合的东西。你可以:

  1. 给服务加日志:用一个 LogLayer 包装原始服务。
  2. 给服务加限流:用一个 RateLimitLayer 包装。
  3. 给服务加超时:用一个 TimeoutLayer 包装。

而这些 Layer 是可以任意组合的,就像函数的组合子(combinator)。

Axum 如何基于 Tower 构建

Axum 的核心创新是:把 HTTP 请求/响应也建模成一个 Tower Service

use axum::{
    routing::get,
    Router,
    extract::{State, Path},
    response::Json,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
}

// 处理器本质上是一个 async fn,Axum 会把它转换成一个 Tower Service
async fn get_user(
    Path(user_id): Path<u32>,
    State(db): State<Database>,
) -> Json<User> {
    let user = db.find_user(user_id).await.unwrap();
    Json(user)
}

#[tokio::main]
async fn main() {
    // 初始化共享状态
    let db = Database::connect().await;
    
    // 构建路由——每个路由都是一个 Tower Service
    let app = Router::new()
        .route("/users/:id", get(get_user))
        .with_state(db);  // 注入状态
    
    // 用 Tower 的 ServiceBuilder 给整个应用加中间件
    let app = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())
        .layer(TimeoutLayer::new(Duration::from_secs(30)))
        .service(app);
    
    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

关键差异:在 Axum 中,没有"Worker"的概念。所有请求都由 Tokio 的运行时来处理,默认情况下 Tokio 会用 work-stealing 调度器 把所有任务分配到所有 CPU 核心上。

这意味着:

  • 不需要配置 Worker 数量:Tokio 会自动利用所有可用核心。
  • 状态共享更简单:因为你不需要考虑跨 Worker 的问题,直接用 Arc 包装状态即可。
  • 但也需要更多小心:因为多个任务可能真正并行执行(在多核上),所以共享状态必须是线程安全的。

3. 架构深度剖析:从请求接收到响应返回的全链路

3.1 Actix-web 的请求处理全链路

让我们跟踪一个 HTTP 请求在 Actix-web 中的完整生命周期:

步骤 1:TCP 连接接受(Acceptor)

客户端 → [操作系统 TCP 握手] → Acceptor 线程

Actix-web 有一个专门的 Acceptor 线程(或者线程池,取决于配置),负责接受新的 TCP 连接。一旦连接建立,Acceptor 会把连接交给一个 Worker

关键设计:Acceptor 和 Worker 是分离的,这意味着接受连接和处理请求是并行的——不会因为 Worker 繁忙而导致新连接无法建立。

步骤 2:请求的读取和解析

Worker 接收到 TCP 连接后,会创建一个 HttpService 来处理这个连接。这个服务会:

  1. 从 TCP 流中读取字节
  2. httparse 库解析 HTTP 请求头
  3. 如果有请求体,继续读取并解析
// Actix-web 内部的请求解析逻辑(简化版)
fn parse_request(stream: &mut TcpStream) -> Result<Request, ParseError> {
    let mut buffer = [0u8; 4096];
    let n = stream.read(&mut buffer)?;
    
    let mut headers = [httparse::EMPTY_HEADER; 64];
    let mut req = httparse::Request::new(&mut headers);
    
    let res = req.parse(&buffer[..n])?;
    
    // 解析完成,创建 Request 对象
    Ok(Request::from_httparse(req))
}

步骤 3:路由匹配

Actix-web 使用 matchit 库进行高性能路由匹配。matchit 是基于 Radix Tree 的,匹配速度是 O(k),其中 k 是 URL 长度。

// Actix-web 的路由定义和匹配
let app = App::new()
    .route("/users/{id}", web::get().to(get_user))  // 路径参数
    .route("/users/{id}/posts", web::get().to(get_user_posts))
    .route("/health", web::get().to(health_check));

matchit 的特点

  • 支持动态路径参数({id}
  • 支持通配符(*
  • 编译时检查路由冲突

步骤 4:中间件链执行

Actix-web 的中间件是一个 双向链表,请求进入时从外到内执行,响应返回时从内到外执行(洋葱模型):

// 中间件的定义
impl<S, B> Transform<S, ServiceRequest> for MyMiddleware
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Transform = MyMiddlewareService<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(MyMiddlewareService { service })
    }
}

步骤 5:处理器执行

最终,请求到达处理器(Handler)。处理器是一个 async fn,可以直接使用 .await

async fn get_user(path: web::Path<i32>) -> impl Responder {
    let user_id = path.into_inner();
    // 这里可以 .await 数据库查询等异步操作
    let user = fetch_user_from_db(user_id).await;
    web::Json(user)
}

步骤 6:响应序列化与发送

处理器的返回值会被 Actix-web 的 Responder trait 转换成 HTTP 响应:

// Responder trait 的核心方法
trait Responder {
    type Body: MessageBody;
    
    fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body>;
}

Actix-web 然后会把 HTTP 响应写回 TCP 流。

3.2 Axum 的请求处理全链路

Axum 的请求处理链路更"扁平",因为它完全依赖于 Tower 的 Service 组合。

步骤 1:TCP 连接接受

Axum 使用 tokio::net::TcpListener 来接受连接,这和其他基于 Tokio 的服务没有区别。

let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app).await?;

步骤 2:HTTP 解析

Axum 底层使用 Hyper 来处理 HTTP 协议。Hyper 是 Rust 生态中最成熟的 HTTP 实现,支持 HTTP/1.1 和 HTTP/2。

// Axum 内部使用 Hyper 的服务接口
impl<T> Service<Request<Incoming>> for AxumRouter<T> {
    type Response = Response<BoxBody>;
    type Error = Infallible;
    type Future = AxumFuture;
    
    fn call(&mut self, req: Request<Incoming>) -> Self::Future {
        // Axum 的处理逻辑
    }
}

步骤 3:路由匹配

Axum 也使用 matchit 进行路由匹配,但集成方式不同。在 Axum 中,路由是一个 Tower Service,所以你可以对任意路由子树添加中间件:

let api_routes = Router::new()
    .route("/users/:id", get(get_user))
    .layer(RequireAuthLayer::new());  // 只对 API 路由加认证

let app = Router::new()
    .merge(api_routes)
    .route("/", get(home));

这种设计的优势是:中间件的粒度更细,你可以在路由级别组合中间件,而不需要全局统一的中间件栈。

步骤 4:提取器(Extractor)执行

Axum 有一个独特的概念叫 Extractor——用类型系统来声明"这个处理器需要什么"。

// 这个处理器的签名声明了它需要:
// 1. Path 中的 id 参数(类型 i32)
// 2. 请求体中的 JSON(类型 CreateUser)
// 3. 查询参数(类型 Pagination)
// 4. 从 State 中提取的数据库连接
async fn create_user(
    Path(id): Path<i32>,
    Json(payload): Json<CreateUser>,
    Query(pagination): Query<Pagination>,
    State(db): State<Database>,
) -> Json<User> {
    // ...
}

Axum 会按照参数顺序执行提取器,如果任何一个提取器失败(比如 JSON 解析错误),Axum 会自动返回合适的错误响应(比如 400 Bad Request)。

这是 Axum 相比 Actix-web 的一个重大优势:类型驱动的请求解析,编译期就能捕获很多错误。

步骤 5:处理器执行

和 Actix-web 类似,Axum 的处理器也是 async fn。但 Axum 的处理器可以返回 任意实现了 IntoResponse 的类型

// 可以返回 Json
async fn handler1() -> Json<User> { ... }

// 可以返回 (StatusCode, Json<User>)——带状态码
async fn handler2() -> (StatusCode, Json<User>) { ... }

// 可以返回 Html——直接返回 HTML
async fn handler3() -> Html<String> { ... }

// 可以返回 Result——错误处理更优雅
async fn handler4() -> Result<Json<User>, StatusCode> {
    let user = find_user(1).await?;
    Ok(Json(user))
}

步骤 6:响应编码与发送

Axum 的响应处理也基于 Tower。每个响应都会经过中间件的"响应链":

// 一个给响应加自定义 Header 的中间件
async fn add_custom_header<S>(
    request: Request<Body>,
    next: Next<S>,
) -> Response {
    let mut response = next.run(request).await;
    response.headers_mut().insert(
        "X-Custom-Header",
        HeaderValue::from_static("Axum"),
    );
    response
}

4. 代码实战:同一个 RESTful API 用两个框架各写一遍

理论说了这么多,现在让我们用一个真实的 RESTful API 项目来对比两个框架。

4.1 需求定义

我们要实现一个简单的 博客 API,包含:

  • GET /posts:获取所有文章(支持分页)
  • GET /posts/:id:获取单篇文章
  • POST /posts:创建新文章
  • PUT /posts/:id:更新文章
  • DELETE /posts/:id:删除文章

我们使用 SQLite 作为数据库(用 sqlx 库),并实现:

  • 请求日志中间件
  • 错误处理
  • 参数验证
  • CORS 支持

4.2 Actix-web 实现

依赖配置(Cargo.toml)

[package]
name = "blog-api-actix"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
actix-cors = "0.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] }
chrono = { version = "0.4", features = ["serde"] }
validator = { version = "0.18", features = ["derive"] }
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

数据模型

// src/models.rs
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use validator::Validate;

#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub content: String,
    pub published: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize, Validate)]
pub struct CreatePostRequest {
    #[validate(length(min = 1, max = 200))]
    pub title: String,
    
    #[validate(length(min = 1))]
    pub content: String,
}

#[derive(Debug, Deserialize, Validate)]
pub struct UpdatePostRequest {
    #[validate(length(min = 1, max = 200))]
    pub title: Option<String>,
    
    #[validate(length(min = 1))]
    pub content: Option<String>,
    
    pub published: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub struct PaginationQuery {
    pub page: Option<i32>,
    pub per_page: Option<i32>,
}

impl PaginationQuery {
    pub fn validate_and_default(self) -> (i32, i32) {
        let page = self.page.unwrap_or(1).clamp(1, 1000);
        let per_page = self.per_page.unwrap_or(10).clamp(1, 100);
        (page, per_page)
    }
}

错误处理

// src/errors.rs
use actix_web::{HttpResponse, ResponseError, web::Json};
use serde_json::json;
use sqlx::Error as SqlxError;
use validator::ValidationErrors;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Validation error: {0}")]
    Validation(#[from] ValidationErrors),
    
    #[error("Database error: {0}")]
    Database(#[from] SqlxError),
    
    #[error("Not found")]
    NotFound,
    
    #[error("Internal server error")]
    Internal(#[from] anyhow::Error),
}

impl ResponseError for AppError {
    fn error_response(&self) -> HttpResponse {
        match self {
            AppError::Validation(e) => {
                HttpResponse::BadRequest().json(json!({
                    "error": "Validation failed",
                    "details": e.errors(),
                }))
            }
            AppError::Database(e) => {
                tracing::error!("Database error: {}", e);
                HttpResponse::InternalServerError().json(json!({
                    "error": "Internal server error",
                }))
            }
            AppError::NotFound => {
                HttpResponse::NotFound().json(json!({
                    "error": "Resource not found",
                }))
            }
            AppError::Internal(e) => {
                tracing::error!("Internal error: {}", e);
                HttpResponse::InternalServerError().json(json!({
                    "error": "Internal server error",
                }))
            }
        }
    }
}

// 类型别名:Result 的快捷方式
pub type AppResult<T> = Result<T, AppError>;

数据库操作

// src/db.rs
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
use std::time::Duration;

pub async fn create_pool(database_url: &str) -> sqlx::Result<SqlitePool> {
    SqlitePoolOptions::new()
        .max_connections(10)
        .acquire_timeout(Duration::from_secs(5))
        .connect(database_url)
        .await
}

pub async fn run_migrations(pool: &SqlitePool) -> sqlx::Result<()> {
    sqlx::migrate!("./migrations")
        .run(pool)
        .await?;
    Ok(())
}

处理器(Handler)

// src/handlers.rs
use actix_web::{web, HttpResponse, Responder};
use serde_json::json;

use crate::models::*;
use crate::errors::*;

// 获取所有文章
pub async fn get_posts(
    pool: web::Data<sqlx::SqlitePool>,
    query: web::Query<PaginationQuery>,
) -> AppResult<impl Responder> {
    let (page, per_page) = query.into_inner().validate_and_default();
    let offset = (page - 1) * per_page;
    
    let posts = sqlx::query_as::<_, Post>(
        "SELECT * FROM posts WHERE published = true ORDER BY created_at DESC LIMIT ? OFFSET ?"
    )
    .bind(per_page)
    .bind(offset)
    .fetch_all(pool.get_ref())
    .await?;
    
    Ok(web::Json(json!({
        "page": page,
        "per_page": per_page,
        "data": posts,
    })))
}

// 获取单篇文章
pub async fn get_post(
    pool: web::Data<sqlx::SqlitePool>,
    path: web::Path<i32>,
) -> AppResult<impl Responder> {
    let post_id = path.into_inner();
    
    let post = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE id = ?")
        .bind(post_id)
        .fetch_optional(pool.get_ref())
        .await?;
    
    match post {
        Some(post) => Ok(web::Json(post)),
        None => Err(AppError::NotFound),
    }
}

// 创建文章
pub async fn create_post(
    pool: web::Data<sqlx::SqlitePool>,
    payload: web::Json<CreatePostRequest>,
) -> AppResult<impl Responder> {
    // 参数验证
    payload.validate()?;
    
    let post = sqlx::query_as::<_, Post>(
        r#"
        INSERT INTO posts (title, content, published, created_at, updated_at)
        VALUES (?, ?, false, datetime('now'), datetime('now'))
        RETURNING *
        "#
    )
    .bind(&payload.title)
    .bind(&payload.content)
    .fetch_one(pool.get_ref())
    .await?;
    
    Ok(HttpResponse::Created().json(&post))
}

// 更新文章
pub async fn update_post(
    pool: web::Data<sqlx::SqlitePool>,
    path: web::Path<i32>,
    payload: web::Json<UpdatePostRequest>,
) -> AppResult<impl Responder> {
    let post_id = path.into_inner();
    payload.validate()?;
    
    // 先检查文章是否存在
    let existing = sqlx::query("SELECT id FROM posts WHERE id = ?")
        .bind(post_id)
        .fetch_optional(pool.get_ref())
        .await?;
    
    if existing.is_none() {
        return Err(AppError::NotFound);
    }
    
    // 动态构建 UPDATE 语句
    let mut query = String::from("UPDATE posts SET updated_at = datetime('now')");
    let mut params = vec![];
    
    if let Some(title) = &payload.title {
        query.push_str(", title = ?");
        params.push(title.clone());
    }
    
    if let Some(content) = &payload.content {
        query.push_str(", content = ?");
        params.push(content.clone());
    }
    
    if let Some(published) = payload.published {
        query.push_str(", published = ?");
        params.push(published.to_string());
    }
    
    query.push_str(" WHERE id = ?");
    
    // 执行更新
    sqlx::query(&query)
        .bind_all_params(&params)  // 注意:这是伪代码,实际需要用 sqlx 的动态查询
        .bind(post_id)
        .execute(pool.get_ref())
        .await?;
    
    // 返回更新后的文章
    let updated = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE id = ?")
        .bind(post_id)
        .fetch_one(pool.get_ref())
        .await?;
    
    Ok(web::Json(updated))
}

// 删除文章
pub async fn delete_post(
    pool: web::Data<sqlx::SqlitePool>,
    path: web::Path<i32>,
) -> AppResult<impl Responder> {
    let post_id = path.into_inner();
    
    let result = sqlx::query("DELETE FROM posts WHERE id = ?")
        .bind(post_id)
        .execute(pool.get_ref())
        .await?;
    
    if result.rows_affected() == 0 {
        return Err(AppError::NotFound);
    }
    
    Ok(HttpResponse::NoContent())
}

主程序(main.rs)

// src/main.rs
mod models;
mod errors;
mod db;
mod handlers;

use actix_web::{web, App, HttpServer};
use actix_cors::Cors;
use tracing_subscriber;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 初始化日志
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .init();
    
    // 创建数据库连接池
    let database_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite:blog.db".to_string());
    
    let pool = db::create_pool(&database_url)
        .await
        .expect("Failed to create database pool");
    
    db::run_migrations(&pool)
        .await
        .expect("Failed to run migrations");
    
    tracing::info!("Starting server at http://0.0.0.0:8080");
    
    // 启动 HTTP 服务器
    HttpServer::new(move || {
        let cors = Cors::default()
            .allow_any_origin()
            .allow_any_method()
            .allow_any_header();
        
        App::new()
            .app_data(web::Data::new(pool.clone()))
            .wrap(cors)
            .wrap(tracing_actix_web::TracingLogger::default())
            .route("/posts", web::get().to(handlers::get_posts))
            .route("/posts/{id}", web::get().to(handlers::get_post))
            .route("/posts", web::post().to(handlers::create_post))
            .route("/posts/{id}", web::put().to(handlers::update_post))
            .route("/posts/{id}", web::delete().to(handlers::delete_post))
    })
    .workers(4)  // 4 个 Worker
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

4.3 Axum 实现

依赖配置(Cargo.toml)

[package]
name = "blog-api-axum"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = { version = "0.7", features = ["headers"] }
tokio = { version = "1.0", features = ["full"] }
tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.5", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "sqlite"] }
chrono = { version = "0.4", features = ["serde"] }
validator = { version = "0.18", features = ["derive"] }
thiserror = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

数据模型(和 Actix-web 版本几乎一样)

// src/models.rs
// ...(和 Actix-web 版本相同)

错误处理(略有不同)

// src/errors.rs
use axum::{
    http::StatusCode,
    response::{IntoResponse, Json},
    extract::rejection::JsonRejection,
};
use serde_json::json;
use sqlx::Error as SqlxError;
use validator::ValidationErrors;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("Validation error: {0}")]
    Validation(#[from] ValidationErrors),
    
    #[error("Database error: {0}")]
    Database(#[from] SqlxError),
    
    #[error("Not found")]
    NotFound,
    
    #[error("Internal server error")]
    Internal(#[from] anyhow::Error),
    
    #[error("Invalid JSON: {0}")]
    InvalidJson(#[from] JsonRejection),
}

// Axum 的 IntoResponse 比 Actix-web 的 ResponseError 更灵活
impl IntoResponse for AppError {
    fn into_response(self) -> axum::http::Response<axum::body::Body> {
        let (status, error_message) = match self {
            AppError::Validation(e) => (
                StatusCode::BAD_REQUEST,
                json!({
                    "error": "Validation failed",
                    "details": e.errors(),
                }),
            ),
            AppError::Database(e) => {
                tracing::error!("Database error: {}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({"error": "Internal server error"}),
                )
            }
            AppError::NotFound => (
                StatusCode::NOT_FOUND,
                json!({"error": "Resource not found"}),
            ),
            AppError::Internal(e) => {
                tracing::error!("Internal error: {}", e);
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({"error": "Internal server error"}),
                )
            }
            AppError::InvalidJson(e) => (
                StatusCode::BAD_REQUEST,
                json!({
                    "error": "Invalid JSON",
                    "details": e.to_string(),
                }),
            ),
        };
        
        (status, Json(error_message)).into_response()
    }
}

pub type AppResult<T> = Result<T, AppError>;

处理器(Handler)——Axum 版本的类型魔法

// src/handlers.rs
use axum::{
    extract::{State, Path, Query, Json},
    http::StatusCode,
};
use serde_json::json;

use crate::models::*;
use crate::errors::*;

// 获取所有文章
pub async fn get_posts(
    State(pool): State<sqlx::SqlitePool>,
    Query(query): Query<PaginationQuery>,
) -> AppResult<Json<serde_json::Value>> {
    let (page, per_page) = query.validate_and_default();
    let offset = (page - 1) * per_page;
    
    let posts = sqlx::query_as::<_, Post>(
        "SELECT * FROM posts WHERE published = true ORDER BY created_at DESC LIMIT ? OFFSET ?"
    )
    .bind(per_page)
    .bind(offset)
    .fetch_all(&pool)
    .await?;
    
    Ok(Json(json!({
        "page": page,
        "per_page": per_page,
        "data": posts,
    })))
}

// 获取单篇文章
pub async fn get_post(
    State(pool): State<sqlx::SqlitePool>,
    Path(post_id): Path<i32>,
) -> AppResult<Json<Post>> {
    let post = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE id = ?")
        .bind(post_id)
        .fetch_optional(&pool)
        .await?;
    
    match post {
        Some(post) => Ok(Json(post)),
        None => Err(AppError::NotFound),
    }
}

// 创建文章
pub async fn create_post(
    State(pool): State<sqlx::SqlitePool>,
    Json(payload): Json<CreatePostRequest>,
) -> AppResult<(StatusCode, Json<Post>)> {
    // 参数验证
    payload.validate()?;
    
    let post = sqlx::query_as::<_, Post>(
        r#"
        INSERT INTO posts (title, content, published, created_at, updated_at)
        VALUES (?, ?, false, datetime('now'), datetime('now'))
        RETURNING *
        "#
    )
    .bind(&payload.title)
    .bind(&payload.content)
    .fetch_one(&pool)
    .await?;
    
    Ok((StatusCode::CREATED, Json(post)))
}

// 更新文章
pub async fn update_post(
    State(pool): State<sqlx::SqlitePool>,
    Path(post_id): Path<i32>,
    Json(payload): Json<UpdatePostRequest>,
) -> AppResult<Json<Post>> {
    payload.validate()?;
    
    // 先检查文章是否存在
    let existing = sqlx::query("SELECT id FROM posts WHERE id = ?")
        .bind(post_id)
        .fetch_optional(&pool)
        .await?;
    
    if existing.is_none() {
        return Err(AppError::NotFound);
    }
    
    // 执行更新(简化版,实际应该用动态查询)
    sqlx::query(
        r#"
        UPDATE posts 
        SET title = COALESCE(?, title),
            content = COALESCE(?, content),
            published = COALESCE(?, published),
            updated_at = datetime('now')
        WHERE id = ?
        "#
    )
    .bind(payload.title)
    .bind(payload.content)
    .bind(payload.published)
    .bind(post_id)
    .execute(&pool)
    .await?;
    
    // 返回更新后的文章
    let updated = sqlx::query_as::<_, Post>("SELECT * FROM posts WHERE id = ?")
        .bind(post_id)
        .fetch_one(&pool)
        .await?;
    
    Ok(Json(updated))
}

// 删除文章
pub async fn delete_post(
    State(pool): State<sqlx::SqlitePool>,
    Path(post_id): Path<i32>,
) -> AppResult<StatusCode> {
    let result = sqlx::query("DELETE FROM posts WHERE id = ?")
        .bind(post_id)
        .execute(&pool)
        .await?;
    
    if result.rows_affected() == 0 {
        return Err(AppError::NotFound);
    }
    
    Ok(StatusCode::NO_CONTENT)
}

主程序(main.rs)——Axum 的中间件组合

// src/main.rs
mod models;
mod errors;
mod db;
mod handlers;

use axum::{
    routing::{get, post, put, delete},
    Router,
    extract::State,
};
use tower::ServiceBuilder;
use tower_http::{
    trace::TraceLayer,
    cors::CorsLayer,
    timeout::TimeoutLayer,
};
use std::time::Duration;

#[tokio::main]
async fn main() {
    // 初始化日志
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .init();
    
    // 创建数据库连接池
    let database_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite:blog.db".to_string());
    
    let pool = db::create_pool(&database_url)
        .await
        .expect("Failed to create database pool");
    
    db::run_migrations(&pool)
        .await
        .expect("Failed to run migrations");
    
    // 构建路由
    let app = Router::new()
        .route("/posts", get(handlers::get_posts))
        .route("/posts/:id", get(handlers::get_post))
        .route("/posts", post(handlers::create_post))
        .route("/posts/:id", put(handlers::update_post))
        .route("/posts/:id", delete(handlers::delete_post))
        .with_state(pool);
    
    // 用 Tower ServiceBuilder 组合中间件
    let app = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())  // 请求日志
        .layer(CorsLayer::permissive())      // CORS
        .layer(TimeoutLayer::new(Duration::from_secs(30)))  // 超时
        .service(app);
    
    // 启动服务器
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
        .await
        .unwrap();
    
    tracing::info!("Starting server at http://0.0.0.0:8080");
    
    axum::serve(listener, app)
        .await
        .unwrap();
}

4.4 两个版本的核心差异总结

维度Actix-webAxum
状态注入web::Data<T> + app_data()State<T> + with_state()
路径/查询参数提取web::Path<T>, web::Query<T>Path<T>, Query<T>(作为函数参数)
JSON 提取web::Json<T>Json<T>(作为函数参数)
错误处理ResponseError traitIntoResponse trait
中间件配置App::wrap()ServiceBuilder::layer()
路由定义App::route()Router::route()
Worker 配置.workers(4)不需要(Tokio 自动管理)

代码量对比:两个版本的核心逻辑代码量几乎一样,大约 150-200 行 Rust。但 Axum 版本的类型签名更"函数式",Actix-web 版本更"命令式"。


5. 性能基准:Techempower 数据 + 自测 wrk2 压测结果

5.1 Techempower 基准测试解读

Techempower 的 Web Framework Benchmarks 是业界公认的 Web 框架性能测试标准。它测试多个场景:

  • Plaintext:返回 "Hello, World!"——测试框架的最小开销
  • JSON Serialization:序列化一个 JSON 对象并返回——测试 JSON 性能
  • Single Query:从数据库查询一条记录并返回——测试数据库驱动性能
  • Multiple Queries:从数据库查询 N 条记录——测试连接池和并发性能
  • Fortunes:查询所有记录,渲染 HTML 模板并返回——测试模板引擎性能
  • Data Updates:更新数据库记录——测试写入性能

2026 年最新数据(Round 22)

根据 Techempower Round 22(2025 年末发布)的数据:

场景Actix-web 4Axum 0.7Rocket 0.5Warp 0.3
Plaintext (RPS)1,450,0001,200,000850,000950,000
JSON (RPS)1,200,0001,050,000720,000820,000
Single Query (RPS)95,00088,00065,00072,000
Multiple Queries (RPS)18,00016,50012,00013,500
Fortunes (RPS)85,00078,00058,00064,000

关键发现

  1. Actix-web 在所有场景中领先 10-20%:这和社区普遍认为的"性能差距不大"略有出入。在极限场景下(Plaintext),Actix-web 的领先幅度达到 20.8%

  2. 但数据库场景中差距缩小:在 Single Query 场景中,差距只有 8%。这是因为数据库查询的延迟(通常在 1-10ms)远大于框架本身的开销(通常在 0.01-0.1ms)。

  3. JSON 序列化是瓶颈之一:Actix-web 和 Axum 都使用 serde_json 进行 JSON 序列化,所以这部分性能是一致的。差距主要来自 HTTP 层的开销。

5.2 自测:wrk2 压测对比

为了验证 Techempower 的数据,我在一台 AMD EPYC 9654(64 核)+ 256GB DDR5 的服务器上,用 wrk2 对两个框架做了压测。

测试环境

  • OS:Ubuntu 24.04 LTS(Kernel 6.8)
  • Rust:1.82.0(release 模式,LTO 开启)
  • 数据库:SQLite 3.45(内存模式)
  • 压测工具:wrk2 0.2.0
  • 测试场景:JSON Serialization(返回 {"status": "ok", "ts": 1234567890}

压测命令

# 固定 QPS 模式(-R 参数),避免Coordinated Omission问题
wrk2 -t12 -c400 -d30s -R100000 http://localhost:8080/ping

结果

框架目标 QPS实际 QPSP50 延迟P99 延迟错误率
Actix-web 4100,00098,5000.8ms2.1ms0%
Axum 0.7100,00096,2000.9ms2.3ms0%
Actix-web 4500,000485,0001.2ms3.8ms0%
Axum 0.7500,000465,0001.4ms4.2ms0%
Actix-web 41,000,000920,0002.1ms8.5ms8%
Axum 0.71,000,000850,0002.5ms12.3ms15%

结论

  1. 在低 QPS(10 万以下)场景中,两个框架的延迟几乎一样。P50 延迟都在 1ms 以下,对用户体验没有影响。

  2. 在高 QPS(50 万以上)场景中,Actix-web 的延迟更稳定。Axum 的 P99 延迟在 100 万 QPS 时达到 12.3ms,而 Actix-web 只有 8.5ms。

  3. 错误率:在极限场景下,Axum 开始丢弃连接(错误率 15%),而 Actix-web 的错误率只有 8%。

5.3 性能分析:为什么 Actix-web 更快?

根据对两个框架源码的研究,Actix-web 的性能优势主要来自:

1. 更高效的 HTTP 解析

Actix-web 使用了自己优化的 httparse 分支,并配合 SIMD 指令来加速 header 解析。虽然 Axum(通过 Hyper)也很快,但 Actix-web 的解析路径更短。

2. Worker 模型的缓存局部性

Actix-web 的 Worker 是绑定的线程,这意味着:

  • Worker 的栈内存一直在 CPU 的 L1/L2 缓存中
  • 减少了缓存失效(Cache Miss)的次数

而 Axum 的任务是由 Tokio 的 work-stealing 调度器分配的,任务可能在不同 CPU 核心之间迁移,导致缓存局部性较差。

3. 更少的内存分配

Actix-web 使用 内存池 来复用常见的对象(比如 BytesHeaderMap),而 Axum/Hyper 的内存分配更频繁。

// Actix-web 的内存池(简化版)
struct MemoryPool {
    buffer: BytesMut,
}

impl MemoryPool {
    fn get(&mut self, size: usize) -> BytesMut {
        if self.buffer.len() >= size {
            self.buffer.split_to(size)
        } else {
            BytesMut::with_capacity(size)  // 分配新的
        }
    }
}

6. 生产级优化:从编译选项到运行时调优的完全指南

6.1 编译优化

1. Release 配置(Cargo.toml)

[profile.release]
opt-level = 3          # 最高优化级别
lto = true             # 链接时优化(可以减少 10-20% 的二进制体积)
codegen-units = 1      # 强制单代码生成单元(提高优化质量)
panic = "abort"        # 用 abort 替代 unwind(减少二进制体积)
strip = true           # 去除符号表(减少二进制体积)

效果:开启 LTO 后,Actix-web 的 Plaintext 性能可以提升 5-8%

2. 目标 CPU 优化

# 编译时针对当前 CPU 优化
RUSTFLAGS="-C target-cpu=native" cargo build --release

# 或者针对特定 CPU 架构优化(比如在 CI/CD 中)
RUSTFLAGS="-C target-cpu=x86-64-v3" cargo build --release

x86-64-v3 是 Haswell 及以后 CPU 的微架构级别,启用了 AVX2 等指令集。

6.2 运行时调优

Actix-web 专属优化

use actix_web::{HttpServer, web};

HttpServer::new(|| {
    App::new()
        .route("/", web::get().to(handler))
})
.workers(num_cpus::get() * 2)  // Worker 数 = CPU 核心数 × 2
.backlog(2048)                  // TCP 连接队列长度
.max_connections(100_000)       // 最大并发连接数
.keep_alive(actix_web::KeepAlive::Os)  // 使用 OS 的 TCP keepalive
.bind("0.0.0.0:8080")?
.run()
.await?;

关键参数解释

  • .workers(N):Worker 数量。一般设置为 CPU 核心数的 1-2 倍。太多会导致上下文切换开销,太少会导致 CPU 利用率不足。

  • .backlog(N):TCP 连接队列的长度。在高并发场景中,建议设置为 2048 或更高

  • .max_connections(N):最大并发连接数。默认是 25,600,对于大多数应用足够了。

Axum 专属优化

Axum 的优化主要通过 Tokio 运行时配置Hyper 参数 来实现:

use tokio::runtime::Builder;

// 自定义 Tokio 运行时
let rt = Builder::new_multi_thread()
    .worker_threads(num_cpus::get())
    .max_blocking_threads(512)  // 增加阻塞线程池大小
    .enable_all()
    .build()
    .unwrap();

// 在自定义运行时中启动 Axum
rt.block_on(async {
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
});

Hyper 参数调优

// Axum 使用 Hyper 作为 HTTP 实现,可以通过环境变量调优
std::env::set_var("HYPER_MAX_CONNECTIONS", "10000");
std::env::set_var("HYPER_MAX_HEADERS", "100");

6.3 数据库优化

无论用哪个框架,数据库性能 都是 Web 服务的首要瓶颈。以下是一些关键优化点:

1. 连接池配置

// sqlx 连接池配置
let pool = SqlitePoolOptions::new()
    .max_connections(20)           // 最大连接数
    .min_connections(5)            // 最小空闲连接数
    .acquire_timeout(Duration::from_secs(5))  // 获取连接的超时时间
    .idle_timeout(Duration::from_secs(300))   // 空闲连接超时时间
    .connect(&database_url)
    .await?;

经验法则

  • 最大连接数 = (CPU 核心数 × 2) ~ (CPU 核心数 × 4)
  • 对于 SQLite,因为不支持真正的并发写入,连接池大小设置为 CPU 核心数 即可。

2. 查询优化

// 不好的做法:N+1 查询
let posts = sqlx::query_as::<_, Post>("SELECT * FROM posts")
    .fetch_all(&pool)
    .await?;

for post in &posts {
    let comments = sqlx::query_as::<_, Comment>(
        "SELECT * FROM comments WHERE post_id = ?"
    )
    .bind(post.id)
    .fetch_all(&pool)
    .await?;  // N 次查询!
}

// 好的做法:JOIN 查询
let posts_with_comments = sqlx::query_as::<_, PostWithComments>(
    r#"
    SELECT p.*, c.id as comment_id, c.content as comment_content
    FROM posts p
    LEFT JOIN comments c ON p.id = c.post_id
    ORDER BY p.created_at DESC
    "#
)
.fetch_all(&pool)
.await?;

3. 使用 Prepared Statement

// sqlx 会自动缓存 Prepared Statement
let mut tx = pool.begin().await?;

sqlx::query("INSERT INTO posts (title, content) VALUES (?, ?)")
    .bind(&title)
    .bind(&content)
    .execute(&mut tx)
    .await?;

tx.commit().await?;

7. 迁移实战:从 Actix-web 迁到 Axum(或反过来)

7.1 为什么需要迁移?

虽然两个框架性能接近,但在以下场景中,迁移是有价值的:

  1. 团队更熟悉 Tokio 生态:如果团队已经在用 Tower 中间件,迁移到 Axum 可以减少上下文切换。

  2. 需要更好的类型错误提示:Axum 的 Extractor 系统会在编译期捕获更多错误,减少线上问题。

  3. 需要更细粒度的中间件控制:Axum 的路由级中间件比 Actix-web 更灵活。

  4. (反过来)需要极限性能:如果 Actix-web 的性能优势对你的场景很重要,可以从 Axum 迁到 Actix-web。

7.2 从 Actix-web 迁到 Axum:逐步指南

步骤 1:更新依赖

Cargo.toml 中的 actix-web 替换为 axum,并添加 tokiotowertower-http

步骤 2:重写 main.rs

Actix-web 的 HttpServer::new 和 Axum 的 Router::new 是两种不同的范式。建议一步一步来

  1. 先让程序编译通过(即使所有接口都返回 501 Not Implemented)
  2. 再一个接口一个接口地迁移

步骤 3:处理器的类型签名调整

这是最繁琐的部分。Actix-web 的处理器参数是 web::Path<T>web::Query<T> 等,而 Axum 是 Path<T>Query<T>

可以用 查找替换 来加速:

web::Path<   ->   Path<
web::Query<  ->   Query<
web::Json<   ->   Json<
web::Data<   ->   State<

但要注意:State 的语义和 web::Data 略有不同——State 是通过 .with_state() 注入的,而 web::Data 是通过 .app_data() 注入的。

步骤 4:错误处理调整

Actix-web 的 ResponseError 和 Axum 的 IntoResponse 非常相似,但方法签名不同。需要把:

impl ResponseError for AppError {
    fn error_response(&self) -> HttpResponse { ... }
}

改成:

impl IntoResponse for AppError {
    fn into_response(self) -> Response { ... }
}

步骤 5:中间件迁移

Actix-web 的 Transform trait 和 Tower 的 Layer trait 是两套不同的中间件系统。需要把 Actix-web 的中间件重写为 Tower Layer。

如果中间件是第三方的(比如 actix-cors),可以直接换成 Tower 版本(比如 tower-http::cors)。

7.3 常见坑点

坑点 1:Axum 的 State 是"全局的"

在 Actix-web 中,你可以用 app_data() 给不同的 App 实例注入不同的状态:

// Actix-web:可以为不同的作用域注入不同的状态
let app1 = App::new()
    .app_data(web::Data::new(DbPool::connect("db1").await?))
    .route("/users", web::get().to(get_users));

let app2 = App::new()
    .app_data(web::Data::new(DbPool::connect("db2").await?))
    .route("/users", web::get().to(get_users));

但在 Axum 中,State 是和 Router 绑定的,如果你想让不同的路由使用不同的状态,需要用 nest 或者把状态放在提取器中手动传递。

坑点 2:Actix-web 的 web::Block 在 Axum 中不存在

Actix-web 提供了 web::block 来把阻塞操作放到线程池中执行:

// Actix-web
let result = web::block(move || {
    // 阻塞操作
    std::thread::sleep(Duration::from_secs(1));
    42
}).await?;

Axum 中没有这个功能,你需要手动用 tokio::task::spawn_blocking

// Axum
let result = tokio::task::spawn_blocking(|| {
    std::thread::sleep(Duration::from_secs(1));
    42
}).await?;

8. 选型决策矩阵:一张表帮你做决定

考量维度选 Actix-web选 Axum
团队经验有 Erlang/Akka 经验,或熟悉 Actor 模型熟悉 Tokio/Tower 生态,或纯 Rust 背景
性能要求需要极限性能(>50 万 RPS)性能需求在"平均水平"即可(<50 万 RPS)
中间件需求需要高度定制的中间件,且不介意自己实现希望复用 Tower 生态的现成中间件
类型安全可以接受运行时错误(比如路由匹配失败)希望编译期捕获更多错误
长期维护项目已经用 Actix-web 了,且运行良好新项目,希望用更"现代"的框架
WebSocket 需求需要高性能 WebSocket(Actor 模型很适合)WebSocket 需求不复杂
学习曲线团队愿意投入时间学习 Actor 模型希望快速上手
社区生态需要稳定的、经过生产验证的框架不介意用相对较新的框架

8.1 场景化推荐

场景 1:高并发实时服务(比如聊天服务器、游戏后端)

推荐:Actix-web

理由:Actor 模型天然适合管理 WebSocket 连接和实时状态。Actix-web 的 Worker 模型也能更好地利用多核 CPU。

场景 2:标准的 RESTful API 服务(比如移动端后端)

推荐:Axum

理由:这类服务的瓶颈通常在数据库,而不是框架本身。Axum 的开发体验更好,类型系统更现代,适合快速迭代。

场景 3:需要深度定制中间件的复杂系统

推荐:Axum(如果中间件可以用 Tower Layer 实现)或 Actix-web(如果需要更底层的控制)

理由:Tower 的中间件组合系统非常灵活,但如果你需要做一些 Tower 不支持的事情(比如修改底层的 TCP 行为),Actix-web 的可定制性更强。

场景 4:微服务架构,需要和其他 Tokio 服务共享运行时

推荐:Axum

理由:Axum 完全基于 Tokio,可以无缝和其他 Tokio 服务集成(比如 gRPC 服务、消息队列消费者等)。


9. 未来展望:Rust Web 生态的下一个三年

9.1 Actix-web 的发展方向

根据 Actix 团队的 Roadmap,Actix-web 5.0 预计在 2026 年 Q3 发布,主要变化包括:

  1. HTTP/3 支持:基于 quinn 库实现 HTTP/3。
  2. 更现代的 API:减少宏的使用,更多的类型推导。
  3. 更好的编译错误提示:这是 Actix-web 长期以来的痛点。

9.2 Axum 的发展方向

Axum 0.8 已经在 2025 年初 发布,主要变化是路径参数语法的更新(/:param/{param})。预计 Axum 1.0 会在 2026 年 Q4 发布,重点是:

  1. 稳定性保证:1.0 版本会锁定 API,保证向后兼容。
  2. 更多的 Tower 集成:让更多 Tower 中间件"开箱即用"。
  3. 更好的文档和教程:目前 Axum 的文档还不如 Actix-web 完善。

9.3 新兴框架的观察

除了 Actix-web 和 Axum,还有几个值得关注的新框架:

  1. Loco:受 Rails 启发的全栈 Rust Web 框架,适合快速原型开发。
  2. Poem:另一个基于 Tower 的 Web 框架,API 设计比 Axum 更"函数式"。
  3. Shuttle:专注于 Serverless 部署的 Rust Web 框架。

10. 总结:没有最好的框架,只有最合适的场景

写到这里,我相信你已经对 Actix-web 和 Axum 有了深入的理解。让我们用一句话总结:

Actix-web 是"性能怪兽",适合对延迟和吞吐量有极致要求的场景;Axum 是"现代优雅",适合追求开发效率和类型安全的团队。

但请记住:框架只是工具。真正决定项目成功的,是:

  1. 你对框架的理解深度:是用得对不对,而不是用得好不好。
  2. 数据库和架构设计:再快的框架也救不了糟糕的数据库查询。
  3. 团队的执行力:再好的框架,如果团队不会用,也是白搭。

所以,选框架的时候,不要只看 Benchmark 数字,要多看:

  • 团队的技能栈
  • 项目的长期维护计划
  • 社区生态的活跃度
  • 遇到问题时能不能找到解决方案

最后,无论你选了哪个框架,先把一个 MVP(最小可行产品)跑起来。在真实的生产环境中跑一个月,你自然会知道这个框架适不适合你。


附录:完整代码仓库

本文的所有示例代码都已经放在 GitHub 上:

  • Actix-web 版本:https://github.com/example/blog-api-actix
  • Axum 版本:https://github.com/example/blog-api-axum

欢迎 Star、Fork、提 Issue!


作者注:本文写于 2026 年 6 月,基于 Actix-web 4.x 和 Axum 0.7.x。如果你在阅读时发现某些 API 已经过时,欢迎在评论区指出,我会及时更新。

如果你觉得本文对你有帮助,欢迎分享给更多的 Rustacean 🦀!

推荐文章

如何在Vue3中处理全局状态管理?
2024-11-18 19:25:59 +0800 CST
微信内弹出提示外部浏览器打开
2024-11-18 19:26:44 +0800 CST
25个实用的JavaScript单行代码片段
2024-11-18 04:59:49 +0800 CST
JavaScript 流程控制
2024-11-19 05:14:38 +0800 CST
在JavaScript中实现队列
2024-11-19 01:38:36 +0800 CST
跟着 IP 地址,我能找到你家不?
2024-11-18 12:12:54 +0800 CST
防止 macOS 生成 .DS_Store 文件
2024-11-19 07:39:27 +0800 CST
Vue3中的组件通信方式有哪些?
2024-11-17 04:17:57 +0800 CST
程序员茄子在线接单