Hyper 与 Tower:工业级 HTTP 栈
第3章 Layer 与 ServiceBuilder:类型级中间件组合
第3章 Layer 与 ServiceBuilder:类型级中间件组合
3.1 问题:洋葱要正着剥
上一章结尾我们手写了一段假代码:
let svc = Retry::new(policy,
RateLimit::new(rate,
Timeout::new(MyHandler)));
这个写法至少有两件事让人不舒服。
第一,顺序反了。请求先过 Retry、再过 RateLimit、再过 Timeout、最后到达 MyHandler——这是它真实的执行顺序,但代码写出来却是反向嵌套的。读者看到 Retry::new(..., Timeout::new(handler)) 必须在大脑里把整个栈倒过来才能理解语义。
第二,类型会越堆越深。两层还勉强能看,堆到十层就变成无法排版的一团 <<<<>>>>。编辑器、rust-analyzer、编译错误信息一起遭罪。
Tower 的回答是 Layer trait + ServiceBuilder。它们是一套把嵌套结构拍平成链式方法的小语法糖。你在 Axum、Tonic 里看到的那种:
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(CorsLayer::permissive())
.service(router)
顺着写、按执行顺序读、每一行只讲一件事。背后不是运行时魔法,是两个 trait 加起来不到一百行代码。这一章我们把它读透。
3.2 Layer trait:只有四行
// tower-layer/src/lib.rs:100-106
pub trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}
一个关联类型,一个方法,四行。
它表达的意思是:
我知道怎么”包装”一个
S——喂给我一个S,我还给你一个Self::Service。
如果 Service 是”async fn(Req) -> Res”,那么 Layer 就是”fn(Service) -> Service”——Service 的 Service。用函数式的话说,Layer 是 Service 范畴上的一个函子(functor),它把一个 Service 映射成另一个。
读一个例子(把上一章 Timeout 的 layer 版本展开):
// tower/src/timeout/layer.rs 全文
#[derive(Debug, Clone, Copy)]
pub struct TimeoutLayer {
timeout: Duration,
}
impl TimeoutLayer {
pub const fn new(timeout: Duration) -> Self {
TimeoutLayer { timeout }
}
}
impl<S> Layer<S> for TimeoutLayer {
type Service = Timeout<S>;
fn layer(&self, service: S) -> Self::Service {
Timeout::new(service, self.timeout)
}
}
Layer 自己不是 Service——它只是一个”工厂”,把任意 S 翻译成 Timeout<S>。TimeoutLayer::new 只配置”超时多久”,真正产生 Service 发生在 layer(svc) 被调用的那一刻。
这种”配置与实例化分离”的设计,是整个 Tower 中间件的统一模式。每个中间件都有一对结构体:
| 配置结构体(Layer) | 实例结构体(Service) |
|---|---|
TimeoutLayer | Timeout<S> |
RateLimitLayer | RateLimit<S> |
BufferLayer | Buffer<S, Req> |
ConcurrencyLimitLayer | ConcurrencyLimit<S> |
RetryLayer<P> | Retry<P, S> |
FilterLayer<F> | Filter<S, F> |
左边只描述”怎么配置”,右边是真正跑业务的 Service。这让配置本身可以独立存在、可以 Clone、可以存到 Vec 里、可以通过 serde 反序列化——而不需要提前绑定到一个具体的 S。
3.2.1 为什么不是直接给 Service 加构造函数?
你可能会问:既然每个 Middleware 都有 MiddlewareLayer 和 Middleware 成对出现,为什么不直接用 Middleware::new(svc, config) 的构造函数,何必多出一个 Layer trait?
答案在”组合”二字。如果没有 Layer trait,每一个”组合工具”都要手动适配每一个中间件——ServiceBuilder::buffer 知道 Buffer::new 的参数顺序、ServiceBuilder::timeout 知道 Timeout::new……每加一个新中间件,ServiceBuilder 都得改代码。
有了 Layer trait 之后,组合工具只需要面对一个统一接口:fn layer(inner) -> wrapped。ServiceBuilder 完全不用知道每个中间件的构造函数长什么样——它只负责”把一个 Layer 堆到下一个 Layer 上面”,剩下的事情由每个 Layer 自己的 impl Layer for MyLayer 负责。
这就是工程上著名的 open-closed 原则:对扩展开放(任何人可以实现 Layer),对修改封闭(ServiceBuilder 不需要修改)。
3.3 Stack:两个 Layer 叠起来
既然 Layer 是”Service 到 Service 的函数”,把两个 Layer 组合起来,就是函数复合。Tower 把它叫做 Stack:
// tower-layer/src/stack.rs:21-53
pub struct Stack<Inner, Outer> {
inner: Inner,
outer: Outer,
}
impl<S, Inner, Outer> Layer<S> for Stack<Inner, Outer>
where
Inner: Layer<S>,
Outer: Layer<Inner::Service>,
{
type Service = Outer::Service;
fn layer(&self, service: S) -> Self::Service {
let inner = self.inner.layer(service);
self.outer.layer(inner)
}
}
拆开看:
Stack<Inner, Outer>自己还是一个Layer<S>——它本身还能被继续叠。layer(s)的实现是”先让 Inner 包一层,再让 Outer 包一层”。- where 子句是关键:
Inner: Layer<S>说 Inner 能包 S;Outer: Layer<Inner::Service>说 Outer 能包 Inner 产出的那个 Service。类型系统在这里钉住了顺序——你不能把两个不兼容的 Layer 叠起来,compiler 会在类型检查阶段拒绝。
注意字段名的语义倒置:inner 存的其实是”先被应用的”那个 layer,outer 存的是”后被应用的”。这两个词的含义是从”最终产出的 Service 洋葱”角度来看:inner 更靠近业务核心、outer 更靠近请求入口。
这件事第一次看容易绕。再用一张图:
ServiceBuilder::new()
.layer(A) // outer-most
.layer(B)
.layer(C) // inner-most
.service(svc)
│
▼
请求流向: req ─> A ─> B ─> C ─> svc ─> C' ─> B' ─> A' ─> resp
先加入 builder 的 layer 在外、后加入的在内。请求从外向内穿透,响应再从内向外返回。Stack<Inner, Outer> 的字段名正好反映了洋葱结构——inner 在里、outer 在外。
3.3.1 Identity:零元
// tower-layer/src/identity.rs:22-45
pub struct Identity { _p: () }
impl Identity {
pub const fn new() -> Identity { Identity { _p: () } }
}
impl<S> Layer<S> for Identity {
type Service = S;
fn layer(&self, inner: S) -> Self::Service { inner }
}
一个”什么都不做”的 Layer。它的存在是出于代数完整性——任何 monoid(幺半群)结构都需要一个零元。Stack 是乘法,Identity 就是 1。
看这个 ServiceBuilder::new:
// tower/src/builder/mod.rs:117-123
impl ServiceBuilder<Identity> {
pub const fn new() -> Self {
ServiceBuilder { layer: Identity::new() }
}
}
刚创建出来的 ServiceBuilder<Identity> 表示”还没加任何中间件的空栈”。当你调用 .layer(A),类型变为 ServiceBuilder<Stack<Identity, A>>;再调用 .layer(B),变成 ServiceBuilder<Stack<Stack<Identity, A>, B>>。每次调用都把类型”加深”一层。
Stack<Identity, A> 和直接 A 在语义上是等价的(因为 Identity.layer(s) = s),但 compiler 不会把它们视为同一个类型。这一般不是问题——只是单态化之后会多生成一层 wrapper struct。LLVM 的 inliner 在 release 模式下会把这一层彻底消除,但 debug 版本的类型名会很长。
3.3.2 tuple impls:糖里糖外
tower-layer 还给 tuple 写了一系列 Layer impl(tower-layer/src/tuple.rs):
impl<S, L1, L2> Layer<S> for (L1, L2)
where L1: Layer<L2::Service>, L2: Layer<S>,
{
type Service = L1::Service;
fn layer(&self, s: S) -> Self::Service {
let (l1, l2) = self;
l1.layer(l2.layer(s))
}
}
impl<S, L1, L2, L3> Layer<S> for (L1, L2, L3)
where L1: Layer<L2::Service>, L2: Layer<L3::Service>, L3: Layer<S>,
{ ... }
以及更多元组元数(最多到 16 元组)。一个空元组 () 也是 Layer,它和 Identity 效果一样:
impl<S> Layer<S> for () {
type Service = S;
fn layer(&self, s: S) -> Self::Service { s }
}
有了 tuple impls,你可以不用 ServiceBuilder 也能快速组合:
let layers = (LogLayer::new(), TimeoutLayer::new(d), MetricsLayer::new());
let svc = layers.layer(handler);
这让 Layer 成为一个可以在 Vec、HashMap、函数返回值里自由携带的一等公民。它不绑定到一个特定的 Service 实例,只在需要的时候 .layer(something) 就地实例化。
3.4 ServiceBuilder:糖
// tower/src/builder/mod.rs:106-108
pub struct ServiceBuilder<L> {
layer: L,
}
就这一个字段。看起来根本没必要单独搞一个 struct——直接用 Layer 本身不就行了?
原因有两个。第一,名字要好看。ServiceBuilder::new().layer(A).layer(B) 读起来像流畅的业务代码;而写成 Stack::new(B, Stack::new(A, Identity)) 就不像了。第二,ServiceBuilder 在纯粹的 Layer 之上还提供了”快捷方法”——.timeout(duration) 等价于 .layer(TimeoutLayer::new(duration)),省掉两个字符和一次命名选择。
3.4.1 .layer(T) 到底做了什么
// tower/src/builder/mod.rs:132-136
impl<L> ServiceBuilder<L> {
pub fn layer<T>(self, layer: T) -> ServiceBuilder<Stack<T, L>> {
ServiceBuilder {
layer: Stack::new(layer, self.layer),
}
}
}
三件事:
- 把当前的
self.layer(类型L)扔进Stack::new的第一个参数位置(即inner); - 新传入的
layer(类型T)扔到第二个参数位置(即outer)——等等,真的是 outer 吗?
让我们对着 Stack::new 的签名再看一眼:
// tower-layer/src/stack.rs:36-38
pub const fn new(inner: Inner, outer: Outer) -> Self {
Stack { inner, outer }
}
Stack::new(inner, outer)——第一个参数是 inner。
再看 ServiceBuilder::layer(T) 里那一句 Stack::new(layer, self.layer)——传入的新 layer 是 inner,旧的 self.layer 是 outer。
等等,这和前面讲的”先加入 builder 的 layer 在外”冲突了吗?
其实没冲突。让我们把一个具体例子完整展开一次:
let sb0 = ServiceBuilder::new(); // ServiceBuilder<Identity>
let sb1 = sb0.layer(A); // ServiceBuilder<Stack<A, Identity>>
// sb1.layer.inner = A, sb1.layer.outer = Identity
let sb2 = sb1.layer(B); // ServiceBuilder<Stack<B, Stack<A, Identity>>>
// sb2.layer.inner = B, sb2.layer.outer = Stack<A, Identity>
let svc = sb2.service(handler);
// = sb2.layer.layer(handler)
// = Stack<B, Stack<A, Identity>>::layer(handler)
// = Stack<A, Identity>::layer(B::layer(handler)) <-- 注意:outer.layer(inner.layer(s))
// = Stack<A, Identity>::layer(WrappedBy_B)
// = Identity::layer(A::layer(WrappedBy_B))
// = Identity::layer(WrappedBy_A_then_B)
// = WrappedBy_A_then_B
看懂了没?虽然字段命名是 Stack { inner, outer },但 Stack::layer 的定义是:
fn layer(&self, s: S) -> Self::Service {
let inner = self.inner.layer(s); // 先应用 inner
self.outer.layer(inner) // 再让 outer 包在 inner 的结果外
}
Stack::new(layer, self.layer) 里,新的 layer(B)被存为 inner——意味着在执行 stack.layer(handler) 时,B 会先应用到 handler 上。而旧的 self.layer(Stack<A, Identity>,代表 A)被存为 outer——A 会后应用。
但是”B 先应用到 handler”意味着什么?意味着 handler 被 B 包了一层,然后这个结果又被 A 包一层——最终形成 A<B<handler>>。从请求流向看:请求先进 A、再进 B、最后到 handler。
所以虽然字段命名直觉上让人困惑,结论是对的:先加入 builder 的 layer(A)在最外层,后加入的 B 在内层。
这个”字段名倒过来”的小细节来自一个历史修改——早期版本 Stack 的字段命名是反的,后来 @hawkw 在 tower#438 PR 里把字段重命名为 inner/outer 以反映”组合结果”的结构,而不是”传参顺序”的结构。注释里也明确写道:
Also, the order of [outer, inner] is important, since it reflects the order that the layers were added to the stack. (见
tower-layer/src/stack.rs:77)
一旦你理解这件事,后面读源码不会再有任何模糊。
3.4.2 快捷方法:便利但不神奇
ServiceBuilder 定义了大量 .timeout()、.buffer()、.concurrency_limit()、.rate_limit() 之类的快捷方法,它们的实现无一例外是”调用 self.layer(SomeLayer::new(...))”:
// tower/src/builder/mod.rs 典型片段
#[cfg(feature = "buffer")]
pub fn buffer<Request>(
self,
bound: usize,
) -> ServiceBuilder<Stack<crate::buffer::BufferLayer<Request>, L>> {
self.layer(crate::buffer::BufferLayer::new(bound))
}
#[cfg(feature = "limit")]
pub fn concurrency_limit(self, max: usize)
-> ServiceBuilder<Stack<crate::limit::ConcurrencyLimitLayer, L>>
{
self.layer(crate::limit::ConcurrencyLimitLayer::new(max))
}
为什么要单独提供这些方法,而不是让用户都写 .layer(BufferLayer::new(bound))?三个小原因:
- 少写一个类型名——
buffer(100)比layer(BufferLayer::new(100))短得多。 - 避免导入——用户不用
use tower::buffer::BufferLayer,读 ServiceBuilder 的 docs 就知道有这个能力。 - feature gate 可以集中——每个快捷方法都带
#[cfg(feature = "xxx")],用户不开 feature 时方法直接消失,错误信息会指向 ServiceBuilder 而不是遥远的 crate。
3.4.3 .service(s):收官
// tower/src/builder/mod.rs:489-494
pub fn service<S>(&self, service: S) -> L::Service
where L: Layer<S>,
{
self.layer.layer(service)
}
所有的 .layer(...) 调用都只是在积累类型——直到 .service(s) 被调用,才真正把这堆 Layer 应用到 Service 上。整个函数只有一行:self.layer.layer(service)。
这一行的意思是:把 self.layer 这个 Layer(可能是一棵很深的 Stack 树)应用到给定的 service 上。编译器会顺着 Stack::layer 的 impl 递归展开,最终单态化成一个具体的、没有虚方法的大 struct。
你会看到 ServiceBuilder 还有一个 into_inner 方法——返回内部的 Layer:
pub fn into_inner(self) -> L { self.layer }
这让你可以”把 ServiceBuilder 当一个 Layer 工厂用”:构造好整条链,但暂时不绑定 Service,把 Layer 存起来或传给别人。Axum 的 Router::layer() 就接受 impl Layer<Route>——你可以直接把 ServiceBuilder 里摘出来的 Layer 传过去。
3.5 让编译器展开一次
让我们做一个思想实验——给一段真实代码手工做 monomorphization。
let svc = ServiceBuilder::new()
.layer(LogLayer)
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.service(handler);
按照上面讨论的类型演化:
| 表达式 | 类型 |
|---|---|
ServiceBuilder::new() | ServiceBuilder<Identity> |
.layer(LogLayer) | ServiceBuilder<Stack<LogLayer, Identity>> |
.layer(TimeoutLayer::new(...)) | ServiceBuilder<Stack<TimeoutLayer, Stack<LogLayer, Identity>>> |
.service(handler) | Timeout<LogService<Handler>> |
最后一步是怎么算出来的?
Stack<TimeoutLayer, Stack<LogLayer, Identity>>::layer(handler)
= outer.layer( inner.layer(handler) )
= TimeoutLayer::layer( Stack<LogLayer, Identity>::layer(handler) )
= TimeoutLayer::layer( LogLayer::layer(Identity::layer(handler)) )
= TimeoutLayer::layer( LogLayer::layer(handler) )
= TimeoutLayer::layer( LogService<Handler> )
= Timeout<LogService<Handler>>
最终 svc 的类型就是 Timeout<LogService<Handler>>。编译器把所有的 Layer 抽象消除得一干二净——没有 Box,没有 vtable,没有任何运行时间接跳转。请求过来时:
svc.call(req)
= Timeout::<LogService<Handler>>::call(&mut svc, req) // 展开为 ResponseFuture { resp: inner.call(req), sleep }
= LogService::<Handler>::call(&mut inner, req) // 打 log,然后 handler.call(req)
= Handler::call(&mut h, req)
最后链式调用全部在编译期确定、全部被 inliner 展平。一个十层中间件的栈和手写十个嵌套函数的性能完全一致。
这种”类型金字塔”的代价是类型名巨长。Rust 编译器 debug 模式下会生成 "core::pin::Pin<alloc::boxed::Box<dyn core::future::future::Future<Output = ...>>>" 这种名字——但错误信息里出现的超长类型是代价,不是 bug。
3.6 Layer 的代数结构
从数学上看,Layer<S>、Stack、Identity 三者构成一个 monoid:
- 集合:所有
Layer<S>实例; - 二元运算:
Stack,满足结合律(Stack::new(Stack::new(A, B), C)和Stack::new(A, Stack::new(B, C))产生相同的 Service); - 单位元:
Identity,满足Stack(Identity, L) = L和Stack(L, Identity) = L。
这不是随意的数学点缀——如果 Layer 没有 monoid 性质,你就没法写出 ServiceBuilder 这样的链式 API。因为链式构造本质上是一个幺半群的左折叠(left fold):
foldl Stack Identity [Layer1, Layer2, Layer3]
= Stack(Stack(Stack(Identity, Layer1), Layer2), Layer3)
monoid 性质保证了加入顺序和实际嵌套层级可以任意拆分,不会影响最终产物。写代码时这件事你感觉不到,但编译器在推导 Stack<Stack<Stack<...>>> 时就是在做这个折叠。
顺带一提,读过卷四《Serde 元编程》的读者会想起第 8 章讨论过的 quote::quote! 宏——它也在拼接 TokenStream,用的同样是 monoid 折叠思路。再往上,读过 Haskell 的 Endo、Scala 的 Monad transformer stacks、甚至 React 的 compose(f, g, h) ——这些模式共享同一个数学骨架。整个程序员行业,都在不同层次重新发明 monoid。
3.7 写自己的 Layer
掌握 Layer trait 之后,写一个自定义中间件是件轻松事。给一个完整的例子——ElapsedLayer,给每个请求打印 handler 的耗时:
use std::task::{Context, Poll};
use std::time::Instant;
use std::pin::Pin;
use std::future::Future;
use tower::{Service, Layer};
use pin_project_lite::pin_project;
#[derive(Clone, Copy, Default)]
pub struct ElapsedLayer;
impl<S> Layer<S> for ElapsedLayer {
type Service = Elapsed<S>;
fn layer(&self, inner: S) -> Self::Service { Elapsed { inner } }
}
#[derive(Clone)]
pub struct Elapsed<S> { inner: S }
impl<S, Req> Service<Req> for Elapsed<S>
where S: Service<Req>,
{
type Response = S::Response;
type Error = S::Error;
type Future = ElapsedFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Req) -> Self::Future {
ElapsedFuture {
inner: self.inner.call(req),
start: Instant::now(),
}
}
}
pin_project! {
pub struct ElapsedFuture<F> {
#[pin] inner: F,
start: Instant,
}
}
impl<F, T, E> Future for ElapsedFuture<F>
where F: Future<Output = Result<T, E>>,
{
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let out = std::task::ready!(this.inner.poll(cx));
println!("handled in {:?}", this.start.elapsed());
Poll::Ready(out)
}
}
这段代码严格按照 Tower 的中间件模式写:
- Layer struct 只存配置(这里没有任何配置,所以是 ZST);
- Service struct 持有被包裹的 inner Service;
- Future struct 持有计时起点 + inner future;
- poll_ready 透传;
- call 构造 future,不做 await;
- Future::poll 做真正的测量逻辑。
用法:
let svc = ServiceBuilder::new()
.layer(ElapsedLayer)
.timeout(Duration::from_secs(10))
.service(handler);
就加一行。运行起来你会看到每个请求打印耗时。
3.8 层数深了怎么办——类型擦除
Layer 的”类型金字塔”在大多数情况下不是问题——编译器帮你处理。但有些场景下你必须把它”拍平”:
- 需要把多种不同链路的 Service 放进一个
Vec<Service>; - 需要把 Service 作为
dyn Trait对象跨模块传递; - 编译时间被类型推导拖慢(罕见,但确实发生过)。
Tower 提供了 BoxService 和 BoxCloneService:
use tower::util::BoxCloneService;
let svc: BoxCloneService<Request, Response, BoxError> =
BoxCloneService::new(
ServiceBuilder::new()
.timeout(Duration::from_secs(10))
.service(handler)
);
BoxCloneService 内部是 Arc<dyn Service<...>>,牺牲一次虚方法调用换取类型擦除。代价是每次 call 都要堆分配一个 future(因为 trait object 的 future 类型必须被 boxed)。
什么时候该擦除:业务路由分发层——各路由的中间件栈可能完全不同,需要统一类型装到路由表里。什么时候不该擦除:hot path(每秒百万请求的代理),类型擦除的成本会累积起来。工程上的判断线大约在”每请求多余 100-300 纳秒的开销是否可接受”。
3.9 和 Vue 3 的 computed effect 对照一眼
我们读过卷五《Vue 3 设计与实现》关于 alien-signals 的章节——Vue 的响应式系统也有一套”一层包一层”的抽象:ref → computed → effect,每一层都接受底下一层产生的响应式对象,返回一个新的响应式对象。
两者的相似点:
- 都是函数式组合——A(B(C(x))) 形式的层层装饰。
- 都依赖编译期(或运行时)类型推导——Vue 的 computed 用 JS 闭包推导依赖,Tower 的 Layer 用 trait 推导。
- 都有**“什么时候开始传播”的显式动作**——Vue 需要
effect.run(),Tower 需要.service(handler)才触发实际应用。
两者的差别:
- Vue 的组合是运行时发生的(每次
computed(() => ...)都在构造闭包);Tower 的组合是编译期发生的(所有 Layer 组合被单态化成具体类型)。 - Vue 的目标是追踪响应式变化——订阅和更新是核心;Tower 的目标是装饰行为——每层都可能修改请求/响应的处理路径。
理解这类”装饰器 + 单态化”模式,让你在 Rust 以外的语言里也能快速看穿类似代码。它们本质都是 monoid 折叠。
3.10 与错误处理的暗礁
Layer 的组合看起来优雅,但隐藏一个工程陷阱:错误类型会越来越复杂。
假设你要做这么一个栈:
Timeout → Retry → Buffer → MyHandler
MyHandler::Error = MyErrorBuffer<MyHandler>::Error = BoxError(因为 Buffer 需要统一 drop 错误)Retry<Buffer<MyHandler>>::Error = BoxErrorTimeout<Retry<Buffer<MyHandler>>>::Error = BoxError
请求过来,最里层的 MyHandler 产生了 MyError::NotFound,它被封装成 Box<dyn Error + Send + Sync>。Timeout 层看到的是 BoxError,完全不知道底下是什么具体错误——它只能透传。
上层拿到 Result<Response, BoxError> 时,想判断”是不是 NotFound”就得做 downcast:
match result {
Err(e) => match e.downcast_ref::<MyError>() {
Some(MyError::NotFound) => ...,
_ => ...,
}
}
这是 Tower 的一个长期痛点——错误类型擦除是组合性的代价。不同中间件用不同的错误类型(Timeout 的 Elapsed、Retry 的 Retries、RateLimit 的 Overloaded),合起来必须有一个统一的容器,BoxError 就是这个容器。
实务上你会看到两种应对:
- 让中间件只处理超集错误。Tonic 的
tonic::Status就是一个覆盖所有可能 gRPC 错误的超集。Tonic 里每个中间件的 Error 都是tonic::Status,不走 BoxError 那一套。 - 顶层统一 downcast。Axum 在最外层做一个统一的
ErrorHandler,把 BoxError 下塑回具体类型、生成恰当的 HTTP 响应。
这两个模式在第 22 章(Axum / Tonic 如何构建在 Hyper + Tower 之上)会再详细讨论。
3.10.1 tower-layer 整个 crate 只有 655 行
读完前面几节你可能以为”Layer 抽象很大”——实测整个 tower-layer 0.3.3 crate 只有 655 行源码——分布如下:
| 文件 | 行 | 角色 |
|---|---|---|
tuple.rs | 330 | 本章 §3.3.2 讨论的 17 个 impl Layer for (L1, ..., Ln)——从 () 一直到 16 元组——就这一个文件占 crate 50% |
layer_fn.rs | 114 | LayerFn / layer_fn(f)——把 Fn(S) -> NewS 闭包包成 Layer |
lib.rs | 112 | Layer trait 本尊(4 行) + re-export + 文档注释 |
stack.rs | 62 | Stack<Inner, Outer> 组合器(本章 §3.3) |
identity.rs | 37 | Identity 零元(§3.3.1) |
三条值得记住的事实——
tuple.rs330 行、占半个 crate——有趣的是这 330 行全是样板(manual macro expansion:16 个类似的impl块)——没有用macro_rules!是因为 Rust 的宏生成出的 type bound 在 rustdoc 里显示不友好、直接手展让 API 文档更清晰。这是一个非常有品味的刻意选择- 整个抽象层 655 行、builder 单一文件 871 行——
tower-layer(抽象)比tower::builder(糖)更薄——中间件定义语义的核心、其实只需要 Layer trait 4 行 + 5 种组合方式(Identity / Stack / tuple × 16 / layer_fn)。这是”协议极简、糖丰富”的经典分层 - 两个 crate 的拆分映射发布节奏——
tower-layer版本在 0.3.x 停住三年不动、说明 Layer 抽象已完全稳定;tower还在 0.5.x 演进、因为具体中间件(Buffer / LoadShed / Rate / Retry)的实现细节随 tokio / hyper 生态在变
3.10.2 ServiceBuilder 全部方法的源码行号账本(tower 0.5.3 实测)
前面第 3.4 节讨论了 .layer() / .service() 两个”核心动作”,但 ServiceBuilder 实际暴露的方法远不止这两个。本节把 tower 0.5.3 src/builder/mod.rs(实测 871 行)里所有公开 pub fn 按行号列出来,方便读者对着源码逐条验证——所有行号都是 grep -n "pub fn " src/builder/mod.rs 的直接产物、不允许漂移。
| 行号 | 方法 | feature gate | 作用 |
|---|---|---|---|
| 132 | layer<T> | 核心 | 本章 §3.4.1 的主角,产出 Stack<T, L> |
| 155 | option_layer<T> | 核心 | 传 Option<L>,None 时退化为 Identity,实现里封装了 util::Either<T, Identity> |
| 167 | layer_fn<F> | 核心 | 把 Fn(S) -> NewS 闭包包成 LayerFn,见 §3.X.B |
| 178 | buffer<Request> | buffer | MPSC 通道 + 后台任务,把非 Clone Service 变成多发者可持有 |
| 196 | concurrency_limit | limit | 信号量,超出就在 poll_ready 挂起 |
| 219 | load_shed | load-shed | 不再挂起、直接返回 Overloaded 错——背压转负反馈 |
| 230 | rate_limit | limit | 令牌桶,按 num / per 配速 |
| 249 | retry<P> | retry | 需要 Policy trait,失败后决定是否重放 |
| 263 | timeout | timeout | 最常用,Duration 到就 Err(Elapsed) |
| 280 | filter<P> | filter | 同步 predicate 拒请求 |
| 297 | filter_async<P> | filter | async predicate |
| 365 | map_request<F, R1, R2> | util | 改写请求 |
| 385 | map_response<F> | util | 改写响应 |
| 402 | map_err<F> | util | 改写错误——§3.10 错误类型擦除场景的缓和剂 |
| 415 | map_future<F> | util | 把 inner 产出的 Future 再包一层 |
| 438 | then<F> | util | 不管 Ok / Err 都跑一遍 |
| 459 | and_then<F> | util | 仅 Ok 时跑 |
| 475 | map_result<F> | util | 接管 Result,可以转换两端 |
| 480 | into_inner | 核心 | 剥掉 builder 外壳,拿回 L 本身,可传给 Router::layer |
| 489 | service<S> | 核心 | §3.4.3 收官 |
| 540 | service_fn<F> | util | service(service_fn(f)) 的快捷写法 |
| 573 | check_clone | 核心 | 编译期断言:Self: Clone,失败返回 trait bound 错误 |
| 610 | check_service_clone<S> | 核心 | 断言产出的 L::Service: Clone |
| 667 | check_service<S, T, U, E> | 核心 | 固定 Service 的四个关联类型,供类型推导失败时”钉住”签名 |
| 706 | boxed<S, R> | util | 类型擦除成 BoxService——§3.8 讨论的场景 |
| 769 | boxed_clone<S, R> | util | 擦除成 BoxCloneService(Arc + dyn) |
| 832 | boxed_clone_sync<S, R> | util | 同上,但底下是 Mutex<Box<dyn Service>>,额外付一把锁换 Sync |
三条读表结论——
- 27 个方法、八成是糖。从
buffer到map_result这 15 个方法实现体几乎是一行:self.layer(SomeLayer::new(...))。真正不平凡的只有layer/option_layer/service/into_inner/check_*/boxed*这几组。这是一个 871 行的糖 crate,不是 871 行的算法。 - feature gate 切片干净。
buffer/limit/retry/timeout/filter/util/load-shed七个 feature 覆盖了全部可选方法——只开util的用户只能拿到 map / boxed / check 这一簇,连timeout都调不出。上游 Axum 默认开util + filter + limit + timeout + buffer,Tonic 客户端只开balance + util。 check_*系列是”拿 trait bound 当断言用”。它们什么都不做——不走self.layer、不记录状态——只是把 where 子句写在签名里、让编译器替你检查。这是 Rust 里一个常见技巧,类似static_assertions::assert_impl_all!的运行时免费版本。
对比跨章账本:
| 项目 | 行数 | 来源 |
|---|---|---|
tower-service 0.3.3 定义 Service trait | 390 行 | ch02 |
tower-layer 0.3.3 全 crate | 655 行 | §3.10.1 |
tower 0.5.3 src/builder/mod.rs | 871 行 | 本节 |
tower 0.5.3 全 crate | 1091 行(仅 src/*.rs 顶层)→ 加上子模块 ≈ 7K+ 行 | ch06 §6.5.12 |
结论:ServiceBuilder 自己就占 tower crate 顶层代码的 80%——本章讨论的东西几乎就是整个 tower 的门面。
3.10.3 Layer trait 与 layer_fn 函数式写法:两条路等价但成本不同
写 Layer 有两条路——实现 Layer trait,或者用 layer_fn(闭包)。两条路产出的 Service 在语义上没有区别,但在 编译期展开、代码组织、类型签名 上差异明显。本节用一张表把差异钉死。
| 维度 | impl Layer for MyLayer | layer_fn(|s| MyService { inner: s, cfg }) |
|---|---|---|
| 源码行数(含 Service impl) | 30–100 行 | 1–3 行 |
| 是否需要新 struct | 需要(Layer struct) | 不需要(闭包即 Layer) |
| 是否需要 Clone 配置 | 手写 #[derive(Clone)] 即可 | 要求闭包 Fn(S) -> _,配置被闭包捕获;如需 Clone 需把闭包标为 Clone |
| type Service 可见度 | 公开类型,rustdoc 友好 | 匿名(闭包类型),用户看到的是 LayerFn<{closure}> |
| Debug 输出 | 可自定义 | 源码里是 LayerFn { f: <crate>::...::{{closure}} }(见 tower-layer/src/layer_fn.rs:88-110) |
| 适合场景 | 公共库中间件、需要命名、需要复杂泛型 | 业务代码本地封装、一次性 wrapper、适配 ServiceBuilder::layer_fn() |
| 源码入口 | tower-layer/src/lib.rs:100-106 | tower-layer/src/layer_fn.rs:67-86 |
| ServiceBuilder 快捷方法 | .layer(MyLayer) | .layer_fn(|s| ...)(见 tower/src/builder/mod.rs:167) |
为什么两条路并存——读一眼 layer_fn.rs:77-86:
impl<F, S, Out> Layer<S> for LayerFn<F>
where F: Fn(S) -> Out,
{
type Service = Out;
fn layer(&self, inner: S) -> Self::Service {
(self.f)(inner)
}
}
LayerFn 的实现本质是”把闭包披上 Layer 的外衣”。它的意义在于:Rust 的闭包类型是匿名的、无法直接 impl Layer,所以 tower 提供了一个通用 wrapper——任何 Fn(S) -> Out 闭包都自动满足 Layer<S>,代价只是多一层 LayerFn 类型包装。
一个真实小陷阱——layer_fn 的闭包不能捕获 &mut 环境,因为 Layer::layer(&self, _) 是 &self。所以这段代码不能编译:
let mut counter = 0;
let lf = layer_fn(|s| { counter += 1; MyService { inner: s } });
// ERROR: closure may outlive the current function, but it borrows `counter`
要让闭包做计数,得把 counter 放进 Arc<AtomicUsize>、让闭包捕获 Arc。这个限制不是 bug、是”Layer 应该是无状态工厂”的设计显式化——Layer 的每一次 layer(s) 调用必须是幂等的、可重复的,否则组合性会被打破。
给读者的决策树——
- 写的是一次性 adapter(比如”把这个 Service 的 request 从
(u32, String)改成String”)→layer_fn一行。 - 写的是业务中间件,有配置字段(超时时长、采样率、feature flag)→
impl Layer,把配置放在 Layer struct 里、让Clone和Debug自动派生。 - 写的是可公开发布的库中间件 → 一定走
impl Layer路线,给用户一个稳定类型名和 rustdoc 页。
3.10.4 与 ch06 / ch19 / ch21 / ch22 / ch23 账本的串联
把本章的数字与后续章节的账本放在一起看一眼、省得读者到处翻——
flowchart LR
A["ch02 tower-service 390 行<br/>Service trait 定义"]
B["本章 tower-layer 655 行<br/>Layer / Stack / Identity / LayerFn / 17 个 tuple impl"]
C["本章 tower/builder/mod.rs 871 行<br/>27 个 pub fn(§3.10.2)"]
D["ch06 §6.5.12 tower 1091 行<br/>顶层模块 + Retry / Buffer / Limit"]
E["ch19 §19.10.5 hyper-util 12693 行<br/>Client + Pool + LegacyClient"]
F["ch21 §21.7.5 hyper/client 6619 行<br/>conn / pool / dispatch"]
G["ch22 §22.8.7 axum 33693 行<br/>Router / extract / response"]
H["ch23 §23.8.6 14400 行旋钮<br/>production-tuning 默认值"]
A --> B --> C --> D --> E --> F --> G --> H
style B fill:#ffe4b5
style C fill:#ffe4b5
三条跨章结论——
- 抽象层极薄,糖层极厚。
tower-service+tower-layer合计 1045 行定义了 Service + Layer 两个 trait 和它们的组合律;在这之上tower 0.5.3用 1091 行顶层 + 大量子模块堆出 Retry / Buffer / Limit 等 12 个中间件;再往上hyper-util12693 行、axum33693 行。每爬一层,代码量翻 10 倍、抽象浓度降一半——这是好的分层设计的特征。 - 本章教的两个 trait 会在 ch22 再见。Axum
Router::layer()的签名是fn layer<L>(self, layer: L) -> Router where L: Layer<Route>——它直接吃本章讨论的 Layer。ch22 §22.8.7 统计axum::routing::Router有 5 个layer*系列方法(layer/route_layer/nest_service_with等),全部最终落到L: tower_layer::Layer这一个 trait bound 上。 - ch23 的 14400 行旋钮里,大约 4.2% 与 Layer 直接相关——ch23 §23.8.6 账本统计的 602 个调参旋钮中,
tower::buffer::bound/tower::limit::rate::num/tower::retry::budget::bucket_size等 25 条是 Layer 层面的配置。读完本章的 §3.10.2 表就能在 ch23 快速定位这些旋钮的实现文件。
读者可以把本章当成”从 trait 到生产配置的入口”——4 行 trait 背后串起了 tower 生态 7K 行、hyper-util 12K 行、axum 33K 行、加上 600+ 生产旋钮。每一层代码都在复用前一层的数学性质(§3.6 讨论的 monoid 折叠),这也是为什么这套抽象能撑起整个 Rust 异步服务端生态。
3.10.5 BoxService vs BoxCloneService vs BoxCloneSyncService:三种擦除的成本账
§3.8 提到了类型擦除,只点到 BoxCloneService。实务里 tower 提供了三款擦除器,区别不在语义而在 Send / Sync / Clone 的组合——选错会导致编译失败或性能浪费。把它们钉在一张表里:
| 擦除器 | 底层 | Send | Sync | Clone | 每次 call 开销 | 典型场景 |
|---|---|---|---|---|---|---|
BoxService<Req, Res, E> | Box<dyn Service + Send> | Y | N | N | 一次 vtable 跳转 + Box<dyn Future> | Axum handler 分发,只有一个持有者 |
BoxCloneService<Req, Res, E> | Arc<Mutex<dyn Service + Send>> 语义等价 | Y | N | Y(Arc::clone) | 同上 + Arc 的 atomic refcount | Router 表里被多任务共享 |
BoxCloneSyncService<Req, Res, E> | Arc<dyn CloneService + Send + Sync> | Y | Y | Y | 同上 + 可能一把锁换 Sync | 跨多个 tokio::spawn 借用同一份 Service 实例,要求 Sync |
三款共用一个入口——BoxService::layer() / BoxCloneService::layer() / BoxCloneSyncService::layer(),见 tower 0.5.3 src/util/boxed_clone.rs:80、boxed_clone_sync.rs 类似位置。它们都返回 LayerFn<fn(S) -> Self>——即 §3.10.3 讨论的”匿名闭包变 Layer”的典型用例——把 Box::new(s) / BoxCloneService::new(s) 这样的构造函数当成 Layer。
一条重要事实——ServiceBuilder::boxed() / .boxed_clone() / .boxed_clone_sync() 这三个方法(src/builder/mod.rs:706 / 769 / 832)不是”构造一个 BoxService”,它们是”在 Layer 链的当前位置插入一层擦除”。这意味着你可以这么写:
ServiceBuilder::new()
.boxed_clone() // 擦除点 1:统一类型、方便放 HashMap
.load_shed()
.concurrency_limit(64)
.timeout(Duration::from_secs(10))
.service_fn(handler)
类型演化:ServiceBuilder<Stack<LayerFn<fn(_)->BoxCloneService<_,_,_>>, Identity>> → 加 LoadShed → 加 ConcurrencyLimit → 加 Timeout → ……最外层仍是 Timeout<ConcurrencyLimit<LoadShed<BoxCloneService<...>>>>。擦除只在那一个位置发生、没有扩散到外层。这个能力在 Axum 里被大量使用——每个 handler 单独擦除一次、Router 内部所有 Service 类型统一为 Route(本质是 BoxCloneService)。
3.10.6 poll_ready 在 Layer 组合下的传播(预告 ch04)
Layer 组合的讨论离不开一个问题:背压信号 poll_ready 怎么穿过多层 Stack?本章不展开(留给 ch04),但给一个纲要,避免读者产生”Layer 只是包裹 call”的错觉。
看一眼 Timeout<S> / LogService<S> 这类简单中间件的 poll_ready——都是直接透传:
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
所以在组合 A<B<C<Handler>>> 的情况下,svc.poll_ready() 的调用链是:
A::poll_ready(&mut A)
└─> B::poll_ready(&mut B)
└─> C::poll_ready(&mut C)
└─> Handler::poll_ready(&mut Handler)
这是好的——背压信号从最里层原样传回请求入口。但有三类例外值得记下:
Buffer<S, Req>的poll_ready不看 inner,只看 MPSC 通道是否还有名额——因为 inner 跑在后台任务里、调用者访问不到它的poll_ready。这是 Buffer 存在的全部理由:解耦前后两端的 readiness。ConcurrencyLimit<S>的poll_ready先看自己的 semaphore、拿到 permit 之后再问 inner——如果 semaphore 满了就直接Pending,inner 根本不会被打扰。Retry<P, S>的poll_ready只看 inner——重试语义发生在call里的 Future,不在 readiness 检查里。
所以 Layer 组合对 poll_ready 不是单纯透传——每一层都有权决定”是否问下一层”。这是整个 Tower 背压体系的核心,下一章 ch04 会用整整一节剖。
3.10.7 编译错误目录:Layer 组合最常见的三个坑
读了前面几节可能觉得 Layer 组合”在类型系统支持下很安全”——确实如此,但代价是编译错误信息非常长。收集三个真实高频错误,列出排查步骤:
坑 1:the trait Layer<_> is not implemented for Stack<...>
ServiceBuilder::new()
.layer(MyFancyLayer) // MyFancyLayer: Layer<SomeSpecificService>,不是 Layer<S>
.service(router);
根因:MyFancyLayer 的 impl Layer 只覆盖了一个具体类型,泛型参数太窄。排查:检查 MyFancyLayer 的 impl Layer<S> 是否对 S 泛型。修:把 impl Layer<SomeSpecificService> for MyFancyLayer 改成 impl<S> Layer<S> for MyFancyLayer。
坑 2:Service<Request> 的关联类型不匹配——错误信息 1500 字符
expected `tower::timeout::Timeout<LogService<MyHandler>>`,
found `tower::timeout::Timeout<LogService<tower::util::MapRequest<MyHandler, ...>>>`
根因:链里某一层改了 Request 类型,下游期望的 request 不对。排查:.service(handler) 之前加一个 .check_service::<_, ExpectedReq, ExpectedRes, ExpectedErr>()——这正是 §3.10.2 表里 check_service 行 667 的存在理由。修:把错误的 .map_request() 或 .filter() 调到正确位置。
坑 3:LayerFn<[closure]>: !Clone
let lf = layer_fn(|s| MyService { inner: s });
let sb = ServiceBuilder::new().layer(lf);
let svc1 = sb.clone().service(handler1); // error: sb doesn't implement Clone
根因:闭包默认不是 Clone。排查:.check_clone()(§3.10.2 行 573)会直接在构造点报错,而不是等到使用 Clone 时。修:给闭包加 move |s| -> _ { ... } 并确保捕获的变量都实现 Clone,或者改写成实名 struct。
把这三个坑当成”Layer 组合的三大 checklist”——每次加一层 Layer 时在脑子里过一遍,能省掉 80% 的编译错误调试时间。
3.11 小结:落到你键盘上
我们本章做了五件事:
- 读完了
Layertrait 的全部源码——4 行。 - 拆清楚
Stack的字段语义,解开”为什么 inner 字段存的是后加入的 layer”的困惑。 - 读完
ServiceBuilder的构造、.layer(T)、.service(S)三个关键方法的源码,完整手工 monomorphize 了一条中间件链。 - 讨论了 Layer 的 monoid 代数结构,和 Serde、Vue 3、函数式 compose 的思想对照。
- 手写了一个
ElapsedLayer完整实现,讨论了类型擦除、错误处理两个工程考量。
落到你键盘上的三件事:
- 打开
cargo expand,用上面ElapsedLayer的例子或者一段真实 axum 代码,展开看编译器生成了什么。你会看到一串impl Service for Timeout<LogService<Handler>>。 - 读
tower/src/builder/mod.rs的全部快捷方法。实测 871 行(tower 0.5.3),绝大部分是简单的self.layer(SomeLayer::new(...))。读完你会对 Tower 的全部内置中间件有个完整印象。 - 自己写一个非平凡的 Layer。比如一个
MetricsLayer——在call开始时记下 request 类型、在 response future 完成后记录耗时、失败时递增错误计数器。这是最好的学习方式。
下一章我们回到最微妙也最值得深挖的一件事:poll_ready 到底在做什么、为什么这套背压协议需要 &mut self、为什么很多人用错了。