一个请求穿过 axum
主线位置:两条主线都在。本页先把一个小应用装配起来(装配主线),再给它发几个请求(请求主线),每一步都打印出来,看清哪些事只在启动时发生一次,哪些事每个请求都要再做一遍。细节留给后面的章节。
sequenceDiagram participant B as 启动代码 participant R as Router participant M as MethodRouter participant F as from_fn 中间件 participant H as HandlerService participant U as get_user Note over B,U: 装配主线:启动时一次 B->>M: get(get_user) 放进 get 槽 B->>R: route 两次,路径进 matchit B->>R: layer:现成的 Route 当场套 B->>R: with_state:状态克隆 6 次 R->>M: 方法槽变 Route,补套 layer Note over B,U: 请求主线:每个请求一次 B->>R: oneshot GET /users/7 R->>R: matchit 匹配,存路径参数 R->>M: 按方法选 get 槽 M->>F: 克隆 Route,oneshot F->>H: next.run(req) H->>U: 依次跑 State、Caller、Path U-->>H: String H-->>F: into_response F-->>B: 200 OK
程序
两个路由:GET /users/{id} 用到状态、一个自定义提取器和 Path,GET /health 什么都不用。整个 Router 外面套一个 from_fn 中间件。为了看清每件事在什么时候发生,程序做了四件事:
AppState手写Clone,每克隆一次计数加一。- 自定义提取器
Caller实现FromRequestParts,被调用时打印一行。 - 用
LogLayer包住from_fn(log_mw),每次Layer::layer被调用(也就是中间件真正套到某个服务上)时计数并打印。 - 中间件打印它看到的
MatchedPath。
不起服务器:Router<()> 是一个 tower Service,直接用 oneshot 调用(怎么用它写测试见 oneshot)。程序放在 src/bin/walk.rs,所以输出里的类型名带 walk:: 前缀。
axum = { version = "=0.8.9", default-features = false, features = ["matched-path"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "=0.1.5"
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::any::type_name_of_val;
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::body::Body;
use axum::extract::{FromRequestParts, MatchedPath, Path, Request, State};
use axum::http::{request::Parts, StatusCode};
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use http_body_util::BodyExt;
use tower::{Layer, ServiceExt};
// 数 AppState 被 clone 了几次
static CLONES: AtomicUsize = AtomicUsize::new(0);
// 数 Layer::layer 被调用了几次
static LAYERS: AtomicUsize = AtomicUsize::new(0);
fn clones() -> usize {
CLONES.load(Ordering::SeqCst)
}
struct AppState {
name: &'static str,
}
impl Clone for AppState {
fn clone(&self) -> Self {
CLONES.fetch_add(1, Ordering::SeqCst);
Self { name: self.name }
}
}
// 自定义提取器:从 x-caller 头里取调用者
struct Caller(String);
impl<S: Send + Sync> FromRequestParts<S> for Caller {
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
println!(" Caller::from_request_parts");
let value = parts.headers.get("x-caller").and_then(|v| v.to_str().ok());
match value {
Some(v) => Ok(Caller(v.to_owned())),
None => Err((StatusCode::BAD_REQUEST, "missing x-caller")),
}
}
}
// 包住 from_fn 的 Layer,只为了打印 layer() 什么时候被调用
#[derive(Clone)]
struct LogLayer<L>(L);
impl<L: Layer<I>, I> Layer<I> for LogLayer<L> {
type Service = L::Service;
fn layer(&self, inner: I) -> Self::Service {
let n = LAYERS.fetch_add(1, Ordering::SeqCst) + 1;
println!(" Layer::layer #{n}");
self.0.layer(inner)
}
}
async fn log_mw(req: Request, next: Next) -> Response {
let matched = req.extensions().get::<MatchedPath>().map(|m| m.as_str().to_owned());
println!(" middleware before: {} {} matched={:?}", req.method(), req.uri(), matched);
let res = next.run(req).await;
println!(" middleware after: {}", res.status());
res
}
async fn get_user(State(state): State<AppState>, caller: Caller, Path(id): Path<u32>) -> String {
println!(" handler get_user(id={id})");
format!("user {id} on {}, asked by {}", state.name, caller.0)
}
async fn health() -> &'static str {
println!(" handler health");
"ok"
}
async fn send(app: &Router, method: &str, uri: &str) {
let before = clones();
println!("\n[request] {method} {uri}");
let req = Request::builder()
.method(method)
.uri(uri)
.header("x-caller", "alice")
.body(Body::empty())
.unwrap();
let res = app.clone().oneshot(req).await.unwrap();
let status = res.status();
let allow = res.headers().get("allow").map(|v| v.to_str().unwrap().to_owned());
let body = res.into_body().collect().await.unwrap().to_bytes();
println!(" -> {status} allow={allow:?} body={body:?}");
println!(" AppState clones during this request: {}", clones() - before);
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("[build] start");
let state = AppState { name: "demo" };
let users = get(get_user);
println!("[build] get(get_user) : {}", type_name_of_val(&users));
let app = Router::new()
.route("/users/{id}", users)
.route("/health", get(health));
println!("[build] .route x2 : {}", type_name_of_val(&app));
println!("[build] .layer(...)");
let app = app.layer(LogLayer(from_fn(log_mw)));
println!("[build] .with_state(...)");
let app: Router = app.with_state(state);
println!("[build] after with_state : {}", type_name_of_val(&app));
println!(
"[build] done: Layer::layer calls = {}, AppState clones = {}",
LAYERS.load(Ordering::SeqCst),
clones()
);
let layers_before = LAYERS.load(Ordering::SeqCst);
send(&app, "GET", "/users/7").await;
send(&app, "GET", "/users/abc").await;
send(&app, "GET", "/health").await;
send(&app, "POST", "/health").await;
send(&app, "GET", "/nope").await;
println!(
"\n[after 5 requests] Layer::layer calls = {}",
LAYERS.load(Ordering::SeqCst) - layers_before
);
}
输出
M2 Max、rustc 1.99.0,跑了 3 次,输出逐字节相同(没有任何依赖时序的部分):
[build] start
[build] get(get_user) : axum::routing::method_routing::MethodRouter<walk::AppState>
[build] .route x2 : axum::routing::Router<walk::AppState>
[build] .layer(...)
Layer::layer #1
Layer::layer #2
Layer::layer #3
Layer::layer #4
Layer::layer #5
[build] .with_state(...)
Layer::layer #6
Layer::layer #7
[build] after with_state : axum::routing::Router
[build] done: Layer::layer calls = 7, AppState clones = 6
[request] GET /users/7
middleware before: GET /users/7 matched=Some("/users/{id}")
Caller::from_request_parts
handler get_user(id=7)
middleware after: 200 OK
-> 200 OK allow=None body=b"user 7 on demo, asked by alice"
AppState clones during this request: 5
[request] GET /users/abc
middleware before: GET /users/abc matched=Some("/users/{id}")
Caller::from_request_parts
middleware after: 400 Bad Request
-> 400 Bad Request allow=None body=b"Invalid URL: Cannot parse `abc` to a `u32`"
AppState clones during this request: 5
[request] GET /health
middleware before: GET /health matched=Some("/health")
handler health
middleware after: 200 OK
-> 200 OK allow=None body=b"ok"
AppState clones during this request: 4
[request] POST /health
middleware before: POST /health matched=Some("/health")
middleware after: 405 Method Not Allowed
-> 405 Method Not Allowed allow=Some("GET,HEAD") body=b""
AppState clones during this request: 0
[request] GET /nope
middleware before: GET /nope matched=None
middleware after: 404 Not Found
-> 404 Not Found allow=None body=b""
AppState clones during this request: 0
[after 5 requests] Layer::layer calls = 0
前半段是装配主线,后半段是请求主线。下面逐段对照源码。
装配:类型在变
三行 type_name_of_val 是装配主线的骨架:
| 步骤 | 类型 |
|---|---|
get(get_user) | MethodRouter<AppState> |
.route(...) 两次 | Router<AppState> |
.with_state(state) | Router,即 Router<()>(默认类型参数不打印) |
MethodRouter<AppState> 和 Router<AppState> 里的 AppState 不是「装着一个 AppState」,而是「还欠一个 AppState」:Router 的文档原话是「Router<S> means a router that is missing a state of type S」。欠着状态的路由器不能处理请求:Service<Request<B>> 只为 Router<()> 实现,忘了 with_state 是编译错误,不是运行时错误。这一点在 状态:State 与 FromRef 展开。
get(get_user) 走到 MethodRouter::on,把 handler 包成 BoxedIntoRoute 放进 get 槽:
self.on_endpoint(
filter,
MethodEndpoint::BoxedHandler(BoxedIntoRoute::from_handler(handler)),
)
BoxedIntoRoute::from_handler 存下的是 handler 和一个函数指针 |handler, state| Route::new(Handler::with_state(handler, state)):「拿到状态之后怎么变成 Route」。这时它还不是服务。
Router::route 由 PathRouter::route 完成:分配一个 RouteId,把「路径 → RouteId」插进 matchit,把「RouteId → Endpoint::MethodRouter」放进 HashMap。同一路径第二次 route 会和已有的 MethodRouter 合并,所以 .route("/", get(a)).route("/", post(b)) 能用。
.layer 为什么套了 5 次,with_state 又补了 2 次
Router::layer 把同一个 Layer 分别交给三处:
map_inner!(self, this => RouterInner {
path_router: this.path_router.layer(layer.clone()),
fallback_router: this.fallback_router.layer(layer.clone()),
default_fallback: this.default_fallback,
catch_all_fallback: this.catch_all_fallback.map(|route| route.layer(layer)),
})
Router::new() 时这三处已经各有东西,加上两条路由,一共 7 个服务要套:
| 位置 | 是什么 | 什么时候套 | 次数 |
|---|---|---|---|
两个 MethodRouter 的方法级 fallback | MethodRouter::new 放的 Route,回 405 | .layer 时,当场 | 2 |
fallback_router | new_fallback 在 / 和 /{*__private__axum_fallback} 各注册一个 NotFound | .layer 时,当场 | 2 |
catch_all_fallback | Fallback::Default(Route::new(NotFound)) | .layer 时,当场 | 1 |
两个 get 槽 | BoxedIntoRoute,还欠状态 | with_state 时 | 2 |
已经是 Route 的,Route::layer 当场调用 layer.layer(self),再装箱成新的 Route。还欠状态的 BoxedIntoRoute 套不上去:from_fn 的服务要包住一个真正的服务,而 handler 没有状态就造不出 HandlerService。于是 BoxedIntoRoute::map 只把「怎么套」这个闭包存进 Map,等 into_route 拿到状态、先造出 Route,再把闭包作用上去:
fn into_route(self: Box<Self>, state: S) -> Route<E2> {
(self.layer)(self.inner.into_route(state))
}
这就是输出里 #6、#7 出现在 .with_state(...) 之后的原因。之后发了 5 个请求,Layer::layer 一次也没再调用:服务树在启动时已经造完了。
with_state,layer 什么时候套?平台 M2 Max、rustc 1.99.0。下面的应用没有状态,类型本来就是 Router<()>,不调用 with_state 也能直接 oneshot。LogLayer 和上面的一样,只计数不打印。先不调用 with_state 发 2 个请求,再调用 with_state(()) 发 2 个请求。两次各打印多少?
axum = { version = "=0.8.9", default-features = false }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::sync::atomic::{AtomicUsize, Ordering};
use axum::body::Body;
use axum::extract::Request;
use axum::middleware::{from_fn, Next};
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use tower::{Layer, ServiceExt};
static LAYERS: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)]
struct LogLayer<L>(L);
impl<L: Layer<I>, I> Layer<I> for LogLayer<L> {
type Service = L::Service;
fn layer(&self, inner: I) -> Self::Service {
LAYERS.fetch_add(1, Ordering::SeqCst);
self.0.layer(inner)
}
}
async fn mw(req: Request, next: Next) -> Response {
next.run(req).await
}
async fn get_twice(app: &Router, label: &str) {
let before = LAYERS.load(Ordering::SeqCst);
for _ in 0..2 {
let req = Request::get("/").body(Body::empty()).unwrap();
app.clone().oneshot(req).await.unwrap();
}
println!("{label}: Layer::layer calls during 2 requests = {}", LAYERS.load(Ordering::SeqCst) - before);
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// 没有状态的应用:类型本来就是 Router<()>,不调用 with_state 也能直接用
let app: Router = Router::new()
.route("/", get(|| async { "hi" }))
.layer(LogLayer(from_fn(mw)));
println!("after build: Layer::layer calls = {}", LAYERS.load(Ordering::SeqCst));
get_twice(&app, "without with_state").await;
let app = app.with_state(());
get_twice(&app, "after with_state(())").await;
}
with_state 本身做的事在 Router::with_state → PathRouter::with_state → MethodRouter::with_state → MethodEndpoint::with_state 这四层里,每层都按值往下传一份状态。输出里的 6 次克隆就是这么来的:
| 位置 | 克隆 |
|---|---|
Router::with_state:给 path_router、fallback_router 各一份,最后一份移进 catch_all_fallback | 2 |
PathRouter::with_state:两条路由的 Endpoint::MethodRouter 各一份 | 2 |
MethodEndpoint::with_state:两个 get 槽 into_route(state.clone()) | 2 |
注意 /health 的 handler 根本不用状态,它的 HandlerService 里照样存了一份 AppState:with_state 不看 handler 用不用,Router<S> 里所有 handler 的 S 都是同一个。Router 那一层的 Endpoint::MethodRouter 并没有变成 Route,变的是 MethodRouter 里面的方法槽:MethodEndpoint::BoxedHandler 变成 MethodEndpoint::Route。
请求:值在流动
路由在中间件之前
第一个请求的第一行输出是中间件打印的,而它已经看到了 matched=Some("/users/{id}")。MatchedPath 是 PathRouter::call_with_state 在 matchit 匹配成功后插进 extensions 的,所以中间件执行时路由已经做完了。原因在上一节:Router::layer 不是给整个 Router 包一层,而是给每个路由的 Route 各包一层。请求先由 PathRouter 匹配路径、由 MethodRouter 选出方法槽,然后才进入这个槽里被包好的 Route,中间件在它的最外层。想要在路由之前就执行的中间件(比如改写路径),要把整个 Router 当成服务再包一层,见 layer 与 route_layer。
PathRouter 匹配上以后,还把 matchit 抓到的 id=7 存成 UrlParams 放进 extensions(insert_url_params),后面的 Path 提取器从这里读。
提取器按参数顺序执行
get_user 的参数是 State、Caller、Path。impl_handler! 生成的 call 按参数顺序展开:
fn call(self, req: Request, state: S) -> Self::Future {
let (mut parts, body) = req.into_parts();
Box::pin(async move {
$(
let $ty = match $ty::from_request_parts(&mut parts, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
)*
let req = Request::from_parts(parts, body);
let $last = match $last::from_request(req, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
self($($ty,)* $last,).await.into_response()
})
}
前两个参数走 from_request_parts,最后一个 Path 走 from_request(Path 只实现了 FromRequestParts,axum-core 的 blanket 实现把它也变成 FromRequest,见 FromRequestParts 与 FromRequest)。/users/abc 那次,Caller 成功了,Path<u32> 解析失败,rejection 直接 into_response() 成 400,handler get_user 那一行没有出现:你的函数根本没被调用。中间件照样看到了这个 400,因为 rejection 也是一个普通的响应。
405 和 404 也经过中间件
POST /health:路径匹配上了,MethodRouter 的 post 槽是空的,落到方法级 fallback,回 405,Allow: GET,HEAD(注册 get 时一起记下了 HEAD)。GET /nope:path_router 没匹配上,交给 fallback_router,回 404;fallback 路由不设置 MatchedPath,所以是 None。两个请求都打印了 middleware before,因为上表里这些 fallback 在 .layer 时都被套过。换成 route_layer 再跑,404 那次不再经过中间件,405 那次仍然经过:route_layer 只跳过 fallback_router 和 catch_all_fallback,每个 MethodRouter 连同它的方法级 fallback 照样被套上。区别见 layer 与 route_layer。
每个请求克隆 5 次状态
AppState clones during this request: 5,五次分别发生在:
| # | 位置 | 克隆的是什么 |
|---|---|---|
| 1 | MethodRouter::call_with_state:route.clone().oneshot_inner_owned(req) | 整个 Route,一路克隆到最里面的 HandlerService |
| 2 | FromFn::call:self.inner.clone() | 中间件里面的 Route |
| 3 | next.run(req) 调用里面的 Route,Route::oneshot_inner:self.0.clone() | 又一份 HandlerService |
| 4 | HandlerService::call:self.state.clone() | 交给 Handler::call 的那份状态 |
| 5 | State 的 from_request_parts 调 FromRef,对 T: Clone 的 blanket 实现就是 clone() | 你的 handler 拿到的那份 |
/health 没有 State 参数,少第 5 次,是 4。405、404 的路由里没有 HandlerService,是 0。去掉中间件再跑同一个程序,/users/7 是 3 次、/health 是 2 次:每多一层 from_fn,多 2 次(第 2、3 行)。
所以 crate 文档说「State is cloned for every request. Wrapping your state in Arc makes those clones cheap.」实际的次数取决于中间件的层数,不是 1。状态里放 Arc,或者每个字段本身都是 Arc / Copy,克隆就只是几次引用计数加一。
放进 axum::serve 之后
上面一直用 oneshot,真正跑服务时请求从 axum::serve 进来。handle_connection 每接受一条连接,先调用一次 make_service.call(IncomingStream { .. }) 拿到这条连接的服务,用 TowerToHyperService 包好交给 hyper-util 的 auto::Builder(后半段见 hyper 卷)。直接传 Router 时,这个 call 是 Router 对 IncomingStream 的 Service 实现:
fn call(&mut self, _req: serve::IncomingStream<'_, L>) -> Self::Future {
// call `Router::with_state` such that everything is turned into `Route` eagerly
// rather than doing that per request
std::future::ready(Ok(self.clone().with_state(())))
}
每条连接一次 with_state(())。如果交进来的 Router 里还有 BoxedIntoRoute(无状态的应用、从没调用过 with_state),layer 就在这里每条连接套一次。下面用真实的监听端口验证,客户端是 hyper 自己的 client::conn,开两条连接,每条发 3 个请求:
axum = { version = "=0.8.9", default-features = false, features = ["tokio", "http1"] }
tower = { version = "=0.5.3", features = ["util"] }
hyper = { version = "=1.12.0", features = ["client", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "net"] }
LogLayer、mw 和动脑筋里的一样,main 换成:
#[tokio::main(flavor = "current_thread")]
async fn main() {
let app: Router = Router::new()
.route("/", get(|| async { "hi" }))
.layer(LogLayer(from_fn(mw)));
println!("after build: {}", layers());
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
for conn_no in 1..=2 {
let before = layers();
let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let (mut sender, conn) = hyper::client::conn::http1::handshake(TokioIo::new(stream)).await.unwrap();
tokio::spawn(conn);
for _ in 0..3 {
let req = Request::get("/").header("host", "x").body(Body::empty()).unwrap();
let res = sender.send_request(req).await.unwrap();
assert_eq!(res.status(), 200);
}
println!("connection {conn_no}, 3 requests: Layer::layer calls = {}", layers() - before);
}
}
(layers() 读 LAYERS 计数,TokioIo 来自 hyper_util::rt。)跑了 3 次,输出相同:
after build: 4
connection 1, 3 requests: Layer::layer calls = 1
connection 2, 3 requests: Layer::layer calls = 1
每条连接套一次,同一条连接上的 3 个请求共用。把 let app = app.into_make_service(); 放在 serve 之前再跑,into_make_service 在转换时就调用了一次 with_state(()),之后每条连接只是克隆现成的服务树(3 次输出相同):
after into_make_service: 5
connection 1, 3 requests: Layer::layer calls = 0
connection 2, 3 requests: Layer::layer calls = 0
有状态的应用不受影响:with_state(state) 之后已经没有 BoxedIntoRoute,每条连接的 with_state(()) 只是把 Route 原样搬一遍。层里如果有「每个实例各自一份」的东西(比如 tower ConcurrencyLimit 的信号量),套的次数就决定了它们的作用范围。
axum::serve 曾经每个请求都重新套一遍 layer上面那段 call 里的 with_state(()) 是 #2586(「Fix layers being cloned for each request」)加的,在那之前这里只是 std::future::ready(Ok(self.clone()))。直接 axum::serve(listener, router) 的无状态应用,路由器里一直留着 BoxedIntoRoute,每个请求在 MethodRouter::call_with_state 里现场 into_route,layer 也就每个请求套一遍,正是动脑筋里「without with_state」那一行的情形。
issue #2582 发现的后果是:MethodRouter::layer 文档里 get(handler).layer(ConcurrencyLimitLayer::new(64)) 的例子「compiles, but no concurrency limit is enforced. This is because each request to GET / is sent through a different ConcurrencyLimit instance, each with its own semaphore.」PR 的说明是:「Turns out we were missing a .with_state(()) to eagerly apply layers like we used to do for .into_make_service(). This might also have a performance impact.」CHANGELOG 记为「Fixed layers being cloned when calling axum::serve directly with a Router or MethodRouter」,随 axum 0.7.5(2024-03-24)发布。into_make_service 里那段一模一样的注释,就是 PR 说的「like we used to do」。
动脑筋答案
after build: Layer::layer calls = 4
without with_state: Layer::layer calls during 2 requests = 2
after with_state(()): Layer::layer calls during 2 requests = 0
跑了 3 次,输出相同。逐行看:
after build: 4:一个MethodRouter的方法级 fallback 1 次,fallback_router的两个NotFound2 次,catch_all_fallback1 次。唯一的 handler 还是BoxedIntoRoute,它的那次被推迟了。without with_state: 2:每个请求 1 次。Router<()>的call把()当状态传进MethodRouter::call_with_state,get槽还是MethodEndpoint::BoxedHandler,于是走handler.clone().into_route(state),现场造Route、现场套layer,用完就扔。类型上Router<()>和with_state(())之后完全一样,区别只在里面的方法槽有没有被提前变成Route。after with_state(()): 0:with_state(())把方法槽变成了MethodEndpoint::Route,layer在那一刻套好,之后每个请求只是克隆。
所以「装配主线在启动时做完」有一个前提:with_state 被调用过。有状态的应用一定会调用它;无状态的应用,axum::serve 和 into_make_service 会替你调用。自己拿 Router<()> 去 oneshot、或者交给别的服务器时,先 .with_state(()) 一下。
小结
- 装配主线里类型在变:
get得到MethodRouter<S>,route得到Router<S>,with_state得到Router<()>;S表示「还欠一个S」。 - handler 在
with_state之前是BoxedIntoRoute(handler 加一个「拿到状态后怎么变成Route」的函数指针);.layer对它只记下闭包,拿到状态时再套。已经是Route的(各种 fallback)当场套。 with_state逐层按值传状态,本例启动时克隆 6 次;不用状态的 handler 也存一份。Router::layer包的是每个路由的Route,所以中间件在路由之后、提取器之前执行,能看到MatchedPath;405、404 的 fallback 也被包了。- 提取器按参数顺序执行,失败时 rejection 直接变成响应,你的函数不会被调用。
- 本例每个请求克隆状态 5 次:
Route克隆 1 次、每层from_fn2 次、HandlerService::call1 次、State提取 1 次。状态要放Arc。 axum::serve每条连接调用一次with_state(())(#2586);没有with_state过的Router<()>直接oneshot,layer会每个请求重套一遍。