Appearance
第2章 Service trait:async fn(Req) -> Res 的协议无关抽象
2.1 先看一眼"答案"
这一章我们要拆开 Rust 异步服务生态里最重要的十行代码。为了避免把"工程史诗"讲得太虚,先把代码摆出来:
rust
// tower-service/src/lib.rs:322-367
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>)
-> Poll<Result<(), Self::Error>>;
#[must_use = "futures do nothing unless you `.await` or poll them"]
fn call(&mut self, req: Request) -> Self::Future;
}就这些。
没有 default 方法,没有隐藏的 supertrait,没有 Send/Sync 的强制绑定,甚至没有 async 关键字——就一个 trait、三个关联类型、两个方法。但在这不到 400 字节的源码背后,栖身着整整一代 Rust 异步中间件的共识。
Tonic、Axum、Warp、Linkerd、Tower、reqwest、tower-http、twirp-rust、aws-smithy——全部建立在这十行之上。你每写一行 .await,都在间接地通过这几个符号。
那我们的任务就清楚了:把这十行一个字一个字读懂。
2.2 为什么是三个关联类型
先看三个关联类型:
rust
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;为什么不是泛型参数?为什么不是 Box<dyn Future>?为什么不是 async fn?这三个问题每一个都值得展开。
2.2.1 关联类型 vs 泛型参数
对比一下两个写法:
rust
// 现实:Tower 用关联类型
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
}
// 另一种可能性:全部泛型
pub trait Service<Request, Response, Error, Future: ...> {
}差别:关联类型对某个具体的 impl 是唯一的——一个 Timeout<S> 只能有一个 Response 类型;而泛型参数允许一个类型对不同的 <Response, Error> 有多份 impl。
对一个 Service 来说,关联类型是正确的选择:给定 MyService,它只会产出一种 Response。如果它能产出多种,那说明其实是两个服务。用关联类型迫使 API 使用者选择具体的响应类型,而不是让调用方自己推导。
这还带来一个语义收益:bound 可以直接写。你会经常在 Tower 生态里看到这样的 where 子句:
rust
S: Service<Req, Response = Response<Incoming>>读起来就是"S 是一个接受 Req、返回 Response<Incoming> 的服务"。如果 Response 是泛型参数,你就得写 S: Service<Req, SomeResp> 再另外约束 SomeResp = Response<Incoming>——丑得多。
(Request 之所以是泛型参数而不是关联类型,是因为一个服务可以同时实现多种请求类型。比如一个 Redis 客户端可以同时 impl Service<GetCmd> 和 impl Service<SetCmd>,用同一个连接处理两种命令。)
2.2.2 type Future vs Pin<Box<dyn Future>>
type Future 是一个关联类型:在编译期决定每个 Service 具体的 Future 形状。另一个常见的做法是:
rust
// 另一个可能性:用 trait object
pub trait Service<Request> {
fn call(&mut self, req: Request)
-> Pin<Box<dyn Future<Output = Result<Response, Error>> + Send>>;
}后者每一次 call 都会在堆上分配一个 Box、绕一层 vtable 查找。前者在编译期就知道具体类型——单态化之后连一次间接跳转都没有。
代价当然是:类型签名会非常复杂。看一下 tower::Timeout<S> 真实的 Future 类型:
rust
// tower/src/timeout/future.rs 简化版
pin_project! {
pub struct ResponseFuture<T> {
#[pin]
response: T,
#[pin]
sleep: tokio::time::Sleep,
}
}当你把 Timeout<Retry<RateLimit<MyService>>> 堆起来,最终的 Future 类型是层层嵌套的结构体。编译期无忧、运行时零开销;代价是类型签名能绕地球三圈。Tower 为此在 utility 里提供了 BoxService 和 BoxCloneService 当逃生舱——要类型擦除的时候显式装盒。
这是一个经典的 Rust 风格:"默认零成本,显式付费":不想看巨型类型?好,Box::new(service) as Box<dyn Service>——然后你就要付一次虚方法调用的代价。
2.2.3 为什么不是 async fn?
到 2026 年,Rust 已经支持 trait 里的 async fn。一个很自然的问题是:既然 Rust 现在有了 async fn in trait(AFIT),Tower 为什么还不把 Service::call 改成 async fn call?
答案分两层。
第一层,向后兼容。Service 这个 trait 被整个 Rust 生态上万个 crate 依赖,tower-service 这个 crate 从 2016 年发布以来 API 没有发生过 breaking change。改一个方法签名会让所有依赖方同时升级——代价巨大。
第二层,async fn in trait 还没到"完全替代 type Future"的程度。到 2026 年,AFIT 仍有以下工程限制:
- 没法写显式
Send/Syncbound。async fn foo(&self)返回的 future 到底Send不Send?这取决于&self指向的数据。如果你在一个tower-http中间件里想要"这个 Service 的 Future 必须 Send(因为要跨线程调度)",你没法优雅地在 trait 上直接表达(社区现在用trait_variant之类 workaround)。这个细节在第 13 章对 hyper 1.0 的 Service 设计讨论里会再出现。 - Future 类型无法命名。当你写
async fn call(&self, req: Request) -> Response,返回的 Future 类型没有公共名字;如果你在中间件里想改写这个 Future(比如包一层 timeout),就得把它包进Box<dyn Future>,又回到堆分配。 - 大部分下游项目还没有准备好。Axum 的 handler 抽象、tonic 的 server codegen、tower-http 的中间件都建立在
type Future可命名的假设上。
所以现状是:Tower 的稳定版保持 type Future,同时 hyper 1.0 的 hyper::Service(第 13 章)已经开始尝试用 type Future + &self 做一个更现代的组合。这是一场未完成的迁移,Tower 大概率会在 1.0 以后跟进。
2.3 poll_ready 的灵魂
rust
fn poll_ready(&mut self, cx: &mut Context<'_>)
-> Poll<Result<(), Self::Error>>;这是 Tower 全书最容易被初学者跳过、最值得花时间读懂的方法。
2.3.1 它在问什么
表面看,poll_ready 在问:"这个服务准备好接受一个请求了吗?"更准确的语义是:
如果我现在调用
call(req),这个服务能立即接收这个请求,而不需要拒绝或排队吗?
关键字是"立即"。call 总是同步返回一个 Future(它只是构造 future,不 await)——所以"立即"和 Future 会不会很快完成无关。poll_ready 问的是"call 能不能成功发出"。
2.3.2 为什么不在 call 里判断
既然中间件要能判断"满不满",为什么不在 call 里面写?比如:
rust
// 错误的设计
async fn call(&mut self, req: Request) -> Result<Response, Error> {
if self.is_full() {
return Err(Overloaded);
}
...
}问题在于:这没法传递背压。假设你在写一个连接池 client:
- 你有 10 个空闲连接;
- 调用方 spawn 100 个 task 同时调用
call; - 最早的 10 个拿到连接,接下来 90 个被你拒绝;
- 被拒绝的 90 个要么重试(变成 retry storm),要么放弃(用户请求失败)。
理想的行为是:让调用方等待,直到有连接可用再调用 call。这种"等待容量"的能力,必须发生在 call 之前,不是之后。这就是 poll_ready 存在的意义:
rust
// 正确的协议
loop {
// 异步等待服务准备就绪
futures::future::poll_fn(|cx| svc.poll_ready(cx)).await?;
// 获得许可之后再 call
let response = svc.call(req).await?;
break response;
}或者更惯用的写法:svc.ready().await?.call(req).await?(ready 是 ServiceExt 上的便利方法)。
这里的关键在于:poll_ready 返回 Poll::Pending 时会注册 waker,意味着调用方可以把当前 task 挂起,等服务有余量了自己唤醒。这是一个纯粹的异步背压信号,没有任何阻塞、没有任何轮询、没有 retry storm。
这个设计来自 Finagle 的经验——Carl Lerche 把它翻译到 Rust 类型系统里。可以理解成:poll_ready 是"许可发放",call 是"使用许可"。两步协议让容量管理和业务调用在时间上分离。
2.3.3 poll_ready 的几条铁律
Tower 文档里关于 poll_ready 的约束读起来像法律条文,每一条都是血的教训。摘录一下(源码注释在 tower-service/src/lib.rs:332-367):
- "Once
poll_readyreturnsPoll::Ready(Ok(())), a request may be dispatched"——一旦报告就绪,你必须能接一个请求。 - "Until a request is dispatched, repeated calls to
poll_readymust return eitherPoll::Ready(Ok(()))orPoll::Ready(Err(_))"——就绪状态是持久的,不会自动失效(除非出错)。 - "
poll_readymay reserve shared resources"——如果你poll_ready返回Ready,你可能已经预扣了某些资源(比如信号量许可、连接池里的一条连接)。 - "It is critical for implementations to not assume that
callwill always be invoked"——调用方可能在poll_ready之后不调用call就把 Service 扔了。你必须在Drop时释放预扣资源。 - "Implementations are permitted to panic if
callis invoked without obtainingPoll::Ready(Ok(()))"——没poll_ready就call,后果自负。
第 5 条看起来很夸张,但它是整个协议的强制约束。你在 RateLimit::poll_ready 里耗费许可,在 call 里假设许可已发。如果有人不遵守协议直接 call,许可会被透支,程序就错了——panic! 是最坏但合理的反应。
第 3 条的"预扣语义"引申出第 4 条的"要释放资源"。合在一起告诉你:poll_ready 和 call 是一对"开 - 闭"事务,你必须保证这对事务在所有代码路径(包括异常、drop)上都被正确处理。
2.3.4 真实场景:Buffer 中间件
为了把 poll_ready 的协议落到具体代码,我们看一眼 tower::Buffer——一个给任意 Service 加排队能力的中间件。
rust
// tower/src/buffer/service.rs 概念摘录
impl<T, Request> Service<Request> for Buffer<T, Request>
where T: Service<Request> + Send + 'static, ...
{
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<...> {
// 问 mpsc channel 有没有位置
self.tx.poll_ready(cx).map_err(...)
}
fn call(&mut self, request: Request) -> Self::Future {
// 把 (request, response_tx) 塞进 channel
self.tx.send(...).unwrap();
// 等 response_tx
ResponseFuture { ... }
}
}Buffer 自己就是一个 poll_ready → call 协议的典型:
poll_ready只检查**"我能不能往 channel 里塞一个请求"**(即 channel 有没有空位);- 真正的业务逻辑(把请求送到真正的 inner service)发生在 channel 的消费端,由一个独立的 task 处理。
- 如果 channel 满,
poll_ready返回Pending并注册 waker,调用方会被挂起直到有空位。
这是把背压传递给调用方的最小例子。第 6 章会完整读 Buffer 的源码。
2.4 call 的边界条件
rust
#[must_use = "futures do nothing unless you `.await` or poll them"]
fn call(&mut self, req: Request) -> Self::Future;三件事值得注意。
2.4.1 #[must_use] 标注
返回值是一个 Future——如果你不 await 或者不 poll,什么都不会发生。在 Rust 里,futures are lazy。#[must_use] 让编译器在你丢弃返回值时发出警告,避免"我 call 了怎么没反应"这种 bug。
这不是 Tower 独创的,整个 Rust async 生态都在用——tokio::spawn 返回的 JoinHandle、futures::future::ready 返回的 Ready——每一个都有 #[must_use]。
2.4.2 &mut self 的坚持
tower::Service::call 签名里的 &mut self 是一个长期争议点。
&mut self 的意义是:同一时间只能有一个 call 在进行。你不能拿到一个 &mut MyService 然后并发地调用两次 call——borrow checker 会拦。如果你想"一个服务同时处理多个并发请求",你有两条路:
- Clone:
let mut s1 = svc.clone();然后让每个并发任务持有自己的 clone。这是最常见的做法——Axum 的Router就Clone。 - Arc + Mutex:共享一个 Service,用锁串行化访问。罕见。
&mut self 的"隐式串行化"带来了一个副作用:它强制 call 持有者有独占权,这让一些场景很自然。比如你在 call 里修改 service 内部的统计计数,不用 AtomicU64、不用 Mutex,直接写 self.count += 1 就行——因为 &mut self 保证你是唯一的 writer。
但 &mut self 也有代价:和 Rust 的 async fn 配合糟糕。如果你想写
rust
async fn call(&mut self, req: Request) -> Result<Response, Error> {
let lock = self.some_lock.lock().await; // 持锁跨越 await 点
// ...
}这里的 &mut self 会跨越 .await 存活——这让 borrow checker 和异步调度器难以协作。实践中 Tower 的中间件往往在 call 里不持有 &mut self:call 只是同步构造一个 future,真正的 await 都发生在返回的 future 里(future 的状态由 pin_project! 管理,不涉及 self 的借用)。
这就是为什么 Tower 的中间件源码有那么多"构造 future"的模式——每个中间件定义自己的 type Future,call 只做"初始化 future 的字段",所有 await 都在 future 的 poll 里发生。详见下一节的 Timeout 例子。
2.4.3 hyper 选择不用 &mut self
剧透一下第 13 章:hyper 1.0 的 Service trait 故意不用 &mut self。
rust
// hyper/src/service/service.rs:32-57
pub trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn call(&self, req: Request) -> Self::Future; // &self,不是 &mut self
}原因是 HTTP/2 的多路复用:在同一个 Connection<T, S> 上可能同时处理几十个 stream,每个 stream 都要调用同一个 Service 的 call。&mut self 会把这些调用强制串行化——整个 HTTP/2 的多路复用变得毫无意义。&self 让并发调用在类型系统层面被允许,对 HTTP/2 server 至关重要。
这个选择的代价是:你不能在 call 里修改 self。所有状态必须通过 Arc<Mutex> 或 atomic 共享。hyper::Service 和 tower::Service 之间这个"一个字符的区别",就是第 13 章的全部故事。
2.5 从零手写一个 Service
理论讲够了,我们实际手写一个 Service 把上面的协议串起来。目标:一个 HelloService,收到 http::Request<Incoming>,返回 http::Response<Full<Bytes>>。
rust
use std::future::{Ready, ready};
use std::task::{Context, Poll};
use tower::Service;
use http::{Request, Response};
use http_body_util::Full;
use bytes::Bytes;
use hyper::body::Incoming;
#[derive(Clone)]
struct HelloService;
impl Service<Request<Incoming>> for HelloService {
type Response = Response<Full<Bytes>>;
type Error = std::convert::Infallible;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(())) // 没有容量限制,永远就绪
}
fn call(&mut self, _req: Request<Incoming>) -> Self::Future {
ready(Ok(Response::new(Full::new(Bytes::from("Hello")))))
}
}这段代码能跑起来,一个完整的 Tower Service 就完成了。注意几个实现细节:
type Future = Ready<...>:没有实际的异步工作,用futures::future::ready构造一个"立即完成"的 future。它是一个struct Ready<T>的具体类型,不走 heap——单态化后零开销。poll_ready永远返回Ready:我们不做任何容量限制。任何并发调用都能立刻被接。call返回 future,不是 await:严格遵守协议——call只构造,不等待。
把这个 Service 用在 hyper 上需要一步"桥接"(因为 hyper 1.x 的 Service trait 和 tower 的不一样,详见第 13 / 19 章),这里先不展开。
2.6 一个典型的中间件:Timeout 如何实现
现在我们读一段真实的 Tower 中间件源码:tower::Timeout。完整源码在 tower/src/timeout/mod.rs 和 future.rs,版本 0.5.3。
2.6.1 结构定义
rust
// tower/src/timeout/mod.rs 关键片段
pub struct Timeout<T> {
inner: T,
timeout: Duration,
}
impl<T> Timeout<T> {
pub const fn new(inner: T, timeout: Duration) -> Self {
Timeout { inner, timeout }
}
}Timeout<T> 是一个 wrapper——持有被包裹的 Service T 和超时时长。T 是一个泛型参数,意味着 Timeout 不绑定任何具体协议:
rust
// 用在 HTTP 服务端
Timeout<HelloService>
// 用在 gRPC 客户端
Timeout<tonic::client::Grpc<Channel>>
// 用在 Redis 客户端
Timeout<redis::MultiplexedConnection> // 如果它实现了 Service2.6.2 Service impl
rust
impl<S, Request> Service<Request> for Timeout<S>
where
S: Service<Request>,
S::Error: Into<BoxError>,
{
type Response = S::Response;
type Error = BoxError;
type Future = ResponseFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self.inner.poll_ready(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(r) => Poll::Ready(r.map_err(Into::into)),
}
}
fn call(&mut self, request: Request) -> Self::Future {
let response = self.inner.call(request);
let sleep = tokio::time::sleep(self.timeout);
ResponseFuture::new(response, sleep)
}
}关键细节:
Response = S::Response:响应类型不变——Timeout 不修改响应的形状,只可能在时间上拦截。Error = BoxError:错误类型被"擦除"成Box<dyn Error + Send + Sync + 'static>。原因是 Timeout 自身可能产生Elapsed错误,又要把内层S::Error透传。两种不同类型的错误要合并成一种,最通用的做法是装盒。Future = ResponseFuture<S::Future>:这里是 Tower 的另一个典型模式——每个中间件定义自己的 Future 类型。不是Pin<Box<dyn Future>>,不是impl Future,而是一个命名的 struct。这是为了和稳定 trait + 关联类型配合(第 2.2.3 节讨论过)。poll_ready透传:Timeout 本身不限制容量,直接把内层S::poll_ready的结果转发。map_err(Into::into)是为了把S::Error装箱。call里构造 future:构造一个ResponseFuture,把内层响应 future 和 sleep future 绑在一起。注意:tokio::time::sleep这一行不 await,它只是返回一个 Sleep future。
2.6.3 ResponseFuture 的 poll
rust
// tower/src/timeout/future.rs
pin_project! {
pub struct ResponseFuture<T> {
#[pin] response: T,
#[pin] sleep: Sleep,
}
}
impl<F, T, E> Future for ResponseFuture<F>
where F: Future<Output = Result<T, E>>, E: Into<BoxError>,
{
type Output = Result<T, BoxError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
// 1. 先 poll 业务 future
match this.response.poll(cx) {
Poll::Ready(v) => return Poll::Ready(v.map_err(Into::into)),
Poll::Pending => {}
}
// 2. 业务没就绪,poll sleep
match this.sleep.poll(cx) {
Poll::Ready(_) => Poll::Ready(Err(Box::new(Elapsed(())))),
Poll::Pending => Poll::Pending,
}
}
}整个超时逻辑就这么几行:
- 先 poll 内层响应 future——如果业务已完成,立刻返回。
- 业务没完成,poll sleep——如果超时到了,返回
Elapsed错误;否则挂起。
pin_project! 是一个关键细节——它让 ResponseFuture 安全地处理内部 !Unpin 的字段(Sleep 是 !Unpin 的)。原理在卷三《Rust 编译器与运行时揭秘》第 10 章(Pin / Waker / Future)里讲过。
这段代码的美在于:没有一个 async/await,没有任何堆分配,没有任何运行时类型查询。整个 Timeout 就是 trait impl + 状态机,单态化后生成的机器码就是"手写一个 select(response, sleep)"。
2.7 Service 为什么能跨协议
到这里你应该能理解一个核心问题:为什么 Tower 的中间件能跨协议。
答案是:中间件只看 Service<Request>,不关心 Request 是什么。
我们来看 Timeout 的约束:
rust
impl<S, Request> Service<Request> for Timeout<S>
where S: Service<Request>, S::Error: Into<BoxError>,
{ ... }整个 Timeout 的代码里,你找不到任何一处对 Request 或 Response 具体类型的假设。它只需要:
- 内层有一个
Service<Request>能处理这个请求; - 内层的
Error能被转成BoxError。
这是一个完全通用的 impl。因此你既可以这样用:
rust
// HTTP 服务端
let svc: Timeout<AxumRouter> = Timeout::new(router, Duration::from_secs(30));也可以这样:
rust
// gRPC 客户端
let client: Timeout<GrpcClient> = Timeout::new(grpc, Duration::from_secs(5));或这样:
rust
// 纯业务 Service
let handler: Timeout<HelloService> = Timeout::new(hello, Duration::from_millis(100));这是 Service trait 最强的一个特性:它消除了"协议"这个维度。只要你能把一次请求-响应交互装进 call(Request) -> Future<Response> 这个形状,Tower 生态所有的中间件都是免费的。
反过来,这也给中间件作者提了一个要求:不要假设 Request 类型。不要写 req.headers()——那只对 HTTP 成立。不要假设 Clone——要就显式 bound。不要假设 Send——需要时再加。越是少假设,中间件的适用面越广。tower-http crate 存在的意义就是:那些确实需要 HTTP 特性的中间件(比如 CORS、Compression、Authz)被单独放一个 crate,保证 tower 核心 crate 是"协议无关"的纯抽象。
2.8 与 Serde 的 Serializer 对照
读过卷四《Serde 元编程》第 3 章(Serializer trait)的读者,会注意到 Service 和 Serializer 之间一个深层的相似:
Serializer解决 "M 种数据结构 × N 种格式" 的组合爆炸——把矩阵从 M×N 拆成 M+N。Service解决 "M 种协议 × N 种中间件" 的组合爆炸——同样把矩阵从 M×N 拆成 M+N。
两者的共同设计哲学是:用一个中立的 trait 定义"接触面",让两边独立演进。Serializer 是数据结构和格式的接触面,Service 是协议和中间件的接触面。任何一边的改变都不影响另一边。
更深一层:这两个 trait 都提供了"关联类型 + 泛型参数"的混合形式。Serializer 有 type Ok、type Error、type SerializeSeq 等一系列关联类型,配合泛型的 serialize_i32 等方法;Service 有 type Response、type Error、type Future 三个关联类型,配合泛型的 Request。这是 Rust trait 设计里的一个成熟模式:关联类型定义"这个实现会产出什么",泛型参数定义"这个实现接受什么"。
每当你在 Rust 生态里看到一个 trait 使用这种"关联类型 + 泛型"混搭,大概率它也在解决某个 M×N 问题。这是 Rust 生态几大核心抽象(Serde、Tower、Futures)共享的工程美学。
2.9 小结:落到你键盘上
我们本章做了什么:
- 把
Servicetrait 的十行源码逐字读了一遍——三个关联类型、两个方法、每一个的含义和边界条件。 - 深入
poll_ready的设计哲学——它为什么存在、它和call之间的事务语义、它如何传递背压。 - 对比了
&mut self和&self的选择——Tower 为什么坚持&mut self,hyper 1.0 为什么又选了&self(预告第 13 章)。 - 手写了一个最小的
HelloService,读了真实的tower::Timeout源码。 - 理解了"Service 为什么能跨协议"——中间件只看 trait,不看具体 Request。
下一步落到你键盘上的三件事:
- 克隆 tower 源码后读一遍
tower-service/src/lib.rs:整个文件 400 行,大部分是文档,代码部分一页就看完。你要把它从头到尾读一遍,不跳过。 - 跑
cargo expand:找一个用了async fn的Service::call写法,展开看生成的状态机。你会看到impl Future for ...GenFut { ... }就是编译器为你的 async fn 生成的关联类型。 - 手写一个
LoggingService:接受任意Service<Request>,在call里打印 request、await 之后打印 response。看看你能不能在没有Box<dyn Future>的情况下写出来——type Future = impl Future<...>在 Rust 2024 edition 已经稳定,可以用。
下一章,我们讲 Layer trait 和 ServiceBuilder——它们是怎样把"一个洋葱从外到内正向堆起来"的类型魔法。