Route 与类型擦除
主线位置:两条主线都在。装配主线上,每个 handler、服务、套上的 layer 最后都变成同一个类型
Route,Router才能把它们放进同一张表;还欠状态的 handler 先擦成BoxedIntoRoute,等with_state时再变。请求主线上,每个请求都要克隆一次Route、经过一个装箱的 future,最后由最外层的RouteFuture整理响应头。
flowchart TB
subgraph build["启动时"]
h["handler H, T"] --> bir["BoxedIntoRoute S"]
svc["服务 T"] --> rnew["Route::new"]
bir -->|".layer:只记下"| bmap["Map:handler 加闭包"]
bir -->|"with_state"| rnew
bmap -->|"with_state"| rnew
rnew --> route["Route:盒子"]
route -->|".layer:当场套"| route
end
subgraph perreq["每个请求"]
rc["克隆 Route"] --> os["oneshot:future 装箱"]
os --> rf["RouteFuture"]
rf --> fix["最外层整理响应头"]
end
route --> rc
Router 的路由表是 HashMap<RouteId, Endpoint<S>>(Router 的内部结构),MethodRouter 的每个方法槽也是同一个类型。两个 handler 只要参数不同,类型就不同;套了不同 layer 的服务,类型也不同。要把它们放进同一张表,只能擦掉类型。axum 擦两次:拿到状态之前擦成 BoxedIntoRoute,拿到之后擦成 Route。
Route:一个能克隆的盒子
Route 和 Route::new:
pub struct Route<E = Infallible>(BoxCloneSyncService<Request, Response, E>);
impl<E> Route<E> {
pub(crate) fn new<T>(svc: T) -> Self
where
T: Service<Request, Error = E> + Clone + Send + Sync + 'static,
T::Response: IntoResponse + 'static,
T::Future: Send + 'static,
{
Self(BoxCloneSyncService::new(MapIntoResponse::new(svc)))
}
MapIntoResponse在 future 完成时调into_response()。服务返回什么都行,只要实现了IntoResponse,进了盒子之后响应类型统一是 axum 的Response。- 盒子是 tower 的
BoxCloneSyncService:服务本身装进Box<dyn ..>,Clone靠clone_box,每次call返回的 future 用Box::pin装箱(tower 卷 类型擦除 逐行讲过)。选带Sync的盒子,对应 0.8.0 的一条破坏性改动:「RequireSyncfor all handlers and services added toRouterandMethodRouter」(#2473)。这样Router本身是Sync,内部才能放进Arc共享(Router 的内部结构 的冷知识)。 - 错误类型
E默认是Infallible。Router里存的都是Route<Infallible>,所以.layer套上的中间件错误类型必须能转成Infallible,见 layer 与 route_layer。
Route::layer 把旧的 Route 整个交给 layer,再装进一个新盒子:
let layer = (MapErrLayer::new(Into::into), layer);
Route::new(layer.layer(self))
套 n 层就是 n+1 层盒子,最里面是你的服务,每层盒子里是「layer 产出的服务包着下一层 Route」。
每个请求:克隆,然后 oneshot
Router 处理请求时只有 &self(同一个 Router 被很多连接共享),而 tower 的 Service::call 要 &mut self。所以选中的 Route 先克隆一份,再对这一份做 oneshot。克隆由调用方做:MethodRouter::call_with_state 是 route.clone().oneshot_inner_owned(req),PathRouter 里的 Endpoint::Route 是 route.clone().call_owned(req)。两个 _owned 方法自己不再克隆:
/// Variant of [`Route::call`] that takes ownership of the route to avoid cloning.
pub(crate) fn call_owned(self, req: Request<Body>) -> RouteFuture<E> {
let req = req.map(Body::new);
self.oneshot_inner_owned(req).not_top_level()
}
pub(crate) fn oneshot_inner(&mut self, req: Request) -> RouteFuture<E> {
let method = req.method().clone();
RouteFuture::new(method, self.0.clone().oneshot(req))
}
/// Variant of [`Route::oneshot_inner`] that takes ownership of the route to avoid cloning.
pub(crate) fn oneshot_inner_owned(self, req: Request) -> RouteFuture<E> {
let method = req.method().clone();
RouteFuture::new(method, self.0.oneshot(req))
}
但 Route 作为 tower Service 被别人调用时(impl Service for Route),走的是 oneshot_inner(&mut self),里面又有一个 self.0.clone()。谁会这样调用 Route?layer 产出的服务:它包着下一层 Route,处理请求时调用 inner.call(req)。所以每多一层 layer,内层的盒子就在请求时再被克隆一次。
用一个计数的服务和计数的全局分配器量一下。Svc 每次 Clone、call 都计数;layer 用 tower 的 Identity,它的 layer 原样返回内层服务,自己不加任何东西,量到的全是 axum 的包装。每种配置先跑一次热身,再量第二次请求从 call 到拿到 Response:
axum = { version = "=0.8.9", default-features = false }
tower = "=0.5.3"
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
use std::alloc::{GlobalAlloc, Layout, System};
use std::convert::Infallible;
use std::future::{ready, Ready};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::task::{Context, Poll};
use axum::body::Body;
use axum::extract::Request;
use axum::routing::get_service;
use axum::Router;
use tower::layer::util::Identity;
use tower::Service;
// 数堆分配次数
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::SeqCst);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
// 数 Svc 被 clone、被 call 了几次
static CLONES: AtomicUsize = AtomicUsize::new(0);
static CALLS: AtomicUsize = AtomicUsize::new(0);
// 带一个字段:零大小类型装箱不分配,量不出来
struct Svc(u64);
impl Clone for Svc {
fn clone(&self) -> Self {
CLONES.fetch_add(1, Ordering::SeqCst);
Svc(self.0)
}
}
impl Service<Request> for Svc {
type Response = &'static str;
type Error = Infallible;
type Future = Ready<Result<&'static str, Infallible>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request) -> Self::Future {
CALLS.fetch_add(1, Ordering::SeqCst);
ready(Ok("svc"))
}
}
fn load(c: &AtomicUsize) -> usize {
c.load(Ordering::SeqCst)
}
async fn measure(label: &str, app: Router) {
// 先跑一次热身,再量第二次
for round in 0..2 {
let req = Request::get("/").body(Body::empty()).unwrap();
let mut svc = app.clone();
let (c0, k0, a0) = (load(&CLONES), load(&CALLS), load(&ALLOCS));
let res = svc.call(req).await.unwrap();
let (c1, k1, a1) = (load(&CLONES), load(&CALLS), load(&ALLOCS));
assert_eq!(res.status(), 200);
if round == 1 {
println!("{label:<34} clones={} calls={} allocs={}", c1 - c0, k1 - k0, a1 - a0);
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
measure("route_service(Svc)", Router::new().route_service("/", Svc(1))).await;
measure("get_service(Svc)", Router::new().route("/", get_service(Svc(1)))).await;
measure(
"get_service(Svc) + 1 layer",
Router::new().route("/", get_service(Svc(1))).layer(Identity::new()),
)
.await;
}
M2 Max、rustc 1.99.0,跑了 3 次,输出相同:
route_service(Svc) clones=1 calls=1 allocs=8
get_service(Svc) clones=1 calls=1 allocs=9
get_service(Svc) + 1 layer clones=2 calls=1 allocs=12
- 没有 layer 时,每个请求克隆 1 次服务。这 1 次克隆是盒子的
clone_box,要 1 次分配;oneshot调用盒子,Box::pin装 future,又 1 次。剩下的分配和路由无关(插进 extensions 的路径参数、构造响应等),三种配置都一样。 route_service比get_service少 1 次分配:它的RouteFuture不是「最外层」,不设Content-Length,省了"3"这个头值。下一节讲。- 加 1 层 layer,服务的克隆变成 2 次,分配多了 3 次:外层盒子克隆时连同里面的内层盒子一起克隆(2 次),外层
call装箱 future(1 次),中间件调内层Route::call时内层盒子再克隆一次(1 次)、再装箱 future(1 次),一共 5 次,比没有 layer 时的 2 次多 3 次。
Identity 是最便宜的 layer。真实的中间件还有自己的开销,比如 from_fn 每个请求还会克隆它包着的服务、把 Next 再装一次箱(FromFn::call,见 from_fn 与它的兄弟)。服务里如果带着状态,克隆服务就是克隆状态,这就是 状态:State 与 FromRef 和 一个请求穿过 axum 里每个请求克隆状态好几次的来源。
Identity,服务被克隆几次,分配比没有 layer 时多几次?平台 M2 Max、rustc 1.99.0。把上面程序的 main 换成只量一种配置:
#[tokio::main(flavor = "current_thread")]
async fn main() {
measure(
"get_service(Svc) + 2 layers",
Router::new()
.route("/", get_service(Svc(1)))
.layer(Identity::new())
.layer(Identity::new()),
)
.await;
}
clones= 是几?allocs= 比 get_service(Svc) 那一行的 9 多几?
还没有状态的 handler:BoxedIntoRoute
Route::new 要一个能直接处理请求的服务。handler 在 with_state 之前造不出这样的服务:Handler::with_state(handler, state) 才得到 HandlerService(Handler trait)。get(handler) 时手里只有 handler,于是先擦成 BoxedIntoRoute:
pub(crate) struct BoxedIntoRoute<S, E>(Box<dyn ErasedIntoRoute<S, E>>);
// ...
pub(crate) trait ErasedIntoRoute<S, E>: Send + Sync {
fn clone_box(&self) -> Box<dyn ErasedIntoRoute<S, E>>;
fn into_route(self: Box<Self>, state: S) -> Route<E>;
#[allow(dead_code)]
fn call_with_state(self: Box<Self>, request: Request, state: S) -> RouteFuture<E>;
}
pub(crate) struct MakeErasedHandler<H, S> {
pub(crate) handler: H,
pub(crate) into_route: fn(H, S) -> Route,
}
ErasedIntoRoute 只承诺两件事:能克隆自己(clone_box,和 tower 的 BoxCloneService 是同一个技巧),给一份 S 就能变成 Route(into_route)。trait 只带 S 和 E,handler 的类型 H 和标记类型 T 都不出现,所以 MethodRouter<S> 的方法槽能装任何 handler。
MakeErasedHandler 是唯一被构造的实现。from_handler 填进去的 into_route 是一个不捕获任何东西的闭包 |handler, state| Route::new(Handler::with_state(handler, state)),所以能当成函数指针 fn(H, S) -> Route 存。T 在这个闭包被单态化的那一刻用掉了:它知道 H: Handler<T, S>,结构体本身不需要再带 T。
BoxedIntoRoute 一生只有三种走向:
| 时机 | 调用 | 结果 |
|---|---|---|
.layer(...) 时还没状态 | BoxedIntoRoute::map | 包成 Map,记下「拿到 Route 后怎么套 layer」的闭包,仍是 BoxedIntoRoute |
with_state(state) | MethodEndpoint::with_state 调 handler.into_route(state.clone()) | 方法槽从 BoxedHandler 变成 Route,此后和服务没有区别 |
从没 with_state 过的 Router<()> 直接处理请求 | MethodRouter::call_with_state 里的 handler.clone().into_route(state) | 每个请求现场造一个 Route,用完就扔 |
第一行套 layer 的推迟和第三行的代价,一个请求穿过 axum 用计数的 layer 实测过:无状态应用不调用 with_state 时,layer 每个请求重套一次。Router 的内部结构 里 Debug 打出的 get: BoxedHandler 就是还没走到第二行的状态。
boxed.rs 里另有一个 MakeErasedRouter(把整个 Router<S> 擦成 ErasedIntoRoute)和 trait 上的 call_with_state,都标着 #[allow(dead_code)],0.8.9 里没有任何地方构造或调用它们。它们是 nest 还把子路由器整个存起来时的用法;#1711(0.6.13)把 nest 改成把路由摊平插进父路由器,删掉了构造它的 BoxedIntoRoute::from_router。
RouteFuture:只有最外层整理响应
RouteFuture 包着 Oneshot,另外记着请求方法、可能要补的 Allow 头,和一个 top_level 标记:
pub struct RouteFuture<E> {
#[pin]
inner: Oneshot<BoxCloneSyncService<Request, Response, E>, Request>,
method: Method,
allow_header: Option<Bytes>,
top_level: bool,
}
它的 poll 在内层完成后改响应:
let mut res = ready!(this.inner.poll(cx))?;
if *this.method == Method::CONNECT && res.status().is_success() {
// From https://httpwg.org/specs/rfc9110.html#CONNECT:
// > A server MUST NOT send any Transfer-Encoding or
// > Content-Length header fields in a 2xx (Successful)
// > response to CONNECT.
if res.headers().contains_key(&CONTENT_LENGTH)
|| res.headers().contains_key(&header::TRANSFER_ENCODING)
|| res.size_hint().lower() != 0
{
error!("response to CONNECT with nonempty body");
res = res.map(|_| Body::empty());
}
} else if *this.top_level {
set_allow_header(res.headers_mut(), this.allow_header);
// make sure to set content-length before removing the body
set_content_length(res.size_hint(), res.headers_mut());
if *this.method == Method::HEAD {
*res.body_mut() = Body::empty();
}
}
三件事:
- CONNECT 的 2xx:只要带了
Content-Length/Transfer-Encoding头,或者 body 可能非空,就换成空 body,并打一条error!日志。不看top_level。Router 的内部结构 里CONNECT那次响应 body 为空就是这里清掉的。 - 补头:
Allow(只在 405 时由MethodRouter交进来,MethodRouter);set_content_length在响应还没有Content-Length、且size_hint是精确值时,按 body 长度写上。流式 body 的size_hint不精确,不写。 - HEAD:先补
Content-Length,再把 body 换成空的。顺序不能反:反过来写的是空 body 的长度 0。注意 handler 照常执行完、body 照常生成,只是被丢掉。
后两件只在 top_level 为真时做。RouteFuture::new 默认是 true,not_top_level 把它关掉:
| 创建方式 | 谁在用 | top_level |
|---|---|---|
oneshot_inner_owned | MethodRouter 的方法槽和方法级 fallback、catch_all_fallback | 是 |
call_owned | PathRouter 里的 Endpoint::Route:route_service、nest_service、fallback_router 里的服务 | 否 |
Route::call(Service 实现) | layer 产出的服务调内层 Route | 否 |
最后一行的意思是:套了 layer 的路由,只有最外层的 RouteFuture 整理响应,内层的不动。中间件改了 body,Content-Length 按改过的算。route_service 那一行意味着:直接挂的 tower 服务,axum 不替它补 Content-Length、不清 HEAD 的 body。
下面用 oneshot 和真实的监听端口对比四种路由对 HEAD 的响应:get handler、route_service 挂的服务、流式 body、以及一个在 handler 之后把 body 换长的 map_response 中间件。线上那一半用原始的 TCP 写请求、读到连接关闭,看真正发出去的字节:
axum = { version = "=0.8.9", default-features = false, features = ["tokio", "http1"] }
tower = { version = "=0.5.3", features = ["util"] }
http-body-util = "=0.1.5"
futures = "=0.3.34"
tokio = { version = "=1.54.1", features = ["macros", "rt", "net", "io-util"] }
use std::convert::Infallible;
use axum::body::Body;
use axum::extract::Request;
use axum::middleware::map_response;
use axum::response::Response;
use axum::routing::get;
use axum::Router;
use http_body_util::BodyExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tower::{service_fn, ServiceExt};
fn app() -> Router {
let svc = service_fn(|_req: Request| async { Ok::<_, Infallible>("hello world") });
let chunks = || async {
let s = futures::stream::iter([Ok::<_, Infallible>("hello "), Ok("world")]);
Body::from_stream(s)
};
Router::new()
.route("/get", get(|| async { "hello world" }))
.route_service("/svc", svc)
.route("/stream", get(chunks))
.route("/mw", get(|| async { "hi" }).layer(map_response(longer)))
}
// 中间件在 handler 之后把 body 换成更长的
async fn longer(mut res: Response) -> Response {
*res.body_mut() = Body::from("hello, middleware");
res
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let paths = ["/get", "/svc", "/stream", "/mw"];
println!("== oneshot (no server)");
for path in paths {
let req = Request::head(path).body(Body::empty()).unwrap();
let res = app().oneshot(req).await.unwrap();
let cl = res.headers().get("content-length").cloned();
let body = res.into_body().collect().await.unwrap().to_bytes();
println!("HEAD {path:<8} content-length={cl:?} body={body:?}");
}
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() });
println!("== on the wire");
for path in paths {
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let head = format!("HEAD {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
stream.write_all(head.as_bytes()).await.unwrap();
let mut raw = Vec::new();
stream.read_to_end(&mut raw).await.unwrap();
let text = String::from_utf8_lossy(&raw);
// date 每次不同,不打印
let lines: Vec<&str> = text.split("\r\n").filter(|l| !l.starts_with("date:")).collect();
println!("HEAD {path}: {} bytes read\n {lines:?}", raw.len());
}
}
跑了 3 次,输出相同(字节数里含被过滤掉的 date 行,它的长度是固定的):
== oneshot (no server)
HEAD /get content-length=Some("11") body=b""
HEAD /svc content-length=None body=b"hello world"
HEAD /stream content-length=None body=b""
HEAD /mw content-length=Some("17") body=b""
== on the wire
HEAD /get: 136 bytes read
["HTTP/1.1 200 OK", "content-type: text/plain; charset=utf-8", "content-length: 11", "connection: close", "", ""]
HEAD /svc: 136 bytes read
["HTTP/1.1 200 OK", "content-type: text/plain; charset=utf-8", "connection: close", "content-length: 11", "", ""]
HEAD /stream: 75 bytes read
["HTTP/1.1 200 OK", "connection: close", "", ""]
HEAD /mw: 136 bytes read
["HTTP/1.1 200 OK", "content-type: text/plain; charset=utf-8", "content-length: 17", "connection: close", "", ""]
oneshot 那一半是 axum 交出去的 Response:
/get:Content-Length: 11保留,body 清空。/svc:route_service不是最外层,原样交出 11 字节的 body,没有Content-Length。/stream:Body::from_stream的size_hint不精确,不写Content-Length;body 照样清空。/mw:handler 返回 2 字节的"hi",中间件换成 17 字节。内层RouteFuture不是最外层,什么都没写;最外层按换过的 body 写了 17。
线上那一半是 hyper 写出去的字节,四个都没有 body。hyper 的 HTTP/1 服务端自己也处理 HEAD:响应头里没有长度时,body 长度已知就由 hyper 写 content-length(role.rs),然后对 HEAD 把要发的 body 强制成 0 字节(can_have_body)。/svc 那一行的 content-length: 11 排在 connection: close 后面,就是 hyper 补的;其余三行的长度头来自 axum。/stream 两边都不知道长度,HEAD 又不能用 chunked,于是没有任何长度头。
问: 线上四个结果和不清 body 时一样,hyper 自己就会丢掉 HEAD 的 body、补长度。axum 还做这一遍图什么?
答: 图的是 hyper 之前的那一段。Router 是一个普通的 tower Service,它的响应不一定直接交给 hyper:oneshot 测试拿到的就是 RouteFuture 的输出(上面的 /svc 行说明,不清的话测试会读到 body);套在整个 Router 外面的 tower 中间件看到的也是它。axum 在路由层把 HEAD 响应整理成「有长度、无 body」,外面的人看到的就和线上一致,不依赖服务器怎么处理。代价是 handler 仍然完整执行、body 生成后被丢弃;想省掉这部分工作,给 HEAD 单独注册一个 head(...) handler,MethodRouter 会先试 head 槽。
这一步只在顶层的 RouteFuture 上做,原因要从它的来历说起。
top_level 是为了让中间件改 body 时 Content-Length 不出错「先补 Content-Length 再清 body」来自 #734(修 issue #730,随 axum 0.5.0 发布)。PR 的说明:「Hyper will automatically set the length if the body as an exact size ... but since axum would use an empty body for HEAD requests the content-length would be set to 0 instead of the length of the original response body.」所以 axum 要在清空之前自己写上长度。
之后很长一段时间,每一个 RouteFuture 都这样做,包括被中间件包在里面的那些。#2897 的说明指出了问题:「if this occurs before middleware is run then should the middleware add a body to the request, Axum will avoid overwriting the content-length and so the user is stuck with an incorrect content-length, leading to panics in Hyper.」内层先按旧 body 写了长度,set_content_length 见到已有的头就不再改。修法是「Only set content-length for top-level Routes」,也就是 top_level 字段和 not_top_level()。CHANGELOG 记为「Avoid setting content-length before middleware. This allows middleware to add bodies to requests without needing to manually set content-length」,随 axum 0.8.0 发布(预发布版 0.8.0-rc.1 里已有)。上面 /mw 那一行的 17 就是这个修复的结果。
动脑筋答案
get_service(Svc) + 2 layers clones=3 calls=1 allocs=16
跑了 3 次,输出相同。服务克隆 3 次:外层 MethodRouter 克隆整条盒子链时 1 次,两层中间件各调一次内层 Route::call,各 1 次。分配是 16,比 9 多 7。按上一节的数法,盒子的克隆:最外层克隆 3 层盒子,中间那层 Route::call 克隆 2 层,最里层 1 层,共 6 次;三层盒子各装箱一次 future,3 次。合计 9 次,没有 layer 时是 2 次,差 7。
| layer 层数 | 服务克隆 | 和路由有关的分配 |
|---|---|---|
| 0 | 1 | 2 |
| 1 | 2 | 5 |
| 2 | 3 | 9 |
服务克隆随层数线性增长,盒子的克隆按 1+2+…+(n+1) 增长,因为每一层 Route::call 都要克隆它下面的整条链。层数少时这些都只是几十字节的小分配;在乎时,可以先用 tower::ServiceBuilder 把几个 layer 组合成一个,再 .layer 一次,axum 只包一层 Route:把上面的两次 .layer(Identity::new()) 换成一次 .layer(ServiceBuilder::new().layer(Identity::new()).layer(Identity::new())),实测服务克隆 2 次,和只套一层时相同。
小结
Route是BoxCloneSyncService<Request, Response, E>加一层MapIntoResponse:任何响应类型进去,出来都是Response;能克隆、Sync,Router因此能把不同类型的服务放进同一张表。- 每个请求先克隆选中的
Route(Router只有&self),再oneshot,future 装箱一次。没有 layer 时服务克隆 1 次、和路由有关的分配 2 次。 Route::layer把旧Route包进新盒子;请求时中间件调内层的Route::call会再克隆一次。n 层 layer:服务克隆 n+1 次,盒子克隆 (n+1)(n+2)/2 次,future 装箱 n+1 次。- 欠状态的 handler 存成
BoxedIntoRoute:Box<dyn ErasedIntoRoute>,实现是MakeErasedHandler { handler, into_route: fn(H, S) -> Route },H、T都被擦掉;.layer时包成Map,with_state时into_route成Route。 RouteFuture在 CONNECT 2xx 时清空 body;只有最外层(top_level)补Allow、按精确的size_hint补Content-Length、清空 HEAD 的 body。route_service/nest_service挂的服务和被中间件包住的内层都不是最外层(#2897)。- 线上 hyper 也会丢掉 HEAD 的 body、补长度;axum 这一步让
oneshot测试和外层中间件看到的响应与线上一致。