怎么读这一卷
主线位置:两条主线的总览。本页把就绪主线和请求主线各画一遍,再用一段程序把两条线都走一遍。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|会用| use1[01 全景]
use1 --> use2[02 核心契约]
use2 --> use3[03 util 组合子]
use3 --> use4[09 常见坑]
q -->|读懂实现| all[00 到 11 按顺序]
锁定版本
全卷只对着一个版本写:
| 项 | 值 |
|---|---|
| crate | tower 0.5.3,tower-service 0.3.3,tower-layer 0.3.3 |
| tag | tower-0.5.3 |
| commit | 4b0a6b0(2026-01-12,tower v0.5.3) |
| 实验用的 tokio | 1.54.1,和 tokio 卷相同 |
源码链接都用 tag,形如 https://github.com/tower-rs/tower/blob/tower-0.5.3/tower/src/...#L123。tower 的 master 已经走过了这个 tag:写作时本地 master 比 tag 多 7 个提交,其中有 ServiceExt::buffered(#849)、ServiceExt::retry(#850),以及一个 P2C 负载均衡的修复(#874)。这些都不在 0.5.3 里,读 master 会看到 0.5.3 没有的代码,行号也对不上。
想在本地对照着读:
git clone https://github.com/tower-rs/tower
cd tower
git checkout tower-0.5.3
有一处要留意:tower-service 和 tower-layer 跟 tower 在同一个仓库,但它们上一次发布到 crates.io 是 2024 年 8 月(tag tower-service-0.3.3、tower-layer-0.3.3,commit 646804d)。之后仓库里又改过这两个 crate:改成 #、impl Service for &mut S 放宽到 S: ?Sized(#650),还补了文档。所以 tower-0.5.3 这个 tag 里的 tower-service/src/lib.rs 有 401 行,你 cargo build 时下载到的 0.3.3 只有 390 行。trait 的方法和 poll_ready 的约定没变,本卷的行号以 tag 为准。会影响编译的只有一处:你实际用到的 0.3.3 里,&mut dyn Service 还不是 Service,见 Service trait 末尾。
两条主线
tower 本身没有运行时,它只规定了一个约定:调用方先问「能接请求吗」(poll_ready),得到肯定答复后才发请求(call)。全卷围绕这个约定的两个方向展开,每页开头都会标出本页在哪条主线上。下面两张图以 Timeout 套在 ConcurrencyLimit 外面为例。
就绪主线(往里传):调用方 ready().await,最外层的 poll_ready 先处理自己的事,再问内层。任何一层返回 Pending,整条链就挂起,压力一路传回调用方。
flowchart TB A["ready().await"] --> B["Timeout:原样转发"] B --> C["ConcurrencyLimit:取许可"] C -->|没有许可| P[Pending,挂起] P -->|许可归还,wake| A C -->|拿到许可| D["内层服务:Ready"] D --> R["Ready(Ok),可以 call"]
请求主线(往外包):call(req) 从外往里调用,每一层把内层返回的 future 包进自己的 ResponseFuture。等待这个 future 时,响应从里往外一层层解开。
flowchart TB A["call(req)"] --> B["Timeout::call"] B --> C["ConcurrencyLimit::call"] C --> D[内层返回 future] D --> E["包一层:带上许可"] E --> F["再包一层:加 Sleep"] F --> G["await:内层和 Sleep 赛跑"] G --> H[响应或超时错误] H --> I[future drop,许可归还]
下面这段程序把两条线都走了一遍,注释标出每一行对应图里的哪个节点:
tower = { version = "=0.5.3", features = ["limit", "timeout", "util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt-multi-thread", "time"] }
use std::time::Duration;
use tower::{BoxError, Service, ServiceBuilder, ServiceExt};
#[tokio::main]
async fn main() -> Result<(), BoxError> {
// 最内层:一个 async 函数,按请求里的毫秒数睡一会儿,再回一个字符串
let mut svc = ServiceBuilder::new()
.timeout(Duration::from_millis(100)) // 外层:Timeout
.concurrency_limit(1) // 内层:ConcurrencyLimit,最多 1 个在途请求
.service_fn(|ms: u64| async move {
tokio::time::sleep(Duration::from_millis(ms)).await;
Ok::<_, BoxError>(format!("slept {ms}ms"))
});
// 就绪主线:ready() → Timeout::poll_ready → ConcurrencyLimit::poll_ready(取到许可)
// → service_fn 的 poll_ready(永远 Ready)
svc.ready().await?;
// 请求主线:call 从外往里调;ConcurrencyLimit 把许可放进自己的 ResponseFuture,
// Timeout 再把它和一个 100ms 的 Sleep 包在一起
let first = svc.call(30);
// 就绪主线:许可还在 first 手里,ConcurrencyLimit::poll_ready 返回 Pending,
// Timeout 原样转发,ready() 挂起。这里最多等 10ms 看看
let waited = tokio::time::timeout(Duration::from_millis(10), svc.ready()).await;
println!("second ready() within 10ms: {}", if waited.is_ok() { "ready" } else { "pending" });
// 请求主线:响应从里往外解开;first 完成并被 drop,许可归还
println!("first: {:?}", first.await);
// 就绪主线:许可回来了,这次 ready() 立刻返回
svc.ready().await?;
// 请求主线:内层要睡 300ms,Timeout 的 Sleep 先到,返回 Elapsed
let err = svc.call(300).await.unwrap_err();
println!("second: {err} (is Elapsed: {})", err.is::<tower::timeout::error::Elapsed>());
Ok(())
}
输出(M2 Max、rustc 1.99.0,跑 3 次结果相同):
second ready() within 10ms: pending
first: Ok("slept 30ms")
second: request timed out (is Elapsed: true)
逐行对应:
- 第一行:
first还没完成,许可在它的 future 里,第二次ready()卡在ConcurrencyLimit::poll_ready。调用方没有发出请求,只是等着。这就是背压。 - 第二行:
first完成,它的 future 被丢掉,许可回到信号量。 - 第三行:
Timeout::call在发出请求的同时创建了一个 100ms 的Sleep,内层要 300ms,Sleep先到,返回装箱的Elapsed。
两条线在 poll_ready 的 Context 那里交汇:ConcurrencyLimit 返回 Pending 时留下了调用方的 waker,first 归还许可时信号量叫醒它。这套唤醒机制是 tokio 的,见前置知识。
阅读路线
- 只想会用:读 01 全景、02 核心契约、03 util 组合子、09 常见坑。读完能看懂中间件栈的类型、知道
poll_ready的规则、会用ServiceBuilder和ServiceExt,也知道最常见的报错从哪里来。 - 想读懂实现:按顺序读。04 到 06 是中间件的实现,每页都对着源码走一遍两条主线;08 把各章的取舍汇总;11 从零写一个最小的 tower。
摘自 tower 的代码片段一律保留原来的格式(4 空格缩进),方便和原文件对照;本站自己写的示例用 2 空格缩进。每个示例上方写出它需要的 feature,只开用到的那几个。
前置知识
这一卷默认你会写 async Rust。tower 的中间件大量借用 tokio 的原语,这些原语在 tokio 卷里讲过,这里只链过去:
| 概念 | tokio 卷 | 本卷用到它的地方 |
|---|---|---|
Future::poll、Waker | poll 与唤醒 | poll_ready 返回 Pending 前要安排好唤醒,否则调用方永远等下去 |
Semaphore | batch_semaphore | ConcurrencyLimit 的许可 |
mpsc | mpsc | Buffer 把请求送进后台 worker |
oneshot | oneshot、watch、broadcast、Notify | Buffer 的 worker 把结果送回调用方 |
Sleep | Sleep 的一生 | Timeout 的计时、RateLimit 的周期、重试的退避 |
tower 官方仓库的 guides 里有两篇入门:「Inventing the Service trait」从零推出 Service trait,「Building a middleware from scratch」从零写出 Timeout。读过再来看源码会轻松很多。
目录
| 部分 | 内容 |
|---|---|
| 00 导读 | 本部分;源码地图 |
| 01 全景 | tower 解决什么问题;一个请求穿过一摞中间件 |
| 02 核心契约 | Service trait 与 poll_ready 的规则、Layer 与 Stack、ServiceBuilder、ServiceExt 与 Oneshot |
| 03 util:组合子 | service_fn 与 map 一族、类型擦除、Either 与 FutureService |
| 04 单服务中间件 | Timeout、ConcurrencyLimit、RateLimit、LoadShed、Buffer、Filter、SpawnReady、Reconnect |
| 05 重试 | Policy 与重试状态机;退避与重试预算 |
| 06 负载均衡 | 本卷重点:Discover、Load、ReadyCache、P2C、Hedge、Steer 与 MakeService |
| 07 测试 | tower-test 的 mock |
| 08 设计取舍 | 把各章的取舍汇总到一起 |
| 09 常见坑 | 用报错原文做标题,讲它在源码里怎么产生 |
| 10 在本站中 | 本站(开源)代码里怎么用 tower |
| 11 动手 | 从零写 Service、Layer、Timeout、ConcurrencyLimit 的思路 |
小结
- 全卷锁定 tag
tower-0.5.3(4b0a6b0);tag 里的tower-service、tower-layer比 crates.io 上的 0.3.3 多了 no_std 等改动,行号以 tag 为准。 - 就绪主线:
ready()从外往里问poll_ready,任何一层Pending整条链就等,这是背压。 - 请求主线:
call从外往里调,future 从里往外包,响应从里往外解开。 - 只想会用读 01、02、03、09;想读懂实现按顺序读。
- tokio 原语不在本卷重讲,链到 tokio 卷。