Hyper 与 Tower:工业级 HTTP 栈
第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::with_status_code(StatusCode::REQUEST_TIMEOUT, Duration::from_secs(30)))
.layer(CorsLayer::permissive())
.service(router)
顺着写、按执行顺序读、每一行只讲一件事。(这三个 Layer 来自 tower-http;tower-http 0.6.7 起 TimeoutLayer::new 已标为 deprecated,改用 with_status_code,见 tower-http-0.6.8/tower-http/src/timeout/service.rs:28-38。)背后不是运行时魔法,核心只是一个 Layer trait 加一个 Stack 组合器,逻辑不过几十行。这一章我们把它读透。
3.2 Layer trait:只有四行
// tower-0.5.3/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 上的装饰器,它把一个 Service 映射成另一个。
读一个例子(把上一章 Timeout 的 layer 版本展开):
// tower-0.5.3/tower/src/timeout/layer.rs:6-24(省去 use 与文档注释)
#[derive(Debug, Clone)]
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 是个 const fn、只配置"超时多久"(tower-0.5.3/tower/src/timeout/layer.rs:13),真正产生 Service 发生在 layer(svc) 被调用的那一刻。
这种"配置与实例化分离"的设计,是整个 Tower 中间件的统一模式。每个中间件都有一对结构体:
| 配置结构体(Layer) | 实例结构体(Service) |
|---|---|
TimeoutLayer(tower-0.5.3/tower/src/timeout/layer.rs:7) |
Timeout<T>(tower-0.5.3/tower/src/timeout/mod.rs:19) |
RateLimitLayer(tower-0.5.3/tower/src/limit/rate/layer.rs:8) |
RateLimit<T>(tower-0.5.3/tower/src/limit/rate/service.rs:13) |
BufferLayer<Request>(tower-0.5.3/tower/src/buffer/layer.rs:12) |
Buffer<Req, F>(F 是内层 Service 的 Future,tower-0.5.3/tower/src/buffer/service.rs:19) |
ConcurrencyLimitLayer(tower-0.5.3/tower/src/limit/concurrency/layer.rs:10) |
ConcurrencyLimit<T>(tower-0.5.3/tower/src/limit/concurrency/service.rs:14) |
RetryLayer<P>(tower-0.5.3/tower/src/retry/layer.rs:6) |
Retry<P, S>(tower-0.5.3/tower/src/retry/mod.rs:42) |
FilterLayer<U>(tower-0.5.3/tower/src/filter/layer.rs:13) |
Filter<T, U>(tower-0.5.3/tower/src/filter/mod.rs:45) |
左边只描述"怎么配置",右边是真正跑业务的 Service。这让配置本身可以独立存在、可以 Clone、可以先存起来稍后再用——而不需要提前绑定到一个具体的 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-0.5.3/tower-layer/src/stack.rs:21-53(省去 #[derive(Clone)] 与 26-39 行的 Stack::new)
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-0.5.3/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-0.5.3/tower/src/builder/mod.rs:116-123(省去文档注释)
impl ServiceBuilder<Identity> {
pub const fn new() -> Self {
ServiceBuilder { layer: Identity::new() }
}
}
刚创建出来的 ServiceBuilder<Identity> 表示"还没加任何中间件的空栈"。当你调用 .layer(A),类型变为 ServiceBuilder<Stack<A, Identity>>;再调用 .layer(B),变成 ServiceBuilder<Stack<B, Stack<A, Identity>>>(为什么新加的在前,见 §3.4.1)。每次调用都把类型"加深"一层。
Stack<A, Identity> 和直接 A 在语义上是等价的(因为 Identity.layer(s) = s),但 compiler 不会把它们视为同一个 Layer 类型。这一般不是问题——Identity 原样返回 S,所以两者产出的 Service 类型完全相同,不会多出一层 wrapper struct;多出来的只是 builder 自身类型名里的 Stack<…, Identity>。
3.3.2 tuple impls:糖里糖外
tower-layer 还给 tuple 写了一系列 Layer impl(tower-0.5.3/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 也能快速组合(元组第一个元素在最外层,与 ServiceBuilder 先加的在外一致):
let layers = (LogLayer::new(), TimeoutLayer::new(d), MetricsLayer::new());
let svc = layers.layer(handler);
这让 Layer 成为一个可以存进变量、结构体字段、函数返回值里自由携带的一等公民。它不绑定到一个特定的 Service 实例,只在需要的时候 .layer(something) 就地实例化。
3.4 ServiceBuilder:糖
// tower-0.5.3/tower/src/builder/mod.rs:106-108(省去 #[derive(Clone)])
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-0.5.3/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),
}
}
}
两件事:
- 新传入的
layer(类型T)放进Stack::new的第一个参数位置; - 当前的
self.layer(类型L)放进第二个参数位置。
这两个位置分别叫什么?对着 Stack::new 的签名再看一眼:
// tower-0.5.3/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 在内层。
这个"字段名从组合结果命名、而不是从传参顺序命名"的选择有一点历史:这个类型最初叫 Chain,2019 年 4 月 Carl Lerche 在 tower#239 里把它改名为 Stack("Reduce metaphors"),inner/outer 两个字段名当时就已经是这样了。一个月后 Sean McArthur 在 tower#280 里重写了它的 Debug 输出,注释里明确写道:
Also, the order of [outer, inner] is important, since it reflects the order that the layers were added to the stack. (见
tower-0.5.3/tower-layer/src/stack.rs:76-77,Debugimpl 按outer, inner的顺序打印,正好是加入 builder 的顺序)
一旦你理解这件事,后面读源码不会再有任何模糊。
3.4.2 快捷方法:便利但不神奇
ServiceBuilder 定义了大量 .timeout()、.buffer()、.concurrency_limit()、.rate_limit() 之类的快捷方法,它们的实现无一例外是"调用 self.layer(SomeLayer::new(...))":
// tower-0.5.3/tower/src/builder/mod.rs:177-201 典型片段(省去文档注释)
#[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-0.5.3/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) |
LogService<Timeout<Handler>> |
最后一步是怎么算出来的?
Stack<TimeoutLayer, Stack<LogLayer, Identity>>::layer(handler)
= outer.layer( inner.layer(handler) ) // inner = TimeoutLayer, outer = Stack<LogLayer, Identity>
= Stack<LogLayer, Identity>::layer( TimeoutLayer::layer(handler) )
= Stack<LogLayer, Identity>::layer( Timeout<Handler> )
= Identity::layer( LogLayer::layer(Timeout<Handler>) )
= Identity::layer( LogService<Timeout<Handler>> )
= LogService<Timeout<Handler>>
最终 svc 的类型就是 LogService<Timeout<Handler>>——先加入的 LogLayer 在最外层(用 tower 0.5.3 实测,std::any::type_name_of_val(&svc) 打印的正是 LogService<Timeout<ServiceFn<…>>>)。编译器把所有的 Layer 抽象消除得一干二净——没有 Box,没有 vtable,没有任何运行时间接跳转。请求过来时:
svc.call(req)
= LogService::<Timeout<Handler>>::call(&mut svc, req) // 打 log,然后 inner.call(req)
= Timeout::<Handler>::call(&mut inner, req) // 展开为 ResponseFuture { response: handler.call(req), sleep }
= Handler::call(&mut h, req)
最后链式调用全部在编译期确定,编译器有机会把它们内联展平。只要中途没有 Box 擦除,一个十层中间件的栈和手写十个嵌套结构体在调用路径上是同一回事。
这种"类型金字塔"的代价是类型名巨长。编译错误信息里会出现 tower::timeout::Timeout<tower::limit::ConcurrencyLimit<…>> 这样层层嵌套的全路径名,长到 rustc 会把完整类型另写进一个 .long-type-*.txt 文件——但错误信息里出现的超长类型是代价,不是 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。
这不只是数学点缀——ServiceBuilder 的链式构造正好可以看成一次幺半群的左折叠(left fold):(严格说,Stack 的输入输出 Service 类型会随每一层变化,这里的"集合"并不是一个封闭的类型,monoid 是帮助理解的类比。)
Identity 起步,依次 .layer(Layer1) .layer(Layer2) .layer(Layer3)
= Stack(Layer3, Stack(Layer2, Stack(Layer1, Identity))) // Stack(inner, outer) 写法
结合律保证了分组方式可以任意拆分(比如先把两个 Layer 叠成一个 Stack,再和第三个叠),不会影响最终产物;但顺序不能换——Stack 不满足交换律,交换两个 Layer 就交换了它们在洋葱里的内外位置。写代码时这件事你感觉不到,但编译器在推导 Stack<Stack<Stack<...>>> 时就是在做这个折叠。
顺带一提,读过《Serde 元编程》专栏的读者会想起第 8 章讨论过的 quote::quote! 宏——它也在拼接 TokenStream,用的同样是 monoid 折叠思路。再往上,读过 Haskell 的 Endo、Scala 的 Monad transformer stacks、甚至 Redux 的 compose(f, g, h) ——这些模式共享同一个数学骨架。
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 内部是 Box<dyn CloneService<...> + Send>(tower-0.5.3/tower/src/util/boxed_clone.rs:58-63,Clone 通过内部的 CloneService::clone_box 复制一份内层 Service),牺牲一次虚方法调用换取类型擦除。代价是每次 call 都要堆分配一个 future(因为 trait object 的 future 类型必须被 boxed)。
什么时候该擦除:业务路由分发层——各路由的中间件栈可能完全不同,需要统一类型装到路由表里。什么时候不该擦除:hot path(每秒百万请求的代理),类型擦除的成本会累积起来。工程上的判断线是"每请求多出一次堆分配加一次虚调用是否可接受"——拿不准就在自己的负载下压测一次。
3.9 和 Vue 3 的 computed effect 对照一眼
我们读过《Vue 3 设计与实现》专栏关于 alien-signals 的章节——Vue 的响应式系统也有一套"一层包一层"的抽象:ref → computed → effect,每一层都接受底下一层产生的响应式对象,返回一个新的响应式对象。
两者的相似点:
- 都是函数式组合——A(B(C(x))) 形式的层层装饰。
- 都依赖某种自动推导——Vue 的 computed 在运行时自动追踪依赖,Tower 的 Layer 在编译期靠 trait 推导类型。
- 都有"什么时候开始传播"的显式动作——Vue 需要
effect.run(),Tower 需要.service(handler)才触发实际应用。
两者的差别:
- Vue 的组合是运行时发生的(每次
computed(() => ...)都在构造闭包);Tower 的组合是编译期发生的(所有 Layer 组合被单态化成具体类型)。 - Vue 的目标是追踪响应式变化——订阅和更新是核心;Tower 的目标是装饰行为——每层都可能修改请求/响应的处理路径。
理解这类"装饰器 + 单态化"模式,让你在 Rust 以外的语言里也能快速看穿类似代码。
3.10 与错误处理的暗礁
Layer 的组合看起来优雅,但隐藏一个工程陷阱:错误类型会越来越复杂。
假设你要做这么一个栈:
Timeout → Retry → Buffer → MyHandler
MyHandler::Error = MyErrorBuffer<MyHandler>::Error = BoxError(Buffer 要把内层错误和自己的Closed/ServiceError统一成一个类型)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 {
Ok(resp) => ...,
Err(e) => match e.downcast_ref::<MyError>() {
Some(MyError::NotFound) => ...,
_ => ...,
}
}
这是 Tower 的一个长期痛点——错误类型擦除是组合性的代价。不同中间件用不同的错误类型(Timeout 的 Elapsed、LoadShed 的 Overloaded、Buffer 的 Closed / ServiceError;Retry 和 RateLimit 则直接透传 S::Error),合起来必须有一个统一的容器,BoxError 就是这个容器。
实务上你会看到两种应对:
- 让业务错误在进入中间件栈之前就折叠成响应。Tonic 的
tonic::Status就是一个覆盖所有 gRPC 错误的超集,handler 返回的Status由生成代码调用的tonic::server::Grpc转成 HTTP 响应(tonic/tonic/src/server/grpc.rs:16-23的status.into_http()),不以BoxError的形式穿过中间件。 - 顶层统一 downcast。Axum 用
HandleErrorLayer(axum/axum/src/error_handling/mod.rs)把中间件冒出来的BoxError交给你写的处理函数,由它 downcast 回具体类型、生成恰当的 HTTP 响应。
第 22 章(Axum / Tonic 如何构建在 Hyper + Tower 之上)讲 Axum Router 为什么要求 Error = Infallible 时,会再回到这个问题。
3.11 tower-layer 整个 crate 只有 699 行
读完前面几节你可能以为"Layer 抽象很大"——实测整个 tower-layer 0.3.3 crate 只有 699 行源码——分布如下:
| 文件 | 行 | 角色 |
|---|---|---|
tuple.rs |
330 | 本章 §3.3.2 讨论的 17 个 impl Layer for (L1, ..., Ln)——从 () 一直到 16 元组——就这一个文件占 crate 近一半 |
layer_fn.rs |
116 | LayerFn / layer_fn(f)——把 Fn(S) -> NewS 闭包包成 Layer |
lib.rs |
117 | Layer trait 本尊(4 行)+ &T 的 blanket impl + re-export + 文档注释 |
stack.rs |
85 | Stack<Inner, Outer> 组合器(本章 §3.3) |
identity.rs |
51 | Identity 单位元(§3.3.1) |
三条值得记住的事实——
tuple.rs330 行、占近半个 crate——有趣的是这 330 行全是样板(手写展开:17 个几乎一样的impl块,从()到 16 元组,tower#694 一次加入,随 tower-layer 0.3.2 发布)- 整个抽象层 699 行、builder 单一文件 871 行——
tower-layer(抽象)比tower::builder(糖)更薄——中间件定义语义的核心、其实只需要 Layer trait 4 行 + 四类组合工具(Identity / Stack / tuple impl / layer_fn)。这是"协议极简、糖丰富"的经典分层 - 两个 crate 的拆分映射发布节奏——
tower-layer自 2019 年 11 月的 0.3.0 起在 0.3.x 停了六年多、只出过三个补丁版(0.3.3 是 2024 年 8 月),说明 Layer 抽象已完全稳定;tower还在 0.5.x 演进、因为具体中间件(Buffer / LoadShed / Rate / Retry)的实现细节随 tokio / hyper 生态在变
3.12 ServiceBuilder 全部方法的源码行号账本(tower 0.5.3 实测)
前面第 3.4 节讨论了 .layer() / .service() 两个"核心动作",但 ServiceBuilder 实际暴露的方法远不止这两个。本节把 tower 0.5.3 tower-0.5.3/tower/src/builder/mod.rs(实测 871 行)里所有公开 pub fn(new 是 pub const fn,已在 §3.3.1 讲过,不在表内)按行号列出来,方便读者对着源码逐条验证——所有行号都是 grep -n "pub fn " tower-0.5.3/tower/src/builder/mod.rs 的直接产物、不允许漂移。
| 行号 | 方法 | feature gate | 作用 |
|---|---|---|---|
| 132 | layer<T> |
核心 | 本章 §3.4.1 的主角,产出 Stack<T, L> |
| 155 | option_layer<T> |
util |
传 Option<T>,None 时退化为 Identity,实现里封装了 util::Either<T, Identity> |
| 167 | layer_fn<F> |
核心 | 把 Fn(S) -> NewS 闭包包成 LayerFn,见 §3.13 |
| 178 | buffer<Request> |
buffer |
MPSC 通道 + 后台任务,把非 Clone Service 变成多发者可持有 |
| 196 | concurrency_limit |
limit |
信号量,超出就在 poll_ready 挂起 |
| 219 | load_shed |
load-shed |
不再挂起、直接返回 Overloaded 错——背压转负反馈 |
| 230 | rate_limit |
limit |
固定窗口计数:每个 per 周期最多放行 num 个请求,周期到了整批补满 |
| 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(Box<dyn CloneService + Send>) |
| 832 | boxed_clone_sync<S, R> |
util |
擦除成 BoxCloneSyncService(Box<dyn CloneService + Send + Sync>),要求内层 S: Sync |
三条读表结论——
- 27 个方法、过半是糖。从
buffer到map_result这 15 个方法实现体几乎是一行:self.layer(SomeLayer::new(...))。真正不平凡的只有layer/option_layer/service/into_inner/check_*/boxed*这几组。这是一个 871 行的糖文件,不是 871 行的算法。 - feature gate 切片干净。
buffer/limit/retry/timeout/filter/util/load-shed七个 feature 覆盖了全部可选方法——只开util的用户只能在核心方法之外拿到 map / boxed /option_layer/service_fn这一簇,连timeout都调不出。上游 axum 0.8 对 tower 只开util(axum/axum/Cargo.toml:121:tower = { version = "0.5.2", default-features = false, features = ["util"] })——超时、限流这些要用户自己再拉一次 tower 依赖并开 feature。 check_*系列是"拿 trait bound 当断言用"。它们什么都不做——不走self.layer、不记录状态——只是把 where 子句写在签名里、让编译器替你检查。这是 Rust 里一个常见技巧,作用类似static_assertions::assert_impl_all!,只是写在 builder 链的中间。
对比跨章账本:
| 项目 | 行数 | 来源 |
|---|---|---|
tower-service 0.3.3 定义 Service trait |
401 行 | ch02 |
tower-layer 0.3.3 全 crate |
699 行 | §3.11 |
tower 0.5.3 tower-0.5.3/tower/src/builder/mod.rs |
871 行 | 本节 |
tower 0.5.3 全 crate(src/ 下全部 .rs) |
11935 行(其中 ch06 §6.5.11 精读的 buffer / load_shed / limit/concurrency 三个目录合计 1091 行) | ch22 §22.8.6 同口径 |
结论:ServiceBuilder 这一个文件就占 tower crate 约 7%——它是整个 tower 的门面,门面之下才是十余个中间件的实现。
3.13 Layer trait 与 layer_fn 函数式写法:两条路等价但成本不同
写 Layer 有两条路——实现 Layer trait,或者用 layer_fn(闭包)。两条路产出的 Service 在语义上没有区别,但在 编译期展开、代码组织、类型签名 上差异明显。本节用一张表把差异钉死。
| 维度 | impl Layer for MyLayer |
layer_fn(|s| MyService { inner: s, cfg }) |
|---|---|---|
| Layer 部分的源码行数(不含 Service impl,两条路都要写) | 约 10 行(struct + impl Layer) |
1 行 |
| 是否需要新 struct | 需要(Layer struct) | 不需要(闭包即 Layer) |
是否需要 Clone 配置 |
手写 #[derive(Clone)] 即可 |
要求闭包 Fn(S) -> _,配置被闭包捕获;捕获的变量都是 Clone 时闭包自动实现 Clone |
type Service 可见度 |
公开类型,rustdoc 友好 |
匿名(闭包类型),用户看到的是 LayerFn<{closure}> |
| Debug 输出 | 可自定义 | 源码里是 LayerFn { f: <crate>::...::{{closure}} }(用 type_name::<F>() 打印,见 tower-0.5.3/tower-layer/src/layer_fn.rs:89-95) |
| 适合场景 | 公共库中间件、需要命名、需要复杂泛型 | 业务代码本地封装、一次性 wrapper、适配 ServiceBuilder::layer_fn() |
| 源码入口 | tower-0.5.3/tower-layer/src/lib.rs:100-106 |
tower-0.5.3/tower-layer/src/layer_fn.rs:68-87 |
| ServiceBuilder 快捷方法 | .layer(MyLayer) |
.layer_fn(|s| ...)(见 tower-0.5.3/tower/src/builder/mod.rs:167) |
为什么两条路并存——读一眼 tower-0.5.3/tower-layer/src/layer_fn.rs:78-87:
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 的闭包不能修改捕获的环境,因为 LayerFn 只在 F: Fn(S) -> Out 时实现 Layer(Layer::layer 拿的是 &self)。layer_fn 本身不设约束,所以错误要等到把它当 Layer 用时才出现:
let mut counter = 0;
let lf = layer_fn(|s| { counter += 1; MyService { inner: s } });
let svc = ServiceBuilder::new().layer(lf).service(handler);
// error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
要让闭包做计数,得把 counter 放进 Arc<AtomicUsize>、让闭包捕获 Arc。这个限制不是 bug、是"Layer 应该是无状态工厂"的设计显式化——layer(s) 只拿 &self,可以被反复调用,每次都应当产出同样配置的 Service。
给读者的决策树——
- 写的是一次性 adapter(比如"把这个 Service 的 request 从
(u32, String)改成String")→layer_fn一行。 - 写的是业务中间件,有配置字段(超时时长、采样率、feature flag)→
impl Layer,把配置放在 Layer struct 里、让Clone和Debug自动派生。 - 写的是可公开发布的库中间件 → 一定走
impl Layer路线,给用户一个稳定类型名和 rustdoc 页。
3.14 与 ch06 / ch19 / ch21 / ch22 / ch23 账本的串联
把本章的数字与后续章节的账本放在一起看一眼、省得读者到处翻——
flowchart LR
A["ch02 tower-service 401 行<br/>Service trait 定义"]
B["本章 tower-layer 699 行<br/>Layer / Stack / Identity / LayerFn / 17 个 tuple impl"]
C["本章 tower-0.5.3/tower/src/builder/mod.rs 871 行<br/>27 个 pub fn(§3.12)"]
D["ch06 §6.5.11 tower 11935 行<br/>其中 buffer / load_shed / limit/concurrency 1091 行"]
E["ch19 §19.10.1 hyper-util 12726 行<br/>Client + Pool + LegacyClient"]
F["ch21 §21.7.5 hyper-util 仓库 src/client/legacy 6643 行<br/>client / pool / connect"]
G["ch22 §22.8.6 axum 35223 行<br/>Router / extract / response"]
H["ch23 §23.8.2 调优旋钮源码账本<br/>可能要翻的 15229 行,近八成是 tower 全 crate"]
A --> B --> C --> D --> E --> F --> G --> H
style B fill:#ffe4b5
style C fill:#ffe4b5
三条跨章结论——
- 抽象层极薄,糖层极厚。
tower-service+tower-layer合计 1100 行(401 + 699)定义了 Service + Layer 两个 trait 和它们的组合律;在这之上tower 0.5.3用 11935 行堆出 Retry / Buffer / Limit 等十余个中间件;再往上hyper-util12726 行、axum全家桶 35223 行。每爬一层,代码量都上一个台阶、抽象浓度却在下降——这是好的分层设计的特征。 - 本章教的两个 trait 会在 ch22 再见。Axum
Router::layer()的签名是fn layer<L>(self, layer: L) -> Self where L: Layer<Route> + Clone + Send + Sync + 'static(另有对L::Service的约束,此处从略)——它直接吃本章讨论的 Layer。Router上带 layer 的方法一共只有两个:axum/axum/src/routing/mod.rs:296的layer和:313的route_layer,两个的 trait bound 一字不差,区别只在中间件是装在路由匹配之前还是之后(route_layer只作用于已匹配到的路由,404 不经过它)。 - ch23 §23.8.2 列出的 15229 行是调优时可能要翻的源码范围(近八成来自 tower 全 crate),其中 ch23 模板直接用到的 tower 中间件只有 263 行——那张账本里
tower-0.5.3/tower/src/timeout/mod.rs(70 行)、tower-0.5.3/tower/src/load_shed/mod.rs(76 行)、tower-0.5.3/tower/src/limit/concurrency/service.rs(117 行)三个文件就是生产配置里.timeout()/.load_shed()/.concurrency_limit()背后的核心 Service 实现(各自的 layer / future / error 文件另计)。读完本章的 §3.12 表就能在 ch23 快速定位这些旋钮的实现文件。
3.15 BoxService vs BoxCloneService vs BoxCloneSyncService:三种擦除的成本账
§3.8 提到了类型擦除,只点到 BoxCloneService。实务里 tower 提供了三款擦除器,区别不在语义而在 Send / Sync / Clone 的组合——选错会导致编译失败或性能浪费。把它们钉在一张表里:
| 擦除器 | 底层 | Send |
Sync |
Clone |
每次 call 开销 |
典型场景 |
|---|---|---|---|---|---|---|
BoxService<Req, Res, E> |
SyncWrapper<Box<dyn Service + Send>>(tower-0.5.3/tower/src/util/boxed/sync.rs:48-51) |
Y | Y(靠 SyncWrapper,因为只能 &mut 访问) |
N | 一次 vtable 跳转 + Box<dyn Future> |
只有一个持有者的分发点 |
BoxCloneService<Req, Res, E> |
Box<dyn CloneService + Send>(tower-0.5.3/tower/src/util/boxed_clone.rs:58-63) |
Y | N | Y(CloneService::clone_box 复制内层) |
同上;clone 是内层 Service 的一次真实 clone,不是引用计数 |
需要被多个任务各持一份 |
BoxCloneSyncService<Req, Res, E> |
Box<dyn CloneService + Send + Sync>(tower-0.5.3/tower/src/util/boxed_clone_sync.rs:17-22) |
Y | Y | Y | 同上,要求内层 S: Sync |
要放进要求 Sync 的共享结构(如 Arc 里跨线程共享),axum 的 Route 就是它 |
三款各有一个同名入口——BoxService::layer() / BoxCloneService::layer() / BoxCloneSyncService::layer(),见 tower 0.5.3 tower-0.5.3/tower/src/util/boxed/sync.rs:77、tower-0.5.3/tower/src/util/boxed_clone.rs:80、tower-0.5.3/tower/src/util/boxed_clone_sync.rs:40。它们都返回 LayerFn<fn(S) -> Self>——即 §3.13 讨论的"匿名闭包变 Layer"的典型用例——把 Box::new(s) / BoxCloneService::new(s) 这样的构造函数当成 Layer。
一条重要事实——ServiceBuilder::boxed() / .boxed_clone() / .boxed_clone_sync() 这三个方法(tower-0.5.3/tower/src/builder/mod.rs:706 / 769 / 832)不是"构造一个 BoxService",而是往 Layer 链里加一层擦除 Layer;产出的最终 Service 就是那个 Box 类型本身,包在它里面的各层全部被擦除。三者的细节不同:
.boxed()/.boxed_clone()调用的是self.layer(BoxService::layer())/self.layer(BoxCloneService::layer()),返回类型里那个fn(L::Service) -> …要求擦除层接收的正是此前已积累的L产出的 Service。实际能用的写法是把它放在链的最前面(此时L = Identity),tower 文档里的示例也是如此;放在已经加过 Layer 的位置之后(哪怕紧接着就.service_fn(...)),用 tower 0.5.3 实测都会因类型对不上而编译失败。.boxed_clone_sync()的实现不同:它先self.into_inner()取出此前积累的整条 Layer,交给BoxCloneSyncServiceLayer::new装箱,再从ServiceBuilder::new()重新开始(tower-0.5.3/tower/src/builder/mod.rs:850-852)。因此它可以放在链的任意位置,产出的 Service 都是一个BoxCloneSyncService。
于是你可以这么写(tower 0.5.3 实测可编译):
ServiceBuilder::new()
.boxed_clone() // 放在最前:整条链擦除成一个 BoxCloneService
.load_shed()
.concurrency_limit(64)
.timeout(Duration::from_secs(10))
.service_fn(handler)
类型演化:ServiceBuilder<Stack<LayerFn<fn(_)->BoxCloneService<_,_,_>>, Identity>> → 加 LoadShed → 加 ConcurrencyLimit → 加 Timeout → 最终 .service_fn(handler) 的结果类型是 BoxCloneService<Req, Res, BoxError>,被擦除的内部结构是 LoadShed<ConcurrencyLimit<Timeout<ServiceFn<_>>>>——先加入的擦除层在最外面,把后加入的各层整个装进了盒子。这个能力在 Axum 里被大量使用——每个 handler 单独擦除一次、Router 内部所有 Service 类型统一为 Route(axum/axum/src/routing/route.rs:31,本质是 BoxCloneSyncService<Request, Response, E>)。
3.16 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)
}
(这是通用形状;Timeout 的实际实现还顺手把内层错误 map_err 成 BoxError(tower-0.5.3/tower/src/timeout/mod.rs 中 impl Service for Timeout 的 poll_ready)。)
所以在组合 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<Req, F>的poll_ready不看 inner,只看后台 worker 是否还活着、MPSC 通道是否还有名额(tower-0.5.3/tower/src/buffer/service.rs:97-108)——因为 inner 跑在后台任务里、调用者访问不到它的poll_ready。这是 Buffer 存在的全部理由:解耦前后两端的 readiness。ConcurrencyLimit<S>的poll_ready先看自己的 semaphore、拿到 permit 之后再问 inner(tower-0.5.3/tower/src/limit/concurrency/service.rs:64-79)——如果 semaphore 满了就直接Pending,inner 根本不会被打扰。- 容易误会的是
Retry<P, S>:它的poll_ready仍然只是透传 inner(tower-0.5.3/tower/src/retry/mod.rs:81-86)——重试语义发生在call返回的 Future 里,不在 readiness 检查里。
所以 Layer 组合对 poll_ready 不一定是单纯透传——每一层都有权决定"是否问下一层"。这是整个 Tower 背压体系的核心,下一章 ch04 会用整整一节剖。
3.17 编译错误目录:Layer 组合最常见的三个坑
读了前面几节可能觉得 Layer 组合"在类型系统支持下很安全"——确实如此,但代价是编译错误信息非常长。收集三个真实高频错误,列出排查步骤:
坑 1:MyFancyLayer: Layer<…> 的 trait bound 不满足(E0277)
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> 的关联类型不匹配——错误信息很长
// 示意
expected `tower::timeout::Timeout<LogService<MyHandler>>`,
found `tower::timeout::Timeout<LogService<tower::util::MapRequest<MyHandler, ...>>>`
根因:链里某一层改了 Request 类型,下游期望的 request 不对。排查:.service(handler) 之前加一个 .check_service::<MyHandler, ExpectedReq, ExpectedRes, ExpectedErr>()(第一个类型参数要写出内层 Service 的具体类型,写 _ 编译器推不出来)——这正是 §3.12 表里 check_service 行 667 的存在理由。修:把错误的 .map_request() 或 .filter() 调到正确位置。
坑 3:LayerFn<{closure}> 不是 Clone
let counter = AtomicUsize::new(0); // AtomicUsize 没有实现 Clone
let lf = layer_fn(move |s| { counter.fetch_add(1, Ordering::Relaxed); MyService { inner: s } });
let sb = ServiceBuilder::new().layer(lf);
let svc1 = sb.clone().service(handler1);
// error[E0277]: the trait bound `Atomic<usize>: Clone` is not satisfied in `{closure@…}`
根因:闭包只有在捕获的变量全都实现 Clone 时才自动实现 Clone;这里 move 进去的 AtomicUsize 不是 Clone,于是闭包、LayerFn、整个 ServiceBuilder 都不是 Clone(不捕获任何东西的 layer_fn(|s| MyService { inner: s }) 则没有这个问题,tower 0.5.3 实测可以 clone)。排查:.check_clone()(§3.12 行 573)会直接在构造点报错,而不是等到使用 Clone 时。修:把捕获的变量换成 Clone 的(比如 Arc<AtomicUsize>),或者改写成实名 struct。
把这三个坑当成"Layer 组合的三大 checklist"——每次加一层 Layer 时在脑子里过一遍,能省掉大量编译错误调试时间。
3.18 小结:落到你键盘上
我们本章做了五件事:
- 读完了
Layertrait 的全部源码——4 行。 - 拆清楚
Stack的字段语义,解开"为什么 inner 字段存的是后加入的 layer"的困惑。 - 读完
ServiceBuilder的构造、.layer(T)、.service(S)三个关键方法的源码,完整手工 monomorphize 了一条中间件链。 - 讨论了 Layer 的 monoid 代数结构,和 Serde、Vue 3、函数式 compose 的思想对照。
- 手写了一个
ElapsedLayer完整实现,讨论了类型擦除、错误处理两个工程考量。
落到你键盘上的三件事:
- 打印一次类型:用
std::any::type_name_of_val(&svc)打印 §3.5 或 §3.7 那条链产出的svc,亲眼看一遍LogService<Timeout<…>>这样的嵌套顺序,和你在 builder 里写下的顺序对照。 - 读
tower-0.5.3/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、为什么很多人用错了。