Hyper 与 Tower:工业级 HTTP 栈

第3章 Layer 与 ServiceBuilder:类型级中间件组合

作者 杨艺韬 · 8,525 字

第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。用函数式的话说,LayerService 范畴上的一个函子(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)
TimeoutLayerTimeout<S>
RateLimitLayerRateLimit<S>
BufferLayerBuffer<S, Req>
ConcurrencyLimitLayerConcurrencyLimit<S>
RetryLayer<P>Retry<P, S>
FilterLayer<F>Filter<S, F>

左边只描述”怎么配置”,右边是真正跑业务的 Service。这让配置本身可以独立存在、可以 Clone、可以存到 Vec 里、可以通过 serde 反序列化——而不需要提前绑定到一个具体的 S

3.2.1 为什么不是直接给 Service 加构造函数?

你可能会问:既然每个 Middleware 都有 MiddlewareLayerMiddleware 成对出现,为什么不直接用 Middleware::new(svc, config) 的构造函数,何必多出一个 Layer trait?

答案在”组合”二字。如果没有 Layer trait,每一个”组合工具”都要手动适配每一个中间件——ServiceBuilder::buffer 知道 Buffer::new 的参数顺序、ServiceBuilder::timeout 知道 Timeout::new……每加一个新中间件,ServiceBuilder 都得改代码。

有了 Layer trait 之后,组合工具只需要面对一个统一接口:fn layer(inner) -> wrappedServiceBuilder 完全不用知道每个中间件的构造函数长什么样——它只负责”把一个 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)
    }
}

拆开看:

  1. Stack<Inner, Outer> 自己还是一个 Layer<S>——它本身还能被继续叠。
  2. layer(s) 的实现是”先让 Inner 包一层,再让 Outer 包一层”。
  3. 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 成为一个可以在 VecHashMap、函数返回值里自由携带的一等公民。它不绑定到一个特定的 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),
        }
    }
}

三件事:

  1. 把当前的 self.layer(类型 L)扔进 Stack::new第一个参数位置(即 inner);
  2. 新传入的 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.layerouter

等等,这和前面讲的”先加入 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.layerStack<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))?三个小原因:

  1. 少写一个类型名——buffer(100)layer(BufferLayer::new(100)) 短得多。
  2. 避免导入——用户不用 use tower::buffer::BufferLayer,读 ServiceBuilder 的 docs 就知道有这个能力。
  3. 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>StackIdentity 三者构成一个 monoid

  • 集合:所有 Layer<S> 实例;
  • 二元运算Stack,满足结合律(Stack::new(Stack::new(A, B), C)Stack::new(A, Stack::new(B, C)) 产生相同的 Service);
  • 单位元Identity,满足 Stack(Identity, L) = LStack(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 提供了 BoxServiceBoxCloneService

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 = MyError
  • Buffer<MyHandler>::Error = BoxError(因为 Buffer 需要统一 drop 错误)
  • Retry<Buffer<MyHandler>>::Error = BoxError
  • Timeout<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 就是这个容器。

实务上你会看到两种应对:

  1. 让中间件只处理超集错误。Tonic 的 tonic::Status 就是一个覆盖所有可能 gRPC 错误的超集。Tonic 里每个中间件的 Error 都是 tonic::Status,不走 BoxError 那一套。
  2. 顶层统一 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.rs330本章 §3.3.2 讨论的 17 个 impl Layer for (L1, ..., Ln)——从 () 一直到 16 元组——就这一个文件占 crate 50%
layer_fn.rs114LayerFn / layer_fn(f)——把 Fn(S) -> NewS 闭包包成 Layer
lib.rs112Layer trait 本尊(4 行) + re-export + 文档注释
stack.rs62Stack<Inner, Outer> 组合器(本章 §3.3)
identity.rs37Identity 零元(§3.3.1)

三条值得记住的事实——

  1. tuple.rs 330 行、占半个 crate——有趣的是这 330 行全是样板(manual macro expansion:16 个类似的 impl 块)——没有用 macro_rules! 是因为 Rust 的宏生成出的 type bound 在 rustdoc 里显示不友好、直接手展让 API 文档更清晰。这是一个非常有品味的刻意选择
  2. 整个抽象层 655 行、builder 单一文件 871 行——tower-layer(抽象)比 tower::builder(糖)更薄——中间件定义语义的核心、其实只需要 Layer trait 4 行 + 5 种组合方式(Identity / Stack / tuple × 16 / layer_fn)。这是”协议极简、糖丰富”的经典分层
  3. 两个 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作用
132layer<T>核心本章 §3.4.1 的主角,产出 Stack<T, L>
155option_layer<T>核心Option<L>None 时退化为 Identity,实现里封装了 util::Either<T, Identity>
167layer_fn<F>核心Fn(S) -> NewS 闭包包成 LayerFn,见 §3.X.B
178buffer<Request>bufferMPSC 通道 + 后台任务,把非 Clone Service 变成多发者可持有
196concurrency_limitlimit信号量,超出就在 poll_ready 挂起
219load_shedload-shed不再挂起、直接返回 Overloaded 错——背压转负反馈
230rate_limitlimit令牌桶,按 num / per 配速
249retry<P>retry需要 Policy trait,失败后决定是否重放
263timeouttimeout最常用,Duration 到就 Err(Elapsed)
280filter<P>filter同步 predicate 拒请求
297filter_async<P>filterasync predicate
365map_request<F, R1, R2>util改写请求
385map_response<F>util改写响应
402map_err<F>util改写错误——§3.10 错误类型擦除场景的缓和剂
415map_future<F>util把 inner 产出的 Future 再包一层
438then<F>util不管 Ok / Err 都跑一遍
459and_then<F>utilOk 时跑
475map_result<F>util接管 Result,可以转换两端
480into_inner核心剥掉 builder 外壳,拿回 L 本身,可传给 Router::layer
489service<S>核心§3.4.3 收官
540service_fn<F>utilservice(service_fn(f)) 的快捷写法
573check_clone核心编译期断言:Self: Clone,失败返回 trait bound 错误
610check_service_clone<S>核心断言产出的 L::Service: Clone
667check_service<S, T, U, E>核心固定 Service 的四个关联类型,供类型推导失败时”钉住”签名
706boxed<S, R>util类型擦除成 BoxService——§3.8 讨论的场景
769boxed_clone<S, R>util擦除成 BoxCloneServiceArc + dyn
832boxed_clone_sync<S, R>util同上,但底下是 Mutex<Box<dyn Service>>,额外付一把锁换 Sync

三条读表结论——

  1. 27 个方法、八成是糖。从 buffermap_result 这 15 个方法实现体几乎是一行:self.layer(SomeLayer::new(...))。真正不平凡的只有 layer / option_layer / service / into_inner / check_* / boxed* 这几组。这是一个 871 行的糖 crate,不是 871 行的算法
  2. 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
  3. check_* 系列是”拿 trait bound 当断言用”。它们什么都不做——不走 self.layer、不记录状态——只是把 where 子句写在签名里、让编译器替你检查。这是 Rust 里一个常见技巧,类似 static_assertions::assert_impl_all! 的运行时免费版本。

对比跨章账本:

项目行数来源
tower-service 0.3.3 定义 Service trait390 行ch02
tower-layer 0.3.3 全 crate655 行§3.10.1
tower 0.5.3 src/builder/mod.rs871 行本节
tower 0.5.3 全 crate1091 行(仅 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 里、让 CloneDebug 自动派生。
  • 写的是可公开发布的库中间件 → 一定走 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

三条跨章结论——

  1. 抽象层极薄,糖层极厚tower-service + tower-layer 合计 1045 行定义了 Service + Layer 两个 trait 和它们的组合律;在这之上 tower 0.5.3 用 1091 行顶层 + 大量子模块堆出 Retry / Buffer / Limit 等 12 个中间件;再往上 hyper-util 12693 行、axum 33693 行。每爬一层,代码量翻 10 倍、抽象浓度降一半——这是好的分层设计的特征。
  2. 本章教的两个 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 上
  3. 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 的组合——选错会导致编译失败或性能浪费。把它们钉在一张表里:

擦除器底层SendSyncClone每次 call 开销典型场景
BoxService<Req, Res, E>Box<dyn Service + Send>YNN一次 vtable 跳转 + Box<dyn Future>Axum handler 分发,只有一个持有者
BoxCloneService<Req, Res, E>Arc<Mutex<dyn Service + Send>> 语义等价YNYArc::clone同上 + Arc 的 atomic refcountRouter 表里被多任务共享
BoxCloneSyncService<Req, Res, E>Arc<dyn CloneService + Send + Sync>YYY同上 + 可能一把锁换 Sync跨多个 tokio::spawn 借用同一份 Service 实例,要求 Sync

三款共用一个入口——BoxService::layer() / BoxCloneService::layer() / BoxCloneSyncService::layer(),见 tower 0.5.3 src/util/boxed_clone.rs:80boxed_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)

这是好的——背压信号从最里层原样传回请求入口。但有三类例外值得记下:

  1. Buffer<S, Req>poll_ready 不看 inner,只看 MPSC 通道是否还有名额——因为 inner 跑在后台任务里、调用者访问不到它的 poll_ready。这是 Buffer 存在的全部理由:解耦前后两端的 readiness
  2. ConcurrencyLimit<S>poll_ready 看自己的 semaphore、拿到 permit 之后问 inner——如果 semaphore 满了就直接 Pending,inner 根本不会被打扰。
  3. 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);

根因:MyFancyLayerimpl Layer 只覆盖了一个具体类型,泛型参数太窄。排查:检查 MyFancyLayerimpl 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 小结:落到你键盘上

我们本章做了五件事:

  1. 读完了 Layer trait 的全部源码——4 行。
  2. 拆清楚 Stack 的字段语义,解开”为什么 inner 字段存的是后加入的 layer”的困惑。
  3. 读完 ServiceBuilder 的构造、.layer(T).service(S) 三个关键方法的源码,完整手工 monomorphize 了一条中间件链。
  4. 讨论了 Layer 的 monoid 代数结构,和 Serde、Vue 3、函数式 compose 的思想对照。
  5. 手写了一个 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、为什么很多人用错了