Rust in Everything
基于 tower 0.5.3

怎么读这一卷

主线位置:两条主线的总览。本页把就绪主线和请求主线各画一遍,再用一段程序把两条线都走一遍。
flowchart TB
  start[从这里开始] --> q{读这一卷为了什么}
  q -->|会用| use1[01 全景]
  use1 --> use2[02 核心契约]
  use2 --> use3[03 util 组合子]
  use3 --> use4[09 常见坑]
  q -->|读懂实现| all[00 到 11 按顺序]

锁定版本

全卷只对着一个版本写:

项值
cratetower 0.5.3,tower-service 0.3.3,tower-layer 0.3.3
tagtower-0.5.3
commit4b0a6b0(2026-01-12,tower v0.5.3)
实验用的 tokio1.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:改成 #![no_std](#810)、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、Wakerpoll 与唤醒poll_ready 返回 Pending 前要安排好唤醒,否则调用方永远等下去
Semaphorebatch_semaphoreConcurrencyLimit 的许可
mpscmpscBuffer 把请求送进后台 worker
oneshotoneshot、watch、broadcast、NotifyBuffer 的 worker 把结果送回调用方
SleepSleep 的一生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 卷。

💬 关联讨论

发起讨论
加载中...