Rust in Everything
基于 tower 0.5.3

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)和简单的路由都是这种用法。

Steer:

pub struct Steer<S, F, Req> {
    router: F,
    services: Vec<S>,
    not_ready: VecDeque<usize>,
    _phantom: PhantomData<Req>,
}

not_ready 是还没确认就绪的服务的下标队列。new 把所有下标都放进去。

poll_ready:所有服务都要就绪

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 行见下面的框。
📝NOTE
没有蠢问题

问: 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。

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。
📝NOTE
冷知识:Shared 一开始差点只接受 () 作目标

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}");
}
❗IMPORTANT
动脑筋:两行输出里,连接 A 的第二个请求分别返回 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 再在每个请求上克隆一次;服务字段里的状态只活一个请求。

💬 关联讨论

发起讨论
加载中...