Rust后端框架选型实战:Actix与Axum全方位对比与决策指南

发布时间:2026/10/18 8:13:55
Rust后端框架选型实战:Actix与Axum全方位对比与决策指南 我这两年给团队选 Rust 后端框架几乎每次都会被同一个问题拦住到底用 actix 还是 axum问的人多半已经被各种压测榜单和口号绕晕了——今天看某个排名说 actix 是地表最强明天又看到一篇文章说 axum 才是 tokio 官方正统。作为一个两个框架都写过生产代码、也各自踩过坑的人我可以直接告诉你在绝大多数业务场景里它们的性能差距你根本感受不到真正决定体验的是设计理念、中间件生态、WebSocket 写法、错误处理范式以及它们对团队协作方式的潜移默化。这篇文章就把我对 actix 和 axum 从选型、迁移到线上维护的完整经验拆开讲从出身、路由、中间件聊到 WebSocket 和错误处理最后给出具体的选型建议。1. 两个框架的出身完全不同理解它们才能理解所有差异1.1 Actix一个把性能写进基因的老牌选手Actix 系列最初源自 actor 模型框架Web 部分是在这套理念上长出来的。它的 HTTP 层并不是直接复用某个现成实现而是自己维护了一套完整的请求解析、连接管理和 worker 调度逻辑所以你在看 actix-web 源码时能明显感觉到这是个自成体系的项目。它早年连续霸占各类 Web 框架基准测试榜首性能神话是它最亮眼的标签也因此吸引了一大批对吞吐量极其敏感的用户。这也直接塑造了它的气质功能尽量内置配置尽量集中开箱就跑得很快。比如你新建一个HttpServer默认就按 CPU 核数拉起 worker每个 worker 有独立的事件循环请求分散到多 worker 处理的模型非常清晰。对刚接触 Rust Web 开发的人来说actix 提供的是一套框架感很强的 API——你按照它的规则组织App、service、middleware就行它帮你把服务端该有的东西基本都配齐了。1.2 Axumtokio 官方生态里的集成商Axum 登场晚得多2021 年才出现它的定位从一开始就很明确不重复造轮子。底层用 tokio 做异步运行时用 hyper 做 HTTP 实现中间件体系直接建立在 tower 的Service/Layer抽象上在显式持有hyper、tower-http、http-body这些 crate 的组合拳下Axum 更像是一个路由和提取器层的组装方案。这种做法的好处是生态复用率极高。你在 tower 生态里学到的Layer写法换个项目还能用你在其他基于 hyper 的框架里写的服务概念的迁移成本也很低。坏处则是概念链路变长新手经常需要同时弄懂Router、Service、Layer、Handler、Body之间的关系不像 actix 那样跟着框架走就行。Axum 的设计哲学简单说是这样框架只提供最小但完整的原语其他能力交给配套生态。1.3 出身带来的气质差异这两条不同路线最后呈现给开发者的感受差异非常大。用一个生活化的类比actix 像一套精装修交付的房子开发商帮你配好了厨卫、管线、门窗你拎包入住axum 像一套户型极佳但只做了基础硬装的房子给了你高质量的水电走线和板材至于用哪个牌子、怎么组合你可以自由选但前提是你得懂点装修。我自己的体会是这种气质差异会一路渗透到后面的每一个决定写中间件、处理 WebSocket 长连接、给请求加超时、做自定义错误返回……每个环节两个框架都会给你不同的标准答案。这也是为什么我强烈建议大家在真正选型前把这两个框架的核心理念先看清楚而不是只看谁的 demo 跑得快。2. 路由、State 与提取器日常开发接触最多的部分2.1 路由写法从:id到{id}的历史账路由是每个项目第一天就要写的代码所以我把对比重点放在这里。actix-web 的路径参数一直使用{id}这种花括号语法例如/user/{id}Axum 在 0.7 及以前的版本用的是/:id冒号语法到了 0.8 大版本才改成和 actix 一致的/{id}。也就是说如果你在网上看到大量 Axum 教程里写route(/users/:id)那很可能是 0.7 时代的旧写法在 0.8 上编译会直接报错。这一点是很多人从旧教程学习时踩到的最大的暗坑。先看 actix-web 4 的典型写法use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; #[get(/user/{id})] async fn get_user(id: web::Pathi32) - impl Responder { HttpResponse::Ok().body(format!(user id {}, id.into_inner())) } #[actix_web::main] async fn main() - std::io::Result() { HttpServer::new(|| App::new().service(get_user)) .bind((127.0.0.1, 8080))? .workers(4) .run() .await }再看 Axum 0.8 的等价实现use axum::{extract::Path, routing::get, Router}; async fn get_user(Path(id): Pathi32) - String { format!(user id {}, id) } #[tokio::main] async fn main() { let app Router::new().route(/user/{id}, get(get_user)); let listener tokio::net::TcpListener::bind(127.0.0.1:8080) .await .expect(bind failed); axum::serve(listener, app).await.expect(server error); }两种写法的可读性都不错参数提取也都是类型安全的路径里写的必须是能解析成i32的值解析失败会得到 400 而不是 panic。从这一点看两者没有明显的高下之分更多是团队习惯问题。2.2 State依赖注入的两种姿势真实项目里几乎不可能没有全局共享状态数据库连接池、配置对象、外部服务客户端。两个框架都提供了依赖注入式的状态管理但细节上有个容易被忽略的差异。actix 用web::DataT你把它注册到App::app_data(...)在 handler 里直接声明参数即可#[derive(Clone)] struct AppState { db: String, // 替换成你的连接池 } async fn get_user(id: web::Pathi32, state: web::DataAppState) - HttpResponse { let s state.db.as_str(); HttpResponse::Ok().body(format!(user {} from {}, id, s)) } // 初始化时 // App::new().app_data(web::Data::new(AppState { db: ....into() }))web::DataT内部本身就是ArcT所以不用担心克隆开销handler 里直接拿来用就行。Axum 用的是StateT提取器但Router::with_state(state)要求状态类型T: Clone而且每次提取都会 clone 一份。如果你存的是连接池这类克隆成本高的对象通常得手动包一层Arc#[derive(Clone)] struct AppState { db: std::sync::ArcString, // 实践中一般是 ArcPool } async fn get_user( Path(id): Pathi32, State(state): StateAppState, ) - String { format!(user {} from {}, id, state.db) } // 初始化时 // let app Router::new() // .route(/user/{id}, get(get_user)) // .with_state(AppState { db: std::sync::Arc::new(....into()) });这里我见过不少新人踩坑直接往StateT里塞一个没有实现Clone的类型结果启动后第一次请求就 panic。解决方案也很简单要么让状态类型实现Clone要么统一包成ArcT。另外在多个模块同时需要状态时Axum 更推荐用Arc包大对象然后针对每个子路由再次共享actix 则天然没有这个心智负担。2.3 提取器顺序与 body 消费提取器是这两个框架日常开发的手感所在。actix 和 axum 都支持从Path、Query、Json、Form、HeaderMap等来源提取数据也都遵循同一个铁律会消费请求体的提取器必须放在参数最后。actix 里web::JsonT、web::Payload这类属于末端提取器Axum 里JsonT、String、Bytes、multipart::Multipart也是一样必须先提取完所有非 body 参数再提取 body。只讲一句放最后显然不够我举个具体例子。Axum 中这样写是合法的async fn create_user( Path(team_id): Pathi32, Query(params): QueryHashMapString, String, Json(payload): JsonCreateUserReq, ) - impl IntoResponse { // ... }但如果你把Json(payload)挪到中间编译器会直接拒绝因为JsonT实现了FromRequest而不是FromRequestParts它必须处于提取链末尾。actix 也有类似限制只不过报错方式略有不同。对于这种设计我一开始也觉得是约束后来反而觉得是好事它从类型层面杜绝了先消费 body 再读 query这种容易出 bug 的写法。自定义提取器方面actix 需要实现FromRequesttraitAxum 0.8 引入了带M标记的FromRequestS, M和FromRequestPartsS两套 trait主体逻辑虽然没变但签名调整导致很多网上的老代码需要跟着改。如果你计划深度定制提取器建议直接看当前版本文档别拿两年前的帖子当标准。3. 中间件与生态Tower 的降维打击到底体现在哪里3.1 两种中间件的写法对比中间件是另一个日常每天都碰的东西。actix 有一套基于Transform和Service的中间件体系老派写法需要定义一个结构体、实现Transform格式比较仪式化不过它也提供了wrap_fn这种匿名闭包形式日常记日志、计时这类逻辑用闭包最省事use actix_web::{App, HttpServer}; HttpServer::new(|| { App::new() .wrap_fn(|req, srv| { let start std::time::Instant::now(); let fut srv.call(req); async move { let resp fut.await?; println!(actix: {} {:?}, resp.status(), start.elapsed()); Ok(resp) } }) }) .bind((127.0.0.1, 8080)) .unwrap() .run() .awaitAxum 的等价格式则完全建立在 tower 的Layer和Service之上。用middleware::from_fn可以不用了解Service内部细节直接把一个接收 Request 和 Next、返回 Response的平铺函数变成中间件use axum::{ middleware::{self, Next}, response::Response, routing::get, Router, }; use axum::extract::Request; async fn timing_middleware(req: Request, next: Next) - Response { let start std::time::Instant::now(); let resp next.run(req).await; println!(axum: {} {:?}, resp.status(), start.elapsed()); resp } let app Router::new() .route(/, get(|| async { hello })) .layer(middleware::from_fn(timing_middleware));这里正是两个框架的哲学分界点。actix 的中间件系统是自研私有协议你想在 actix 里复用一个 tower 生态组件基本要写适配层axum 则直接把中间件抽象交给了 tower于是 tower 生态里所有现成组件比如超时、重试、限流、追踪、CORS都能以统一方式接入。对于需要通过 ServiceBuilder 做中间件编排的场景axum 的组合表达能力明显更强这也是很多人说的axum 在中间件上是降维打击的真正含义。3.2 现成组件生态对比光说理论不够我列一个日常项目里最常用的中间件对照表需求actix Web 生态Axum 生态访问日志 / 链路追踪actix 自带 Logger集成 tracing 稍显费劲tower_http::trace::TraceLayer和 tracing 无缝衔接CORSactix-corstower_http::cors::CorsLayer压缩actix-web提供 Compress 中间件tower_http::compression::CompressionLayer超时自己包Future或用 actix-timeout 类三方库tower::timeout::TimeoutLayer静态文件actix-filestower-http::services::ServeDir会话actix-sessionaxum 需要自己接三方 session crate所以如果你做的是一个强依赖中间件编排的业务例如网关、BFF 层Axum 的体验会顺畅得多。反过来如果项目只是常规 CRUD 加几个接口actix 自带能力基本够用你甚至不需要去第三方生态翻找。3.3 中间件作用范围的差异还有一个细节非常影响排障中间件的作用范围。axum 的layer既可以挂在整个Router上也可以只挂在某个具体路由上甚至可以通过route_layer只对某条路径生效。这种粒度控制对部分接口需要额外校验、其余接口不动的场景特别有用。actix 的中间件挂在App层面是对全局 worker 生效想针对单个接口做差异化逻辑通常需要在 handler 内部处理或者在中间件里自己判断路径。不能说谁更优毕竟全局挂载更符合统一的横切逻辑应该全局生效的直觉但在路由级中间件这个长尾需求上axum 确实更细。4. WebSocket、流式响应与错误处理直面真实业务场景4.1 WebSocketactor 风格与简单升级回调如果你做实时推送、在线聊天、协作编辑这类功能WebSocket 是躲不掉的而这一块两个框架的语义差别最大选错会直接影响团队开发效率。actix 的 WebSocket 实现和它的 actor 模型深度绑定你为每个连接定义一个 actor 结构体让它实现Actor和StreamHandler然后在WebSocketContext里维护状态、发消息、处理断连。这种模型的优点是每个连接的状态被封装得很干净适合每个客户端一个独立会话的场景缺点是理解门槛高你要先弄懂 actor 的生命周期、消息类型、ctx怎么用新手往往在这一步劝退。一个最小可用的 actix WebSocket 长这样use actix::{Actor, AsyncContext, StreamHandler}; use actix_web::{web, HttpRequest, HttpResponse}; use actix_web_actors::ws::{self, WebSocketContext}; struct WsSession; impl Actor for WsSession { type Context WebSocketContextSelf; } impl StreamHandlerResultws::Message, ws::ProtocolError for WsSession { fn handle(mut self, msg: Resultws::Message, ws::ProtocolError, ctx: mut Self::Context) { if let Ok(ws::Message::Text(text)) msg { ctx.text(format!(echo: {}, text)); } } } async fn ws_route(req: HttpRequest, stream: web::Payload) - HttpResponse { ws::start(WsSession, req, stream) }Axum 的 WebSocket 走的是升级回调路线用WebSocketUpgrade提取器接收升级请求然后on_upgrade挂上一个异步闭包闭包里直接读写 WebSocket 消息异步循环自己掌控。不需要引入 actor 概念心智模型就是一个长连接的异步任务use axum::{ extract::ws::{Message, WebSocket, WebSocketUpgrade}, response::Response, }; async fn ws_route(ws: WebSocketUpgrade) - Response { ws.on_upgrade(handle_socket) } async fn handle_socket(mut socket: WebSocket) { while let Some(Ok(msg)) socket.recv().await { if let Message::Text(text) msg { let _ socket.send(Message::Text(text)).await; } } }两者我都写过多套线上代码客观来说连接数少、逻辑简单时 axum 这种一个 async 函数搞定的写法明显更顺手连接数非常大、状态机复杂、需要超时重连等行为时actix 的 actor 模型能逼你把逻辑拆得更规整反而利于后期维护。但如果你团队里没有熟悉 actor 模型的人我建议先选 axum因为从 async 思维自然迁移过来的成本最低。4.2 流式响应与 SSE除了 WebSocket流式场景也能看出两者底子。axum 的响应体类型Body基于http-body天然支持从异步流构造 body所以做 SSE 或者大文件流式返回非常顺手。SSE 甚至有专门的Sse响应类型配合Sse::keep_alive可以维护心跳use axum::response::sse::{Event, KeepAlive, Sse}; use tokio_stream::wrappers::ReceiverStream; use futures_util::stream::StreamExt; let stream /* 某异步流产出 ResultEvent, Infallible */; Sse::new(stream).keep_alive(KeepAlive::default())actix 做流式响应也不差底层有HttpResponse::streaming传入一个产出Bytes的异步流即可但 SSE 这种文本事件 心跳的结合需要自己拼事件格式、自己发注释行维持连接。如果你的接口清单里 SSE 占了相当比例axum 确实省事不少。use actix_web::{HttpResponse, web}; use futures_util::stream::{once, StreamExt}; async fn sse_handler() - HttpResponse { let stream once(async { Ok(web::Bytes::from(data: hello\n\n)) }); HttpResponse::Ok() .content_type(text/event-stream) .streaming(stream) }4.3 错误处理的哲学差异错误处理是最能体现两个框架范式的场景之一。actix 要求你实现ResponseErrortrait把业务错误映射为某一种 HTTP 响应Axum 的思路更现代——任何实现了IntoResponse的类型都可以直接作为 handler 的返回错误于是你可以把ResultT, E里的E定义成业务错误类型写起来非常自然。actix 的典型做法use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use serde_json::json; #[derive(Debug)] struct BizError(String); impl ResponseError for BizError { fn status_code(self) - StatusCode { StatusCode::BAD_REQUEST } fn error_response(self) - HttpResponse { HttpResponse::build(self.status_code()).json(json!({message: self.0})) } }axum 的写法use axum::{http::StatusCode, response::{IntoResponse, Response}, Json}; struct BizError(String); impl IntoResponse for BizError { fn into_response(self) - Response { (StatusCode::BAD_REQUEST, Json(serde_json::json!({ message: self.0 }))).into_response() } }看着差不多但 axum 的优势在于什么都能转 Response。你甚至可以把StatusCode、字符串、ResultT, E直接作为 handler 返回值框架自动帮你做转换。这一点在快速原型阶段非常有生产力少写很多样板代码。当然反面影响是有的因为返回类型太自由团队如果没有约定错误响应格式会变得五花八门。我见过不少 axum 项目最后不得不用统一的ApiError类型把范式重新收紧而 actix 因为 API 更重反而从一开始就统一了格式。5. 性能、编译体验与版本节奏别被压测数字带偏5.1 性能同一梯队里的细微差别必须先把话说清楚这几年公开的基准测试里actix 和 axum 都处在头部梯队actix 做一些纯短连接压测时偶尔领跑axum 在带中间件链路的长请求场景上差距也小到可以忽略。真到生产环境瓶颈几乎都会转移到数据库、Redis、外部 RPC 和序列化上框架层那点差距对用户响应时间的影响通常是零点几毫秒级。有一个更值得关注的点是连接处理模型。actix 默认多 worker 自研 HTTP 层在某些高并发短连接场景下确实有一点点优势axum 则完全站在 hyper 肩膀上hyper 本身也是大厂反复优化过的 HTTP 实现两边都不是等闲之辈。因此我的建议是如果性能是你的唯一考量直接抛硬币然后花时间把数据库访问和序列化优化好收益远大于纠结这两个框架。5.2 编译时间与依赖体积编译体验是很多人容易忽略的隐性成本。axum 的依赖链路包含 tokio、hyper、tower、tower-http、http-body 这一大套东西特别是开启全部 feature 后冷编译时间明显偏长actix 的依赖也不少但它的 HTTP 层是自研的没有 hyper 那套庞大的泛型体系冷编译感觉上会轻一些。我这里说的轻是相对而言两者首次全量编译在普通笔记本上都是几分钟起步CI 上如果没有缓存构建每次全量编译都是一笔不小的时间成本。小团队持续集成频繁时我建议在 CI 里把编译缓存和 sccache 尽早配上尤其是在快速迭代的 Axum 项目里这比纠结框架本身更实在。开发期也一样热重载工具配合 cargo watch 使用能显著减轻编译等待带来的烦躁感。5.3 版本节奏稳定 vs 快速迭代actix-web 的 4.x 已经稳定了相当长一段时间核心 API 很少有大变动网上资料基本不过期这是它对团队非常友好的地方。axum 则相反目前仍处于 0.x 快速迭代期几乎每年都有大版本发布0.6 改路由嵌套和状态管理、0.7 改Server绑定方式、0.8 又改了路径参数语法并调整了FromRequest。跟版本升级对每个依赖方来说都是一次不小的迁移工作。维度actix-web 4.xAxum 0.8API 稳定性高长期稳定中0.x 阶段仍可能有破坏性变更文档时效性老资料多数可用教程和帖子容易过期务必查新版文档版本升级工作量小大升级时建议逐条过 changelog我并不是说 axum 快速迭代是缺点——它代表了生态还在积极演化很多设计决策也在吸收社区反馈。但如果你是给企业做多年维护的内部系统依赖一个还在快速变动的框架是需要认真权衡的风险。锁定版本、细化依赖、把升级列入技术债日程这才是负责任的做法。6. 选型建议先写一个小 Demo再谈生产力6.1 两张表看清各自适合什么把前面对比抽象成一张决策表可以直接贴在团队文档里对比维度actix-webaxum性能微基准略占优同一梯队差距可忽略HTTP 实现自研 HTTP 层多 worker 模型基于 hyper中间件生态自成一派够用但封闭tower / tower-http 生态复用面广WebSocketactor 模型适合复杂连接状态升级回调简洁直观SSE / 流式可做但 SSE 需自己拼内置 Sse 支持更顺手错误处理ResponseError范式统一IntoResponse灵活自由API 稳定性4.x 长期稳定0.x 快速迭代学习曲线需要理解 actor 模型需要理解 tower 抽象再从业务视角看一遍你要做的如果是实时推送、在线协作、游戏服务这类长连接 每连接独立状态的系统actix 的 actor 模型天然契合能很好地约束并发状态如果你要做的是微服务网关、BFF、REST API、SSE 聚合这类中间件链复杂、请求响应为主的系统axum 的 tower 生态会让你少写大量胶水代码。6.2 我给两类项目的实际选择举两个我个人的实际案例避免纸上谈兵。一个做实时通知中心的项目每个客户端连接要保持状态、要心跳、要根据连接状态广播事件我最终选了 actix。原因很简单团队里最熟悉的那套模式就是 actor 风格把每个连接建模成一个 actor消息收发、超时清理都变得有章法用 axum 的 async 循环写法也能实现但需要自己管理很多如果服务端要主动推消息给指定客户端的状态注册表反而容易乱。另一个做内部 API 聚合层的项目我选了 axum。因为它需要把多个下游服务的输出协议统一、统一加 trace 和超时、部分路由要做白名单校验这套东西用 tower 的ServiceBuilder分层组合非常合理而且IntoResponse可以让我快速把各种原始 RPC 响应转成统一的 JSON 错误结构开发效率高了一大截。这两个案例想说明的不是某某场景只能用某某框架而是选型的决定性因素往往是团队心智模型最契合哪边的范式。两个框架都能完成同样的事差别在于哪种写法在你们的项目里崩溃概率更低、交接成本更小。6.3 最后一个建议如果你看完这篇还是举棋不定我的建议很具体挑一个周五下午用两个框架分别写同一个最小业务 Demo包含两个带路径参数的路由、一个数据库查询、一个 WebSocket 回显、一个自定义错误返回。写完之后不看跑分只看哪份代码你在三个月后再打开时更愿意继续维护。多数人做完这个小实验就有了答案。我这几年围观过不少团队选型最后决定性的往往不是技术参数而是这种最朴素的后端工程师直觉——代码是不是顺手是不是敢放心交给下一个接手的人。选一个能让你长期待下去的开发范式比选一个今天快 3% 的框架重要得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询