Hyper 与 Tower:工业级 HTTP 栈
第24章 设计哲学:从 Tower 到 async trait 的演进
24.0 从这里回看
前二十三章一直在源码里钻——数过 hyper-1.9.0/src/proto/h2/ping.rs 的行号、拆过 dispatch::Dispatcher 的状态机、追过 h2::codec::FramedRead 的每一个 byte。这一章把镜头拉远,看这套抽象从哪里来、演到哪里、下一步会动什么。
路从 Service trait 开始。两个方法、几十行定义,撑起了 Rust 一整代的网络生态——tonic、axum、linkerd、reqwest、vector 都围着它转,hyper 1.x 虽然另立了自己的 Service,也要经 hyper-util 的桥和它对接。Service 的成功不在于 API 优雅,而在于它把"一个异步网络调用应该有哪些可抽象的形状"问得非常清楚——清楚到你只要接受它的问题,就不得不接受它的答案。
但它不是完美的。2016 年 tokio-service 把 Finagle 启发的 Service 落到 Rust、2018 年独立成 tower-service、2019 年 Pin 稳定、2023 年底 async fn in trait 稳定——每一次 Rust 本身的演化都在给 Service 施压。poll_ready 要不要还留着?&mut self vs &self?关联类型 Future 要不要换成 -> impl Future?GATs 落地之后 Layer 能不能写得更简洁?这些问题社区吵了六七年、还在吵。
hyper 1.0 在 2023 年 11 月发布,再一次和 tower::Service 分开(hyper 0.12 有过自己的 Service,0.13/0.14 则直接用 tower-service 的)——hyper 自己维护了一个 hyper::service::Service,方法签名用 &self 不用 &mut self,也不要 poll_ready。这是一次刻意的分叉,代表了 Rust 社区对"trait 稳定性 vs 生态依赖"的一次成熟权衡。
本章会:
- 回到 2016 年的 Twitter,看
Service这个抽象是从哪里来的; - 把
poll_ready、Layer、call三个签名放到显微镜下,看它们为什么长成今天这样; - 解释 hyper 1.0 为什么自己做一个
Servicetrait、不直接复用 tower; - 追踪
#[async_trait]→async fn in trait的演进,以及Service为什么还没切换; - 把前二十三章串起来:每一段源码都是"小 trait + 单态化"这条设计线上的一个节点。
24.1 Service trait 的历史:从 Finagle 到 Tower
24.1.1 2016 年,Twitter 的 Finagle
故事的第一个现场不在 Rust 世界,在 Scala 世界。
2010 年前后,Twitter 的内部 RPC 栈叫 Finagle。Finagle 的核心抽象是一个 trait(Scala 里叫 trait,意义和 Rust 的 trait 相同):
trait Service[-Req, +Rep] extends (Req => Future[Rep])
翻译成 Rust 味儿的伪代码:
trait Service<Req> {
fn apply(&self, req: Req) -> Future<Rep>;
}
一个函数,从 Request 到 Future<Response>。就这么一个 API,统一了 Twitter 内部所有 RPC 客户端和服务端。Marius Eriksen(Finagle 的主要作者)在 2013 年的论文《Your Server as a Function》(PLOS 2013)里把这个思路讲得很清楚:服务是函数,中间件是"函数组合"。这篇论文是很多 Rust 网络库作者的共同起点。
Finagle 的第二个抽象叫 Filter(后来在 tower 里改名叫 Layer):
abstract class Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]
extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])
Filter 接一个 Service、返回一个新 Service——类型级的函数组合。超时、重试、限流、日志、认证,在 Finagle 里全都是 Filter,统一抽象,统一组合。这套模型在 Twitter 内部跑了十几年,证明了两件事:
- "网络调用 = 函数"这个抽象足够通用——RPC、HTTP、gRPC、DB 客户端、缓存客户端,都能套进去。
- "中间件 = 函数组合"的好处是模块性彻底地类型级可验证——编译期就能知道中间件能不能套在服务上。
24.1.2 2016–2018 年,从 tokio-service 到 tower-service
Service 在 Rust 里的第一次落地不叫 tower,叫 tokio-service——tower 仓库的 git 历史直接延续自它:2016-08-26 的 Initial commit(Alex Crichton)里就有 pub trait Service(当时的关联类型叫 Req / Resp / Fut),2016-09 的 "Tweaks to the Service trait" 提交加入了 poll_ready,2017-07 的 "Rename to Tower" 提交把项目改名为 tower,2018-08-09 才以 tower-service 0.1.0 的名字发布到 crates.io。tower-service 0.1.0 里的 Service 长这样(git show tower-service-0.1.0:tower-service/src/lib.rs,去掉了文档和 ready 辅助方法):
pub trait Service {
type Request;
type Response;
type Error;
type Future: Future<Item = Self::Response, Error = Self::Error>;
fn poll_ready(&mut self) -> Poll<(), Self::Error>;
fn call(&mut self, req: Self::Request) -> Self::Future;
}
这时候 Rust 的 async/await 还没稳定(要等到 2019 年 11 月),Pin 也还没稳定(2019 年 2 月,Rust 1.33)。Rust 这边做异步的唯一途径是手写 Future——实现 poll() 方法、自己管理状态机、自己处理 waker。在这个语境下,Service 里关联一个 type Future 是唯一自然的选择——你不能写 async fn call(&mut self, ...),因为 async fn in trait 根本不存在。
tokio-service / tower 继承了 Finagle 的设计哲学,但做了一个关键改动:把 poll_ready 拆出来(2016-09 加入时的签名还是 fn poll_ready(&self) -> Async<()>,后来才改成 &mut self 并带上 Error)。Finagle 的 Scala 里没有显式的 poll_ready——Scala 的 Future 是 eager 的,背压通过捕获异常或者 queue-level 信号传递。Rust 的 Future 是 lazy 的,状态机驱动;这个差异让 poll_ready 从"可选"变成"必需"——第 4 章我们用 unbounded queue collapse 那个事故讲过这个故事。
让我们看今天最终稳定下来的定义,来自 tower-0.5.3/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;
}
对照 tower-service 0.1.0 那版,今天的稳定版本做了三处微调:
type Request变成了泛型参数Service<Request>(tower-service 0.2.0,2018-12)——允许同一个类型对不同 Request 实现多次(例如一个 RPC 客户端对GetUser和UpdateUser都实现Service)。poll_ready带上了&mut Context<'_>(tower-service 0.3.0,2019-11)——Pin/Waker API 稳定后Context成了标准参数。Future的 Item/Error 换成了Output = Result<...>(同样在 0.3)——从 futures 0.1 切到 Rust 标准库的Futuretrait。
API 表面的改动不大,核心形状自 2016 年起没变过,而且自 tower-service 0.3.0(2019-11)起连签名都没再动过。这种稳定性是这个 trait 能扛起整个 Rust 网络生态的根本——它没变,所以你 2019 年写的 Service 实现今天还能编译,今天写的 Service 实现可以被 2019 年的中间件包进去。
24.1.3 "小 trait" 的胜利
这里有一个值得停下来想的事实:tower 的核心 crate 叫 tower-service,整个 crate 除了文档和几个 blanket impl 之外,只定义了一个 trait、三个关联类型、两个方法。tower-0.5.3/tower-service/src/lib.rs 整个文件一共才 401 行——大部分还是文档。Layer 另外分在 tower-layer crate 里,也是同样的量级——一个 trait,一个 layer 方法,完。
这是 Rust 社区一个一直被反复验证的设计经验:trait 要小。标准库的 Iterator 只有一个必须方法 next,Future 只有一个必须方法 poll,Read 一个 read,Write 两个(write + flush)——全都是小 trait。tower 继承了这个传统。小 trait 的好处是:
- 易于实现:用户想写一个 Service 只需要填三个关联类型 + 两个方法;门槛低。
- 易于组合:小 trait 才能 blanket impl、才能做
Layer这种"trait-to-trait"的变换——trait 越大,组合的类型约束越爆炸。 - 易于稳定:小 trait 的表面积小,长期演进的成本小;tower-service 的核心形状能保持近十年稳定,就是因为它表面积够小。
我们会在 24.7 节再回到这个主题——"小 trait + 单态化"是这个专栏从头到尾反复出现的设计线索。
24.2 poll_ready 的语义独特性
24.2.1 它不是 Future::poll
读 Rust 异步代码的时候,一眼看过去 poll_ready 像 Future::poll——都返回 Poll<T>、都接 &mut Context、都可能返回 Pending。第一次读的读者(包括我自己 2019 年第一次读)会本能地把它们归成同一个东西。
但它们的语义完全不一样。
Future::poll 回答的是:"你这个计算推进到 Ready 了吗?"——Future 是一次性的,一旦 Ready 就结束。
poll_ready 回答的是:"你有没有空间接一个新的 Request?"——Service 是多次的,poll_ready 之后 call 一次,下次还要再 poll_ready。这是两个完全不同的问题。tower-service 的文档(tower-0.5.3/tower-service/src/lib.rs:342-344)讲得非常直接:
Once `poll_ready` returns `Poll::Ready(Ok(()))`, a request may be dispatched to the
service using `call`. Until a request is dispatched, repeated calls to
`poll_ready` must return either `Poll::Ready(Ok(()))` or `Poll::Ready(Err(_))`.
翻成人话——一旦 poll_ready 返回 Ready,在你 call 之前,再 poll 它也必须返回 Ready。Service 不是 Future——它是一个持续存在的"有能力接请求"的对象。Ready 状态是一个"容量预留",不是"计算推进"。
24.2.2 它不是 Iterator::next / Stream::poll_next
另一个常见的误判是把 poll_ready 和 Stream::poll_next 混起来。Stream 是"异步的 Iterator"——每次 poll 产出一个 item。表面上看,Service 也是一个"一次 call 产出一个 Future"的流式接口——是不是可以看作 Stream?
不行,方向反了。
Stream 是生产者——你 poll 它,它吐 item 给你。Stream 的 poll_next 返回 Poll<Option<Item>>,回答的是"你的下一条数据在哪里"。
Service 是消费者——你 call 它,把 item 送给它。poll_ready 返回 Poll<Result<(), Error>>,回答的是"你做好准备接下一条了吗"。
两者方向相反。Stream 是"拉",Service 是"推"。poll_ready 的存在就是因为"推"需要接方在推之前表达"我准备好了"——这个信号在"拉"的世界里不需要(拉的一方自然知道自己什么时候准备好)。
再说清楚一点:Iterator::next 是同步拉;Stream::poll_next 是异步拉;Service::call 是异步推;poll_ready 是"异步推"场景下接方的容量广播——它是四种 trait 里独有的那一个,因为前三种都不需要。
24.2.3 Backpressure 的类型级表达
poll_ready 的本质是把"系统容量"搬进类型系统。
传统语言里,"我满了"这个信号要么不表达(然后 OOM)、要么通过 exception 表达(try/catch 在延迟敏感路径上是灾难)、要么通过返回码表达(callsite 要记得检查,检查忘记就是 bug)。Rust 把它放进 trait 的类型签名——调用方必须先看到 Poll::Ready(Ok(())),才能调 call。忘了?编译器不管,但 tower 的中间件会把你打到"panic on call without ready"里(第 4 章列过 ConcurrencyLimit、RateLimit、Buffer 等的 panic 点)。生态工具(tower::ServiceExt::ready)把这个两步协议封装成 svc.ready().await?.call(req).await?,让忘记 poll_ready 难得多——但它只管紧跟着的这一次 call,下一次 call 还得再 ready() 一次。
这是 Rust 整个生态一贯做事方式的一个缩影:能放进类型系统的信号,就不要留在运行时。类似的还有 Send/Sync(线程安全放进类型)、Pin(内存固定放进类型)、?Sized(动态大小放进类型)。poll_ready 让"容量"也进到这个列表里。
24.2.4 但它不是没有代价
我们也要诚实——poll_ready 不是白拿的。它的代价有三个:
- 两步协议增加了心智负担:用户很容易写出先
call再被 panic 的代码;或者在 poll_ready 与 call 之间插进别的 await,白白占着预留的容量;预留了却最终没 call,还得保证资源被释放。tower-service 的文档(tower-0.5.3/tower-service/src/lib.rs:346-350)专门花了一段提醒实现者处理后一种情况。 - 资源预留可能浪费:poll_ready 返回 Ready 之后,如果你没调 call 就 drop 了 Service,预留的资源(semaphore permit、队列位置)必须被释放。这要求每个中间件作者都写正确的 Drop。
&mut self让共享变麻烦:poll_ready(&mut self)意味着你不能同时对同一个 Service 实例做两次 poll_ready,多个调用方要共用一个 Service,就得 clone 或者在前面垫一层Buffer。hyper 1.0 最终去掉了poll_ready、把call改成&self,但那是两次独立的决定,理由也不是这一条——我们下面第 24.4 节讲。
多年运营下来,社区对 poll_ready 的态度是"必要但沉重"。必要,是因为 unbounded queue collapse 真的会死人;沉重,是因为它让一个本应两行的抽象变成了两个方法的协议。有没有更轻的替代方案?现在还没看到好的答案。只要你还要在编译期表达容量信号,poll_ready 式的两步协议就几乎是唯一结构。
24.3 Layer 的类型级设计
24.3.1 Layer 本质是 "Service → Service" 的类型级函数
翻开 tower-0.5.3/tower-layer/src/lib.rs:100-106:
pub trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
}
一个 trait,一个关联类型,一个方法。这就是 tower 的全部中间件基础设施。
数学地看,Layer<S> 是一个类型级的函数:
Layer<S> : type_of(S) -> type_of(Self::Service)
给我一个 Service 类型 S,还我一个新 Service 类型 Self::Service。更进一步——因为 S 和 Self::Service 都要求是 Service,Layer 是一个 Service 类型到 Service 类型的函数。这和 Finagle 的 Filter 本质相同,只是用 Rust 的 trait system 表达。
这种"类型级函数"在 Rust 里有个名字叫 higher-kinded 模拟。真正的 higher-kinded types(Haskell 的 Functor f、Scala 的 F[_])Rust 没有——它有 generic type、associated type、GATs,但没有"把类型构造器本身作为参数"这个能力。tower 的做法是把这个缺失用 trait Layer<S> 模拟出来:Self::Service 关联类型取代了"类型构造器"。
24.3.2 为什么不能用 fn make_layer(svc: S) -> ...
初学者会问——既然 Layer 就是一个"Service 变 Service"的函数,为什么不直接写一个函数:
fn make_timeout<S>(svc: S, d: Duration) -> Timeout<S> {
Timeout::new(svc, d)
}
这样不是更简单吗?——在单一 Layer 的场景里,你说得对。如果你只想给某个具体 Service 包一层 Timeout,写个函数就够了。
但看看 tower-0.5.3/tower-layer/src/stack.rs:41-53:
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 堆在一起——先用 Inner::layer(S) 得到 Inner::Service,再用 Outer::layer(Inner::Service) 得到 Outer::Service。这段代码要编译,Outer 必须 "能作用在 Inner::Service 上"——这个约束只能用 trait bound 表达:Outer: Layer<Inner::Service>。用普通函数是写不出来这种链式的、类型依赖前一步输出的组合的——你可以写,但没法让 Stack 本身也作为一个 Layer 再被塞到更大的 Stack 里。
换句话说,Layer trait 的真正用处不是一次变换,而是能被组合成任意深的变换。ServiceBuilder::new().timeout(..).rate_limit(..).load_shed(..).buffer(..) 每一步都是一个 Stack::new(new_layer, prev_stack)(tower-0.5.3/tower/src/builder/mod.rs:132-135,新加的层当 inner、之前攒下的当 outer,所以先加入的层在最外层),编译出来是一个巨长的嵌套类型:
Stack<BufferLayer<Req>, Stack<LoadShedLayer, Stack<RateLimitLayer, Stack<TimeoutLayer, Identity>>>>
这种类型你不会想手写,但编译器帮你推出来了。这才是 Layer trait 的价值——它把"类型命名"的重担转移给编译器,让用户只管逻辑组合。
24.3.3 layer_fn:通往闭包的桥
为了让简单场景不用定义结构体,tower 提供了 layer_fn(tower-0.5.3/tower-layer/src/layer_fn.rs:68-70 与 78-85):
pub fn layer_fn<T>(f: T) -> LayerFn<T> {
LayerFn { f }
}
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)
}
}
一个结构体 + 一个 blanket impl 就把任何闭包 Fn(S) -> Out 变成了 Layer。这是 "小 trait + blanket impl" 组合拳的又一次胜利——Layer trait 自己只有三行,但配合 LayerFn,任何闭包都能变成中间件;配合 Stack,任何数量的中间件都能堆成一个;配合 tower::ServiceBuilder,这堆中间件可以用 builder 风格 fluent 地写出来。
24.3.4 与其他语言中间件的对比
看看其他语言的中间件模型:
| 框架 | 中间件表示 | 组合 | 类型检查 |
|---|---|---|---|
| Express (JS) | (req, res, next) => {} |
运行时 app.use(...) |
无 |
| Koa (JS) | async (ctx, next) => {} |
运行时 app.use(...) |
无 |
| Django (Py) | class Middleware: __call__ |
运行时配置字符串 | 无 |
| Flask (Py) | 装饰器 + WSGI | 装饰器堆叠 | 无 |
| Gin (Go) | func(*gin.Context) |
engine.Use(...) |
无 |
| Finagle (Scala) | Filter[ReqIn, RepOut, ReqOut, RepIn] |
andThen |
编译期 |
| Tower (Rust) | trait Layer<S> |
Stack<Inner, Outer> |
编译期 |
Express / Koa / Django / Gin 用的是 "运行时中间件列表" 模型——中间件是 Box<dyn Fn>、在 Vec 里排队、请求来了按顺序调。坏处是运行时才知道链接正不正确、每个 request 都要走一遍 dispatch。好处是简单、灵活、可以动态改。
Tower 走另一条路:编译期中间件类型——每一个 Layer 在编译期就把下一层的类型确定下来,整条链是一个单态类型。请求到来时不再需要运行时 dispatch,直接一连串 inline 调用。LLVM 优化掉中间层之后,Tower 的中间件链在跑起来时和一个手写 flat 函数几乎等价。
两种模型各有代价——Tower 的代价是编译时间长(单态化爆炸)、错误信息复杂(嵌套类型一长串);Express 的代价是运行时开销和正确性问题(中间件顺序写错、next() 忘记调)。这是一次典型的把"运行时成本"搬到"编译时成本"的设计选择,而 Rust 的整个生态在各处都在做这种选择——serde 的 Serializer(第 24.8 节还会提)、tokio 的 Future 状态机、hyper 的 Service 本身——全都是同一个哲学。
24.4 hyper::Service 和 tower::Service 的分叉
24.4.1 hyper 1.0 做的那个看似激进的决定
2023 年 11 月,hyper 1.0 发布。它定义了自己的 hyper::service::Service trait,不复用 tower::Service——这件事早在 1.0 之前就定了:PR #2920(2022-09,关闭 issue #2853)引入自己的 Service 并去掉 poll_ready,PR #3223(2023-05,落地 issue #3040 的讨论)再把 call 改成 &self(第 13 章细讲)。
对比两者。先看 tower 的(tower-0.5.3/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>>;
fn call(&mut self, req: Request) -> Self::Future;
}
再看 hyper 的(hyper-1.9.0/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;
}
两个关键改动:
- 没有
poll_ready。 call用&self不用&mut self。
为什么去掉 poll_ready,第 13 章讲过:hyper 把背压留给业务层的 Service。至于 &self,hyper 的注释(hyper-1.9.0/src/service/service.rs:48-55)自己给了理由:
/// `call` takes `&self` instead of `mut &self` because:
/// - It prepares the way for async fn,
/// since then the future only borrows `&self`, and thus a Service can concurrently handle
/// multiple outstanding requests at once.
/// - It's clearer that Services can likely be cloned.
/// - To share state across clones, you generally need `Arc<Mutex<_>>`
/// That means you're not really using the `&mut self` and could do with a `&self`.
/// The discussion on this is here: <https://github.com/hyperium/hyper/issues/3040>
把原因翻译整理:
&self为并发铺路:今天的type Future不借用 self,&mut self只在调用call的那一刻独占,返回的多个 Future 照样并发推进——&mut self本身并不让请求串行(第 13 章 §13.3)。问题在往后:一旦 Future 要借用 self(async fn 的情形),&mut self就只允许一个 Future 在飞,&self才允许多个 Future 同时借 self。HTTP/2 一条连接上同时跑很多 stream,这个差别就是能不能并发。&self让 async fn 自然:未来如果 hyper 切换到async fn call(&self, req),Future 借用&self是天经地义;&mut self需要 future 独占整个 Service 直到完成。- 现实中 Service 基本都 Clone:
&mut self的用处是共享可变状态;但 Rust 里共享可变状态通常要Arc<Mutex<_>>,这时&mut self就成了累赘——你已经在用内部可变性了。
这三个理由每一个单独看都未必能说服所有人(poll_ready 的拥护者会强烈反对整个方向——并发是好的,但失去 backpressure 表达能力是大代价),但合起来构成一个实用主义选择:
hyper 作为最底层的 HTTP 实现,需要的是"一个能跑 HTTP request → response 的抽象"。它不在连接层做 backpressure(交给业务层的 Service 去做,协议层另有 HTTP/2 流控)、它需要
&self(为 async fn 下的多路复用铺路)。tower 的 Service 给得太多——poll_ready对 hyper 来说是累赘,&mut self挡着 hyper 往 async fn 走的路。
24.4.2 一次成熟的权衡
为什么 2016 年前后 tokio-service / tower 刚出生时所有人都用同一个 Service,到 2023 年 hyper 要分叉出去?
原因是约束不同。2016 年——Rust 异步生态一穷二白,所有人都要一个 "事实标准";后来的 hyper 0.13/0.14、tonic、linkerd2-proxy 都依赖 tower 的 Service,生态统一是最大价值。到 2023 年——Rust 异步生态已经成熟,tower 已经扛住了六七年的生产使用,但它的设计也被锁死了(很多生产项目依赖 poll_ready 的存在,tower 不能轻易移除它)。hyper 决定"不变但要前进"——自己做一个更适合 HTTP 场景的 Service,同时通过 hyper-util::service::TowerToHyperService 提供桥接。
hyper-util 仓库 src/service/glue.rs:20-46:
pub struct TowerToHyperService<S> {
service: S,
}
impl<S, R> hyper::service::Service<R> for TowerToHyperService<S>
where
S: tower_service::Service<R> + Clone,
{
type Response = S::Response;
type Error = S::Error;
type Future = TowerToHyperServiceFuture<S, R>;
fn call(&self, req: R) -> Self::Future {
TowerToHyperServiceFuture {
future: Oneshot::new(self.service.clone(), req),
}
}
}
二十几行代码,把 tower::Service 包成 hyper::service::Service。核心是 S: ... + Clone——把 tower 的 &mut self 约束通过 clone 转化成"每次 call 都克隆一份 service 实例",克隆出来的实例上调 poll_ready + call(这里的 Oneshot 是 hyper-util 仓库自己的 src/service/oneshot.rs,不是 tower 的,它做的就是这件事,见第 19 章)。这是一层显式、用户可见的桥。你要 tower 生态(retry、rate_limit、buffer……)的所有 Layer,你就付克隆成本;你不要,你就直接写 hyper::Service。
这个分叉是 Rust 社区一次成熟的集体行动:
- 不破坏下游:tower 的 Service 定义没有动(动了的话,几十个生产项目一夜编译不过)。
- 不跟死历史:hyper 这样的"基石库"得以用它觉得更合适的抽象,不被 tower 五年前的决定锁死。
- 通过中间层桥接:
hyper-util承担胶水的角色。用户写业务代码时不需要关心底层用的是 tower 还是 hyper 的 trait——用service_fn、用 axum、用 tonic,框架层已经帮你选好了。
这是我们这个专栏讲的生态结构在这一层的体现——第 21 章讲 hyper-util::client::legacy::Client 时,我们已经看过 hyper 只管单条连接上的协议收发、连接池和 Client 外壳整个放进 hyper-util 的分工;第 22 章讲 axum 时,我们看过 axum 怎么把 tower 中间件和 hyper 服务器粘起来。24 章看到的分叉,就是前面那些分工的最上层体现。
24.4.3 对读者的启示
作为读者,你从这次分叉可以学到三件事:
- 一个 trait 一旦稳定就很难改——即使你觉得"应该
&self更好",如果下游依赖&mut self的语义(例如 retry 的状态要写在&mut self里),你改了就是 breaking change。tower 当年选了&mut self是那时候最合理的选择,但留下了今天的历史债务。 - 当抽象被锁死而需求在演化,新做一个比硬改一个更好——hyper 做了新 trait,没有试图在 tower 里加 feature flag;这比"兼容性补丁"健康得多,代价是生态暂时分叉。
- 分叉不是末日,桥梁就好——
TowerToHyperService只有几十行。好的抽象即使分叉了也能被廉价桥接,因为两边问的是同一个问题("请求→响应")只是问法不同。
24.5 async trait 的演进:#[async_trait] → stable async trait
24.5.1 Rust 异步历史简表
| 时间 | 事件 |
|---|---|
| 2013 | Finagle 的《Your Server as a Function》发表(PLOS 2013) |
| 2015-05 | Rust 1.0 |
| 2016-07 | futures 0.1(Poll<T, E> 老式签名) |
| 2016-08 | tokio-service 的 Service trait 落地(tower 仓库的 Initial commit) |
| 2017-07 | tokio-service 改名 tower |
| 2018-04 | tower-service 在仓库内拆成独立 crate(tower#67) |
| 2018-08 | tower-service 0.1.0 发布到 crates.io |
| 2019-02 | Layer 拆到独立的 tower-layer crate(tower#163) |
| 2019-02 | Pin<&mut T> 稳定(Rust 1.33) |
| 2019-07 | async-trait crate 发布 0.1.0 |
| 2019-11 | async fn / .await 稳定(Rust 1.39) |
| 2019-11 | tower-service 0.3.0:切到 std Future + Context |
| 2022-11 | GATs 稳定(Rust 1.65)——打开了 async fn in trait 的门 |
| 2023-12 | async fn in trait + -> impl Trait in trait 稳定(Rust 1.75) |
| 2024-至今 | 生态逐步迁移,tower 还没切(本专栏写作时是 2026-04) |
这个时间线的关键是——tower 的 Service 诞生于 "Rust 根本没 async fn"、甚至还没有 std Future 的时代。type Future: Future<...> + fn call(&mut self, req) -> Self::Future 的关联类型写法 + 手写状态机,是那时候唯一能写出来的异步 trait。
24.5.2 #[async_trait] 宏是怎么解决这个问题的
2019 年 async fn 稳定之后,dtolnay(著名的 Rust 工程师,serde 的维护者,syn、quote 的作者)放出了 async-trait crate。它的思路很直接——用宏把
#[async_trait]
trait Greet {
async fn hello(&self, name: &str) -> String;
}
展开成
trait Greet {
fn hello<'life0, 'life1, 'async_trait>(
&'life0 self,
name: &'life1 str,
) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait;
}
两个关键点:
- Future 被装在
Pin<Box<dyn Future>>里——动态分发,每次 call 都要堆分配。 - 生命周期被显式标注——
'async_trait是宏合成的,表示 Future 生命周期的下界。
代价是 堆分配 + 动态分发,跑起来比手写的关联 Future 类型慢。但好处巨大——写起来像普通 async fn、所有 Rust async 工具(tokio::select、tokio::spawn、.await)都自然兼容。从 2019 到 2023 年,大量 Rust 异步 trait 都是用这个宏写的。tonic 生成的 gRPC server trait、axum 0.7 及之前的 FromRequest(axum 早期连 Handler 也是 #[async_trait],后来才改成关联 Future 类型,见 axum CHANGELOG #879),都用过 #[async_trait]。
24.5.3 2023 年底:stable async fn in trait
Rust 1.75(2023-12-28)稳定了 async fn in trait 和 -> impl Trait in trait。从此你可以写:
trait Greet {
async fn hello(&self, name: &str) -> String;
}
不需要宏。编译器内部把它脱糖为:
trait Greet {
fn hello(&self, name: &str) -> impl Future<Output = String>;
}
一个 -> impl Future 的返回值,没有 Box、没有动态分发。如果实现者的 Future 是一个具体类型(async fn 编译成的匿名状态机),那就是零额外成本。
这是 Rust 异步十年来最重要的一次演进。它把"异步 trait"从"有代价的便利"变成"零成本的原语"。
24.5.4 那为什么 tower::Service 还没切换?
如果 async fn in trait 这么好,为什么 tower 今天(2026-04)的 Service 还长这样——三个关联类型、两个方法、用户要手写 Future?
答案分三层,都跟"零成本"的隐藏约束有关。
第一层:Future 必须 Send。tower::Service 的使用场景里,绝大多数情况下我们希望 Service::Future: Send,因为它要在 tokio 多线程 runtime 里 spawn。如果 trait 里写 async fn call(...),编译器脱糖出来的 impl Future 是不默认带 Send bound 的。你必须写:
trait Service<Request> {
type Response;
type Error;
fn call(&self, req: Request) -> impl Future<Output = Result<Self::Response, Self::Error>> + Send;
}
但这样写,实现者如果不小心 future 里借了 !Send 的东西(如 Rc、RefCell),编译就不过。而且——这个 Send bound 一旦写死,就不能去掉。有些用户场景是 !Send(例如本地 single-thread executor 跑的 Service)——他们就用不了这个 trait。
tower 目前的 type Future: Future<...> 写法给了用户选择权——用户自己决定 Future 是不是 Send(在 Impl 的 type Future = ... 行里决定)。
第二层:Return Position Impl Trait in Trait (RPITIT) 的生命周期 elision 还在成熟。stable 版的 -> impl Future 在生命周期自动推导上有些边缘情况——例如 &self 的 lifetime 和 Future 的 lifetime 的绑定、dyn Trait 对象的支持、以及 impl Trait 是否允许 "捕获" 某些生命周期参数。2023 稳定之后 Rust 还在不断修补这些边缘情况(2024 的 "precise capturing" RFC 就是为此)。tower 作为 foundational crate,保守策略是等到这些边缘情况完全沉淀再动。
第三层:语义兼容。tower 今天有大量下游代码依赖 Service::Future 作为关联类型存在——它们写 where S::Future: Unpin、S::Future: 'static、S::Future: Send + 'static——这些 bound 在 async fn in trait 里没有直接对应物(因为返回值是 impl Future,这个类型是匿名的,用户没法写 S::Future: Foo)。切换意味着 breaking change。
所以 tower 的策略是:"观望"。等 Rust 异步的一整套生态(dyn Trait 兼容 async、Send bound 的更细粒度控制、"Return-Type Notation" 语法)稳定下来,再考虑一次彻底的 breaking 升级。这是一个对生态负责的、慢的、正确的策略。
24.5.5 axum 和 tonic 的选择
对比 tower 的保守,框架层更积极。
- axum:看它的
Handlertrait(axum/src/handler/mod.rs:148-153):
pub trait Handler<T, S>: Clone + Send + Sync + Sized + 'static {
type Future: Future<Output = Response> + Send + 'static;
fn call(self, req: Request, state: S) -> Self::Future;
}
等等——这里 axum 其实也用了 type Future 不是 async fn。为什么?同样的原因——Send + 'static bound 要显式写;handler 要能被 Clone 存起来;用户写的 async fn handler(...) 由 axum 用宏生成的 impl 装箱成 Pin<Box<dyn Future<Output = Response> + Send>>,存进 type Future(axum-full/axum/src/handler/mod.rs:214、:236)。这是 async fn in trait 还不够灵活时的务实选择。
- tonic:
tonic-build生成的 gRPC server trait 至今仍是#[async_trait] pub trait XxxServer(tonic-build/src/server.rs的generate_trait)——原因和 tower 一样:生成的 trait 要能装进Box<dyn ...>、方法 Future 要Send,stable 的async fn in trait目前还给不了这两样。
这是 Rust 生态里一眼就能看到的演进方式——基础库(tower、hyper)保守,框架层(axum、tonic)渐进,应用层用最新语法。三层步调不齐,中间层做适配器——这是一个成熟生态应该长的样子。
24.6 未来:下一版 Service 与 GATs
24.6.1 社区讨论中的 Service 改版方向
tower 的 issue tracker 和 Rust 社区里反复出现的两个讨论方向(截止 2026 年 4 月):
- 把
Service::call改成async fn in trait:把fn call(&mut self, req) -> Self::Future换成async fn call(&mut self, req),Future 隐式。 - 去掉
poll_ready或把它变成可选:背压要不要改成基于 Semaphore 的显式协议,从 trait 里拿出去。
两个提案都没落地。第一个因为前面讲的 Send / lifetime / 生态兼容问题;第二个因为社区对"是否丢掉类型级 backpressure"分歧严重——丢了简单但危险,留着复杂但正确。
设想一下下一版 Service 如果真的落地,长什么样:
pub trait Service<Request> {
type Response;
type Error;
// async fn call 取代 type Future + fn call(&mut self, req) -> Self::Future
async fn call(&self, req: Request) -> Result<Self::Response, Self::Error>;
// poll_ready 变成 provided method,默认 Ok
async fn ready(&self) -> Result<(), Self::Error> {
Ok(())
}
}
改动:
call是async fn;Future 匿名;不需要 Pin<Box>;零额外堆分配。&self取代&mut self;并发友好;对齐 hyper 1.0。ready()变 provided method;大多数 Service 不需要实现;backpressure 成为 opt-in 而非必须。
这会带来一个漂亮的未来——一个 trait,两个 async fn,完事。但代价是大 breaking change,tower 生态所有中间件要重写。社区是否愿意付这个代价,还没共识。
24.6.2 GATs 能做什么
Rust 1.65 稳定的 GATs(Generic Associated Types,泛型关联类型)让关联类型可以带生命周期或类型参数:
trait Service<Request> {
type Response;
type Error;
type Future<'a>: Future<Output = Result<Self::Response, Self::Error>> + 'a
where
Self: 'a;
fn call<'a>(&'a self, req: Request) -> Self::Future<'a>;
}
Future 关联类型可以"捕获" self 的生命周期。这让"Future 借用 self"变得可表达——而这正是 &self + async fn 的核心需要。
实际上,async fn in trait 的脱糖正是用 GATs 实现的——编译器合成一个匿名的 type Future<'a>。GATs 是 async fn in trait 的地基。
未来 Service 可能的形态里,GATs 让用户可以显式写出 Future 捕获的生命周期,从而让 bound(Send, 'static)能被精确控制。这是比现在"语法糖自动脱糖"更灵活的形式。
24.6.3 我的预测
基于目前(2026-04)的观察,我对未来 2-3 年 tower / hyper 的走向的个人预测:
- hyper:继续保持自己的
Service,与 tower 分开。可能在 hyper 2.0 把call换成async fn call(&self, req)(省掉用户写 Future)。 - tower:会在 2027 年前后发布一次
Service的 breaking 改版,基本就是上面那个 "async fn call + optional ready" 的版本。过渡期会有兼容层。 - axum / tonic:跟着 tower 的这次改版升级。用户代码(handler、service impl)基本不用改(因为用户一直是写
async fn),底层脱糖方式换。 - poll_ready:会降为 opt-in,但不会消失。严肃的生产 gateway(linkerd 这一类)还会用它。
这不是预言、这是观察社区讨论得出的趋势。真正发生的时候可能差别不小——Rust 社区最不可预测的地方就是"有多久一次会有一个新颖的 insight 改变讨论方向"——例如 2022 年的 GATs 稳定就是这种 insight。
24.7 整个专栏的串联:小 trait + 单态化
现在让我们回头看这个专栏走过的路,把每一章都嵌回整个设计哲学里。
24.7.1 第 2-4 章:Service / Layer / poll_ready
主题——"RPC 是函数、中间件是函数变换"。我们从 Finagle 的思想出发,看 tower 怎么用一个 trait + 一个关联 Future 表达 RPC;用另一个 trait + 一个 type Service 表达中间件;用 poll_ready 表达背压。这三章是全专栏的公理层。
24.7.2 第 5-8 章:具体中间件
主题——"小 Layer 组合出复杂行为"。Timeout、Retry、RateLimit、Buffer、LoadShed、Balance、Discover、Filter、Steer——每个都是几百行代码,大多是一个 Layer 加一个 Service 实现(Discover 则是一个 trait)。单独看都不复杂;组合起来能做出 linkerd 这样的 L7 代理。这几章是全专栏的组合层——证明"小 trait + 组合"在真实场景里能跑。
24.7.3 第 9-10 章:http crate + http-body
主题——"协议领域的 trait 抽象"。http crate 只做 Request/Response 的数据结构,不管 IO;http-body 只做 "流式 body" 的 trait,不管协议。这两个 crate 是 hyper / tonic / axum / reqwest 共用的地基——因为它们只定义类型,不绑定实现。这两章讲明白了"抽象要抽对层次"——http 和 http-body 被抽在协议语义那一层,所以 HTTP/1、HTTP/2、HTTP/3 都能用它们。
24.7.4 第 11-18 章:HTTP/1 + HTTP/2 的具体实现
主题——"在 trait 下面放真实工程"。这 8 章是全专栏最重的部分——字节解析、状态机、流控、HPACK、PING / GOAWAY、Upgrade。每一章精读 hyper 或 h2 的一个模块。这是全专栏的实作层。前面的 trait 是骨,这里的源码是肉。trait 写得再漂亮,没有这些字节级实现,就只是 "PowerPoint 工程"。
24.7.5 第 19-21 章:hyper-util + pool + dispatch
主题——"胶水也是产品"。hyper 做了"精",但生产用户要"全"——连接池、legacy Client、TowerToHyperService 桥、服务端 auto 连接构建器与优雅停机。这些胶水代码在 hyper-util 里,它不是 hyper 的"下位品"——它是 hyper 的"互补品"。这三章展示了 Rust 生态一种特殊的分层:核心库做最小正确、util 库做生产够用、框架库做用户友好。
24.7.6 第 22-23 章:框架层 + 生产调优
主题——"用户视角 + 运维视角"。axum 把 Handler 适配成 tower::Service、再经 TowerToHyperService 交给 hyper,tonic 把 Service 适配成 gRPC stub。两者都建立在 tower + hyper 之上,但给用户展示的都是"写一个普通的 async fn"。生产调优那一章讲参数——keep_alive_interval、initial_stream_window_size / initial_connection_window_size(hyper 的 Builder 上没有一个笼统的 initial_window_size,stream 级和连接级是分开的两个旋钮)、max_send_buf_size——全部挂在 http2::Builder 上(server 侧 hyper-1.9.0/src/server/conn/http2.rs:226、157、170、249;client 侧 hyper-1.9.0/src/client/conn/http2.rs:422、306、319、471),每一个参数都是前面几章某个机制的"旋钮"。抽象再干净,旋钮也得露出来。
24.7.7 一条贯穿线
从第 2 章到第 23 章,每一章每一段源码,都有一个共同主题:小 trait + 单态化。
Service(tower-0.5.3/tower-service/src/lib.rs)小、Layer(tower-0.5.3/tower-layer/src/lib.rs:100-106)小——组合出一切。Body(http-body-1.0.1/http-body/src/lib.rs:38)小、Stream小——抽象流式数据。AsyncRead/AsyncWrite小——抽象 IO 字节。Connect(hyper-util 仓库src/client/legacy/connect/mod.rs:324)/MakeService(tower-0.5.3/tower/src/make/make_service.rs:21)小——抽象连接建立。
每一个 trait 都只问一个问题、只带最少的关联类型、只定义最必要的方法。每一个 trait 都被 blanket impl 扩展出大量派生能力(tower 的 ServiceExt,tower-0.5.3/tower/src/util/mod.rs:77;futures 的 StreamExt、FutureExt)。每一个使用点都用泛型参数传入具体类型,由编译器做单态化,产出一条条零开销的调用链。
这是 Rust 和它之前几乎所有语言最不一样的一点——抽象不用付运行时代价。Haskell 也有好看的抽象,但 GHC 的优化器不能每次都消掉;Java 的接口漂亮,但去虚化要靠 JIT 在运行时猜;Go 的 interface 干净,但 interface call 是 indirect。只有 Rust(以及 C++ 的模板,代价是编译时间爆炸)能做到"定义层看起来像高阶抽象、运行层看起来像 C"。
这是 hyper + tower + axum + tonic 这一整摞库能在同一个时代成型的根本——它们每一层都在用这个"抽象不花钱"的能力。
24.8 可迁移的设计模式
读这个专栏你看到的不只是 hyper / tower。这套设计思路在 Rust 生态的每个角落都在重复。
24.8.1 "trait 统一抽象"
同一种"小 trait"模式在其他地方:
- tokio 的
AsyncRead/AsyncWrite:IO 的最小抽象。TcpStream、UnixStream、TLS stream、File 都实现它(Stdin 只实现AsyncRead),组合器(BufReader、Framed)围着它转。《Tokio 源码深度解析》专栏第 10 章里会细讲。 - serde 的
Serialize/Deserialize/Serializer/Deserializer:数据格式的最小抽象。JSON、CBOR、Bincode、TOML、MessagePack 全都是同一套 trait。数据类型只要 derive 一次,所有格式都支持。《Serde 元编程》专栏第 1、2 章讲这套 trait 怎么靠 Data Model 这一中间层把 M×N 的组合爆炸压成 M+N,第 4 章细讲反序列化侧的 visitor pattern。 - futures 的
Stream/Sink:流式数据的最小抽象。channel、socket、文件、subprocess output 都可以暴露成 Stream。 - std 的
Hash/Hasher/BuildHasher:哈希算法的最小抽象。SipHash、AHash、FxHash 任选其一。
每一个成功的 Rust 库,它的核心都是一个或两个小 trait。记住这个模式。下次你写一个新库的时候,先问自己:我能不能定义一个小 trait,把我要抽象的东西装下?如果能,你已经赢了一半。
24.8.2 "Layer 组合"
同一种"类型级中间件组合"的模式:
- tower 的 Layer:Service → Service。
- serde_json 的 Serializer 组合:
serde_json::Serializer<W, F = CompactFormatter>——Formatter作为类型参数插进Serializer,换成PrettyFormatter就是另一个具体类型。 - nom 的 parser combinators:
parser.map(|x| ...).and_then(...).or(...)——每一步都返回新类型的 parser。 - futures 的组合子:
fut.map(...).then(...).fuse()——每一步都返回新类型的 Future。
Rust 里一个成熟生态的标志,就是它的组合子丰不丰富。组合子就是"类型级函数"——输入类型 A、输出类型 B;A 和 B 都实现同一个 trait;组合子链接起来产生越来越特化的类型,在编译时被 LLVM 消化,跑起来像手写代码。
学这种写法最好的办法不是读 tutorial,是读 tower 的 ServiceBuilder 源码 + 读 serde_json 的 Serializer 实现——一处是 tower-0.5.3/tower/src/builder/mod.rs,另一处是 serde_json 仓库里的序列化器源文件 ser.rs。读完这两块你对"Rust 式组合"就有感觉了。
24.8.3 "async 状态机 + poll 模型"
Rust 的 Future / poll 模型和其他语言的异步设计放在一起看:
- Kotlin coroutines:suspend function 也被编译成状态机,但 continuation 由运行时持有、回调式恢复,没有
poll这种"由外部驱动"的形状。 - Swift async/await(2021):Task、continuation——同样是编译器合成状态机,调度交给运行时。
- JavaScript 的 async iterator:
for await ... of——生成器式的拉取模型,和Stream::poll_next形状最接近。 - Python 的 asyncio:比 Rust 早,基于生成器与事件循环,每个 await 点都有解释器开销。
Rust 把 Pin + Future::poll + Waker 这一套机制推向极致;各家不一定同路,但"异步是状态机"这个思想已经彻底普及。这种"一个语言的实现细节改变了整个领域的心智模型"的现象——C 的指针、Haskell 的 typeclass、Rust 的 borrowck——每十几年一次,异步 Rust 就是其中之一。
24.8.4 "编译器做重活"
这个专栏反复出现的一个暗线——Rust 让编译器帮你做掉很多运行时的事。这不是 Rust 独有(C++ 模板也这样),但 Rust 的做法更克制、错误信息更好、表达能力更强。我们看到的具体体现:
- Service trait 的单态化:每一层 Layer 展开成一个具体类型,调用是 inline 的。
- serde 的 derive:Serialize/Deserialize 代码是 proc-macro 在编译期生成的。
- async 脱糖:
async fn变成状态机,由编译器合成,无堆分配。 - hyper 的泛型 dispatch:
Dispatcher<D, Bs, I, T>(hyper-1.9.0/src/proto/h1/dispatch.rs:22)里每个类型参数都在编译期确定,运行期没有 vtable。
"把人类写抽象代码的负担变成编译器在编译时干的活"——这是 Rust 整个设计哲学的一句话总结。其他几个专栏(《Tokio 源码深度解析》专栏、《Serde 元编程》专栏、《Rust 编译器与运行时揭秘》专栏第 6 章)会从不同角度拆这条主线。
24.8.5 这套模式的代价
诚实地讲,这套设计不是免费的:
- 编译时间:单态化让代码膨胀,编译时间随之拉长。一个中等 tower/axum 项目的 release build 明显慢于同规模的非泛型代码。
- 错误信息复杂:
Stack<LoadShedLayer, Stack<RateLimitLayer, Stack<TimeoutLayer, ...>>>这种类型出现在错误信息里读起来很痛苦。 - 二进制体积:静态分发 + inline 让二进制膨胀。一个只有几行业务代码的 axum 服务,release 二进制里也有相当一部分是展开后的泛型代码。
- 学习曲线:读懂
impl<S, Req> Service<Req> for Wrapper<S> where S: Service<Req>, ...这种 impl 块对新人是一堵墙。
Rust 生态正在各个方向缓解这些成本——cargo-bloat、cargo-llvm-lines 查膨胀;增量编译、cranelift 提编译速度;"help" tag 和 diagnostic attribute 改错误信息——但这些代价不会完全消失。你要的不是"没代价",是"代价和收益匹配"。
24.9 往哪里继续
读源码的几条线——
- reqwest:Rust 最流行的 HTTP 客户端,底层是 hyper + tower。重点看 async_impl 目录下的 client.rs(主 Client)和 decoder.rs(gzip/br 解码的 body 包装)。
- axum 的 routing 模块:
axum/src/routing/的PathRouter(path_router.rs)、MethodRouter(method_routing.rs)、Endpoint(mod.rs)。这是 hyper::Service 上长出来的"路由 Service",代码非常干净。 - linkerd2-proxy:生产级 L7 代理。所有中间件都是 tower Layer,是 tower 生态"能达到什么高度"的例子。
- hyper 的 v0.14 → v1.0 迁移指南和 PR:理解为什么
&mut self要变&self、为什么Body的poll_data/poll_trailers要合并成poll_frame。看一次大项目怎么做不破坏生态的 API 升级。
可以练手的 Layer——
| 练手题 | 依赖章节 | 规模 |
|---|---|---|
InstrumentLayer:给任意 Service 加 tracing span |
第 3 章 | ~30 行 |
IdempotencyLayer:按 Idempotency-Key header 去重 |
第 3、5 章 | ~200 行 |
CircuitBreakerLayer:按下游错误率开关服务 |
第 4、5 章 | ~500 行 |
AdmissionControlLayer:按系统负载动态调 QPS |
第 4、6 章 | ~1000 行 |
hyper / tower / axum 的贡献入口——
- 文档和例子:hyper 的内部机制大部分没有系统文档,把某个晦涩细节写成 hyper 官方 doc PR 通常能 merge。
- edge case 的小修:hyper 的 issue tracker 有很多标了 "help wanted" 的 edge case。挑一个能复现的(例如 h2 GOAWAY 的某个 race、upgrade.rs 的某个错误路径),提一个最小复现 + 修复。
致谢
这个专栏直接依赖下列项目作者的长期工作:
- Sean McArthur——hyper 的主要维护者,自 2014 年至今。hyper 能从 0.x 慢慢磨到 1.0、API 能保持长期稳定、源码能保持这么可读——是 Sean 十二年如一日坚持 "小而对" 的结果。
- Carl Lerche——tower、tokio 的主要作者。tower 的 Service 抽象(最初由 Alex Crichton 在 tokio-service 里起草)、tokio 的 Future 运行时——Rust 异步世界的两个地基都是 Carl 主导的。
- Lucio Franco——tonic 的主要作者、tower 的 co-maintainer。tonic 是 Rust 生态里被广泛使用的纯 Rust gRPC 实现。
- David Pedersen(davidpdrsn)——axum 的主要作者。axum 把 tower + hyper 变成了 "普通 Rust 开发者每天能用" 的样子。
- dtolnay——serde、syn、quote、anyhow、thiserror 的作者,
async-trait的作者。24.5 节讲#[async_trait]的那段历史主角就是他。 - h2 crate 和 HPACK 实现的贡献者们——h2 的代码质量和 spec 覆盖度撑起了 Rust 生态的 HTTP/2。
- Niko Matsakis 与 Rust 编译器团队——GATs 稳定、async fn in trait 稳定,让 24.5 节的演进成为可能。
- hyper-util、hyper-rustls、hyper-tls、tower-http 的贡献者——胶水层不光鲜,生产系统靠这层跑起来。
- 所有在 GitHub 上写过 hyper / tower PR 的贡献者——每一次 review 都沉淀到源码里。