Steer 与 MakeService
主线位置:
Steer在就绪主线上做文章,poll_ready要等所有服务都就绪,call再按请求挑一个,future 原样返回。MakeService把两条主线套了两层:外层的poll_ready问「还能造服务吗」,call的 future 交出来的是一个新服务,这个服务再走自己的两条主线。
flowchart TB
pr["Steer poll_ready"] --> empty{not_ready 空了?}
empty -->|是| ok["Ready(Ok)"]
empty -->|否| head[问队首服务]
head -->|Pending| pend[Pending,后面的不问]
head -->|Ready| pop[弹出队首]
pop --> empty
callreq["Steer call(req)"] --> pick[Picker 选下标]
pick --> back[下标放回 not_ready]
back --> fwd[调用该服务]
本页两个主题都和「多个服务」有关,但方向相反:Steer 把一个请求分给已有的几个服务之一,MakeService 按需要造出新的服务。
Steer:按请求路由
Picker 决定一个请求去哪:
pub trait Picker<S, Req> {
/// Return an index into the iterator of `Service` passed to [`Steer::new`].
fn pick(&mut self, r: &Req, services: &[S]) -> usize;
}
返回的是下标。闭包 Fn(&Req, &[S]) -> usize 自动实现了它。所有服务必须是同一个类型 S,类型不同就先擦除类型,模块文档的例子把每个服务都包成了 BoxService(见 类型擦除)。分片(sharding)和简单的路由都是这种用法。
pub struct Steer<S, F, Req> {
router: F,
services: Vec<S>,
not_ready: VecDeque<usize>,
_phantom: PhantomData<Req>,
}
not_ready 是还没确认就绪的服务的下标队列。new 把所有下标都放进去。
poll_ready:所有服务都要就绪
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
loop {
// must wait for *all* services to be ready.
// this will cause head-of-line blocking unless the underlying services are always ready.
if self.not_ready.is_empty() {
return Poll::Ready(Ok(()));
} else {
if self.services[self.not_ready[0]]
.poll_ready(cx)?
.is_pending()
{
return Poll::Pending;
}
self.not_ready.pop_front();
}
}
}
按队列顺序一个一个问,队首 Pending 就整个 Pending,排在后面的服务这一轮根本没被问到。任何一个服务报错,? 直接把错误返回,Error 就是 S::Error,Steer 不包装。
为什么非要全部就绪?因为 poll_ready 拿不到请求。调用方先问 poll_ready,后给请求,Steer 回答「能接」的时候还不知道下一个请求要去哪个服务,只能保证去哪个都行。结构体的文档把这件事和它的后果说得很直白:
Note that [
Steer] must wait for all services to be ready since it can't know ahead of time which [Service] the next message will arrive for, and is unwilling to buffer items indefinitely. This will cause head-of-line blocking unless paired with a [Service] that does buffer items indefinitely, and thus always returns [Poll::Ready]. For example, wrapping each component service with a [Buffer] with a high enough limit (the maximum number of concurrent requests) will prevent head-of-line blocking in [Steer].
也就是:一个后端慢了或满了,发往其他后端的请求也一起等。这个现象单独列在 Steer 一个后端慢,整个路由都慢。
call:挑一个,只让它重新排队
call:
fn call(&mut self, req: Req) -> Self::Future {
assert!(
self.not_ready.is_empty(),
"Steer must wait for all services to be ready. Did you forget to call poll_ready()?"
);
let idx = self.router.pick(&req, &self.services[..]);
let cl = &mut self.services[idx];
self.not_ready.push_back(idx);
cl.call(req)
}
- 没就绪就
call,assert!panic。 - 只有被调用的那个服务回到
not_ready。其余的服务上次回答过「就绪」、之后又没被call,按 Service trait 的规则它们仍然就绪,不用再问。 Picker返回的下标越界,self.services[idx]会 panic。Future就是S::Future,请求主线上Steer什么都不加。
实验:队头阻塞,以及 Buffer 怎么解开它
下面的程序做三件事:两个后端,后端 0 不就绪,看 Steer 能不能就绪;每个后端前面套一个 Buffer 再试;最后克隆一个已就绪的 Steer 直接 call。依赖 tower = { version = "=0.5.3", features = ["steer", "buffer", "limit", "util"] }、tower-test = "0.4"、tokio-test = "0.4"、tokio(macros、rt):
use std::convert::Infallible;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::task::Poll;
use tokio_test::task;
use tower::buffer::Buffer;
use tower::limit::ConcurrencyLimit;
use tower::steer::Steer;
use tower::{Service, ServiceExt, service_fn};
use tower_test::mock;
fn pick<S>(req: &&'static str, _: &[S]) -> usize {
if req.starts_with("to0") { 0 } else { 1 }
}
fn show<T, E>(p: Poll<Result<T, E>>) -> &'static str {
match p {
Poll::Ready(Ok(_)) => "Ready",
Poll::Ready(Err(_)) => "Err",
Poll::Pending => "Pending",
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// 1. Backend 0 is not ready, backend 1 is.
let (s0, mut h0) = mock::pair::<&'static str, &'static str>();
let (s1, mut h1) = mock::pair::<&'static str, &'static str>();
h0.allow(0);
h1.allow(10);
let mut steer = task::spawn(Steer::new(vec![s0, s1], pick));
let p = steer.enter(|cx, mut s| s.poll_ready(cx));
println!("plain: poll_ready for a request to 1 -> {}", show(p));
// 2. Same, but each backend sits behind a Buffer.
let (s0, mut h0) = mock::pair::<&'static str, &'static str>();
let (s1, mut h1b) = mock::pair::<&'static str, &'static str>();
h0.allow(0);
h1b.allow(10);
let mut steer = Steer::new(vec![Buffer::new(s0, 8), Buffer::new(s1, 8)], pick);
let fut = steer.ready().await.unwrap().call("to1: hello");
let (req, send) = h1b.next_request().await.unwrap();
send.send_response("pong");
println!("buffered: backend 1 got {req:?}, response {:?}", fut.await.unwrap());
drop(h1);
// 3. Clone a Steer that is ready.
let svc = |n: u32| ConcurrencyLimit::new(service_fn(move |_: &'static str| async move { Ok::<_, Infallible>(n) }), 1);
let mut steer = Steer::new(vec![svc(0), svc(1)], pick);
steer.ready().await.unwrap();
let mut copy = steer.clone();
std::panic::set_hook(Box::new(|_| {}));
let r = catch_unwind(AssertUnwindSafe(|| {
let _ = copy.call("to1: from the clone");
}));
let msg = r.err().and_then(|p| {
p.downcast_ref::<String>()
.cloned()
.or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
});
println!("clone: call without poll_ready -> panic {msg:?}");
}
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
plain: poll_ready for a request to 1 -> Pending
buffered: backend 1 got "to1: hello", response "pong"
clone: call without poll_ready -> panic Some("max requests in-flight; poll_ready must be called first")
- 第 1 行:后端 1 完全可以接请求,
Steer还是Pending,因为队首的后端 0 不就绪。 - 第 2 行:
Buffer::poll_ready只问自己的通道有没有空位,不问内层(见 Buffer),所以后端 0 卡住时它照样就绪,Steer也就绪,发往后端 1 的请求正常完成。代价是请求在后端 0 的通道里排队;通道容量(这里是 8)用完以后,Buffer也会Pending,阻塞又回来了。这就是文档说「a high enough limit (the maximum number of concurrent requests)」的原因。 - 第 3 行见下面的框。
问: Steer 实现了 Clone。克隆一个已经就绪的 Steer,副本能直接 call 吗?
答: 不能,但 panic 的不是 Steer 自己的 assert!。Clone 把 not_ready 也原样复制了,原件的 not_ready 是空的,副本的也是空的,于是 Steer 以为所有服务都就绪,放行了。可是 services 里每个服务各自克隆,克隆出来的服务不带就绪状态(ConcurrencyLimit 的副本手里没有许可),真正 panic 的是内层:max requests in-flight; poll_ready must be called first。克隆出来的 Steer 和其他服务一样,要先 poll_ready。
MakeService:返回服务的服务
服务端每接受一个连接,往往要为它准备一个服务;客户端每连一个新地址,也要为它准备一个服务。这件事本身也是「请求 → 响应」:请求是目标(连接、地址),响应是一个服务。tower 没有为它另起一套 trait,而是规定:一个响应类型是 Service 的 Service,就是 MakeService。
pub trait MakeService<Target, Request>: Sealed<(Target, Request)> {
type Response;
type Error;
type Service: Service<Request, Response = Self::Response, Error = Self::Error>;
type MakeError;
type Future: Future<Output = Result<Self::Service, Self::MakeError>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::MakeError>>;
fn make_service(&mut self, target: Target) -> Self::Future;
// ...
}
唯一的实现是一个覆盖实现:
impl<M, S, Target, Request> MakeService<Target, Request> for M
where
M: Service<Target, Response = S>,
S: Service<Request>,
{
type Response = S::Response;
type Error = S::Error;
type Service = S;
type MakeError = M::Error;
type Future = M::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::MakeError>> {
Service::poll_ready(self, cx)
}
fn make_service(&mut self, target: Target) -> Self::Future {
Service::call(self, target)
}
}
- 它是密封的(
Sealed),用户不能直接为自己的类型实现MakeService,只能实现Service<Target>,自动获得MakeService。文档自己的说法是「essentially a trait alias for a [Service] of [Service]s」。 MakeService的作用是给泛型约束起个好读的名字:写M: MakeService<Conn, Request>,比写M: Service<Conn>, M::Response: Service<Request>清楚,还顺带给出M::Service、M::MakeError这些名字。- 外层
poll_ready返回Pending,意思是「暂时不能再造服务」,用来对连接数之类做背压。
into_service 和 as_service 把它变回 Service<Target>(IntoService 拿走所有权,AsService 借用),两者都只是转发。泛型代码只知道 M: MakeService<T, R> 时,编译器并不知道 M 也是 Service<T>,用这两个方法换一下,就能接着用 ServiceExt 的 oneshot、ready 这些方法。
同一个模块还有 MakeConnection,同样的套路:响应是 AsyncRead + AsyncWrite 的 Service<Target> 自动就是 MakeConnection,用来造传输层连接。tower 内部用到 MakeService 的是 Reconnect(内层出错就重新造一个)和 P2C 那一节提到的 MakeBalance。
Shared:每次克隆同一个服务
最常见的 MakeService 不需要按目标做任何事,只要每个连接一份服务。Shared 就是这个:
impl<S, T> Service<T> for Shared<S>
where
S: Clone,
{
type Response = S;
type Error = Infallible;
type Future = SharedFuture<S>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _target: T) -> Self::Future {
SharedFuture::new(std::future::ready(Ok(self.service.clone())))
}
}
- 对任意目标类型
T都实现,目标被忽略。 - 永远就绪,永远成功(
Infallible),future 是一个已经完成的Ready。 - 每次
call克隆原件,所以每份服务都从原件当时的状态开始;内层服务里有共享状态(比如Arc)就共享,有各自的字段就各自一份。服务不能Clone时,文档的例子是先套Buffer。
() 作目标Shared 来自 issue #262,需求是:「A user may wish to satisfy a MakeService<T, Req> requirement with an existing Service<Req>, and have a clone used each time.」
实现它的 PR #533 描述里,作者在考虑让 Shared 只实现 Service<()>,「since cloning doesn't require anything」,又担心接受任意 Target 会让 API 变复杂。合并时的提交里有一条「Make Shared generic over any target」,最终就是现在的 impl<S, T> Service<T> for Shared<S>:不用新 trait,目标是什么类型都行,直接忽略。这正是它能原样传给 axum serve 的原因(下一节)。这个改动在 tower 0.4.5 发布。
hyper 与 axum 怎么用这个模式
这个模式最出名的用户是 HTTP 服务器:目标是一个新连接,造出来的服务处理这条连接上的请求。下面描述的是相关 crate 的源码,不是 tower 的。
- hyper 0.14:
Server::serve(make_service)。服务器的接受循环每次先调 make service 的poll_ready,就绪了才去 accept 下一个连接,然后以&io为目标调用它,得到这条连接的服务(hyper 0.14.32 的server/server.rs,poll_next_)。make service 返回Pending,服务器就暂停接受新连接。hyper 那时有自己的make_service_fn。 - hyper 1.x:hyper 不再依赖 tower。它有自己的
hyper::service::Service,只有一个fn call(&self, req),没有poll_ready,也去掉了Server,接受连接交给使用者。tower 服务要经过 hyper-util 的TowerToHyperService转换,它的call是Oneshot::new(self.service.clone(), req):每个请求克隆一次服务,再走一遍完整的poll_ready+call。 - axum 0.8.9:
axum::serve(listener, make_service)要求make_service是Service<IncomingStream<'_, L>>,响应是处理 HTTP 请求的服务。它在 accept 之后的handle_connection里先make_service.ready().await,再call(IncomingStream { io: &io, remote_addr })拿到这条连接的服务,套上TowerToHyperService交给 hyper-util 的连接处理器。Router自己就实现了Service<IncomingStream<'_, L>>,call返回self.clone().with_state(()),所以可以直接axum::serve(listener, router)。into_make_service()得到的IntoMakeService和Shared几乎一字不差;into_make_service_with_connect_info()则真的用到了目标,从中取出对端地址,作为扩展塞进每个请求。
所以在 axum 里有两次克隆:每个连接从 make service 拿一份,每个请求又从这一份克隆一次。服务里的字段状态活不过一个请求,要跨请求共享的状态得放在 Arc 之类的共享结构里,或者用 axum 的 State。
下面的程序验证这一点。Counter 是一个带计数字段的服务,每次 call 加一并返回当前值。先手动当 MakeService 用:造两个「连接」的服务,A 上调两次、B 上调一次;再把同样的 Shared 交给 axum::serve,用裸 TCP 在一条 keep-alive 连接上发两个请求,另一条连接上发一个。依赖 tower = { version = "=0.5.3", features = ["make", "util"] }、axum = "=0.8.9"、tokio(macros、rt、net、io-util):
use std::convert::Infallible;
use std::future::{Ready, ready};
use std::task::{Context, Poll};
use axum::body::Body;
use axum::extract::Request;
use axum::response::Response;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tower::make::{MakeService, Shared};
use tower::{Service, ServiceExt};
// A service with per-instance state: how many requests this instance has seen.
#[derive(Clone)]
struct Counter {
count: u32,
}
impl Service<Request> for Counter {
type Response = Response;
type Error = Infallible;
type Future = Ready<Result<Response, Infallible>>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _: Request) -> Self::Future {
self.count += 1;
ready(Ok(Response::new(Body::from(format!("count={}", self.count)))))
}
}
async fn body_of(svc: &mut Counter) -> String {
let rsp = svc.ready().await.unwrap().call(Request::new(Body::empty())).await.unwrap();
let bytes = axum::body::to_bytes(rsp.into_body(), 64).await.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
// Send one HTTP/1.1 request on an open connection and return the body.
async fn get(conn: &mut TcpStream) -> String {
conn.write_all(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n").await.unwrap();
let mut buf = Vec::new();
loop {
let mut chunk = [0u8; 1024];
let n = conn.read(&mut chunk).await.unwrap();
buf.extend_from_slice(&chunk[..n]);
let text = String::from_utf8_lossy(&buf).to_string();
if let Some(pos) = text.find("\r\n\r\n") {
let len: usize = text
.lines()
.find_map(|l| l.strip_prefix("content-length: "))
.unwrap()
.trim()
.parse()
.unwrap();
if text.len() >= pos + 4 + len {
return text[pos + 4..pos + 4 + len].to_string();
}
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
// 1. Shared by hand: one service per "connection", each a clone of the original.
let mut make = Shared::new(Counter { count: 0 });
let mut conn_a = make.make_service(()).await.unwrap();
let mut conn_b = make.make_service(()).await.unwrap();
let a1 = body_of(&mut conn_a).await;
let a2 = body_of(&mut conn_a).await;
let b1 = body_of(&mut conn_b).await;
println!("by hand: conn A -> {a1}, {a2}; conn B -> {b1}");
// 2. The same Shared handed to axum::serve.
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
axum::serve(listener, Shared::new(Counter { count: 0 })).await.unwrap();
});
let mut a = TcpStream::connect(addr).await.unwrap();
let mut b = TcpStream::connect(addr).await.unwrap();
let a1 = get(&mut a).await;
let a2 = get(&mut a).await;
let b1 = get(&mut b).await;
println!("axum: conn A -> {a1}, {a2}; conn B -> {b1}");
}
count= 几?第一行是手动调用 make_service,第二行是 axum。完整输出在文末。
动脑筋答案
M2 Max、rustc 1.99.0、--release,跑 3 次输出相同:
by hand: conn A -> count=1, count=2; conn B -> count=1
axum: conn A -> count=1, count=1; conn B -> count=1
- 手动:
count=2。Shared为每个连接克隆一次原件,A 和 B 各从 0 开始;A 上的两次调用用的是同一份conn_a,计数累加到 2。 - axum:
count=1。 连接 A 拿到的那份服务交给了TowerToHyperService,它每个请求再克隆一次,call改的是这个请求独有的副本,下一个请求又从连接那一份(count仍是 0)克隆。所以每个请求都看到 1。 - 两行的连接 B 都是 1:每个连接都从原件克隆。
小结
Steer用Picker按请求选下标;poll_ready按顺序问not_ready里的服务,全部就绪才Ready,因为它看不到下一个请求。一个后端不就绪,所有请求都等:源码注释明说这是队头阻塞。- 给每个后端套足够大的
Buffer能解开队头阻塞,容量用完时阻塞会回来。 call只让被调用的服务重新排队;Steer的Clone会复制not_ready,副本必须先poll_ready。MakeService是密封的「返回服务的服务」,靠覆盖实现自动获得;外层poll_ready用来对「造服务」做背压。Shared对任意目标都返回原件的克隆,永远就绪、不会失败。- axum 的
serve每个连接从 make service 拿一份服务,hyper-util 再在每个请求上克隆一次;服务字段里的状态只活一个请求。