Hyper 与 Tower:工业级 HTTP 栈

第24章 设计哲学:从 Tower 到 async trait 的演进

作者 杨艺韬 · 11,108 字 · 发布于 · 更新于

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 为什么自己做一个 Service trait、不直接复用 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 内部跑了十几年,证明了两件事:

  1. "网络调用 = 函数"这个抽象足够通用——RPC、HTTP、gRPC、DB 客户端、缓存客户端,都能套进去。
  2. "中间件 = 函数组合"的好处是模块性彻底地类型级可验证——编译期就能知道中间件能不能套在服务上。

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 标准库的 Future trait。

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 不是白拿的。它的代价有三个:

  1. 两步协议增加了心智负担:用户很容易写出先 call 再被 panic 的代码;或者在 poll_ready 与 call 之间插进别的 await,白白占着预留的容量;预留了却最终没 call,还得保证资源被释放。tower-service 的文档(tower-0.5.3/tower-service/src/lib.rs:346-350)专门花了一段提醒实现者处理后一种情况。
  2. 资源预留可能浪费:poll_ready 返回 Ready 之后,如果你没调 call 就 drop 了 Service,预留的资源(semaphore permit、队列位置)必须被释放。这要求每个中间件作者都写正确的 Drop。
  3. &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;
}

两个关键改动:

  1. 没有 poll_ready。
  2. 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 对读者的启示

作为读者,你从这次分叉可以学到三件事:

  1. 一个 trait 一旦稳定就很难改——即使你觉得"应该 &self 更好",如果下游依赖 &mut self 的语义(例如 retry 的状态要写在 &mut self 里),你改了就是 breaking change。tower 当年选了 &mut self 是那时候最合理的选择,但留下了今天的历史债务。
  2. 当抽象被锁死而需求在演化,新做一个比硬改一个更好——hyper 做了新 trait,没有试图在 tower 里加 feature flag;这比"兼容性补丁"健康得多,代价是生态暂时分叉。
  3. 分叉不是末日,桥梁就好——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;
}

两个关键点:

  1. Future 被装在 Pin<Box<dyn Future>> 里——动态分发,每次 call 都要堆分配。
  2. 生命周期被显式标注——'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:看它的 Handler trait(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 月):

  1. 把 Service::call 改成 async fn in trait:把 fn call(&mut self, req) -> Self::Future 换成 async fn call(&mut self, req),Future 隐式。
  2. 去掉 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 都沉淀到源码里。