Rust in Everything
基于 tower 0.5.3

ConcurrencyLimit

主线位置:两条主线都在。就绪主线上,poll_ready 先占一个许可,取不到就 Pending;请求主线上,许可跟着响应 future 走,响应结束才归还。

flowchart TB
  pr["poll_ready"] --> has{手里有许可?}
  has -->|没有| acq[PollSemaphore 取许可]
  acq -->|取不到| pend[Pending,等别人归还]
  acq -->|取到| store[存进 self.permit]
  has -->|有| inner[问内层 poll_ready]
  store --> inner
  call["call(req)"] --> take[从 self.permit 拿走]
  take --> rf[future 带着许可]
  rf --> done[完成或被 drop]
  done --> rel[许可归还信号量]

ConcurrencyLimit 限制的是「同时在途的请求数」:已经 call、响应 future 还没结束的请求。它自己不排队,排队交给 tokio 的 Semaphore;它做的事是决定什么时候拿许可、许可放在哪、什么时候还。

结构:信号量加一个可选的许可

ConcurrencyLimit:

#[derive(Debug)]
pub struct ConcurrencyLimit<T> {
    inner: T,
    semaphore: PollSemaphore,
    /// The currently acquired semaphore permit, if there is sufficient
    /// concurrency to send a new request.
    ///
    /// The permit is acquired in `poll_ready`, and taken in `call` when sending
    /// a new request.
    permit: Option<OwnedSemaphorePermit>,
}
  • semaphore 是 tokio-util 的 PollSemaphore,里面包着一个 Arc<tokio::sync::Semaphore>。tokio 的 Semaphore 要排队等许可只能 .await 它的 acquire_owned,PollSemaphore 把这个 future 存起来,对外提供能在 poll_ready 里调用的 poll_acquire。信号量本身怎么排队、怎么保证先来先得,见 batch_semaphore。
  • permit 是 OwnedSemaphorePermit:它持有 Arc<Semaphore> 而不是借用,所以能离开服务、放进响应 future,future 活多久它就活多久。

ConcurrencyLimit 原来叫 InFlightLimit,2019 年的 PR #235 把它和 RateLimit 一起挪进 tower-limit crate 时改了名。

new 新建一个 Semaphore::new(max),with_semaphore 用传进来的那个。

💡TIP
打个比方:寄存处的号牌

寄存处有 max 块号牌(信号量里的许可)。服务员先去领一块号牌揣在兜里(poll_ready 把许可存进 self.permit),领不到就在队尾等。有客人来,服务员把号牌挂在客人的包上(call 把许可移进 ResponseFuture),兜里又空了,下一位客人来之前得再去领。客人取走包,或者包被扔了(响应完成,或 future 被 drop),号牌回到柜台。新来一个服务员(clone),他和别人共用同一个柜台,但兜里是空的。

比方到这里为止:真实的号牌被还回柜台时,如果有人在排队,会直接递到排在最前面的那个人手里,而不是放回柜台让大家抢,所以「柜台上的号牌数」可能一直是 0。

poll_ready:先占许可,再问内层

poll_ready:

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // If we haven't already acquired a permit from the semaphore, try to
        // acquire one first.
        if self.permit.is_none() {
            self.permit = ready!(self.semaphore.poll_acquire(cx));
            debug_assert!(
                self.permit.is_some(),
                "ConcurrencyLimit semaphore is never closed, so `poll_acquire` \
                 should never fail",
            );
        }

        // Once we've acquired a permit (or if we already had one), poll the
        // inner service.
        self.inner.poll_ready(cx)
    }

几个要点:

  • 取到许可后才问内层。 没有许可时,ready! 直接返回 Pending,内层根本没被问到。所以背压在这里截住了:上限满了,内层不会看到多出来的 poll_ready。
  • 许可只取一次。 self.permit 已经是 Some,就跳过取许可,直接问内层。调用方在 call 之前反复 poll_ready(比如内层一直 Pending)不会多占许可。
  • 内层 Pending 或报错时,许可留在 self.permit 里。 下次 poll_ready 不用重新排队。代价是:一个就绪了却一直不 call 的服务,会一直占着一个许可。这正是 Service trait 里那条规则说的情况,poll_ready 预留的资源要在服务被 drop 时释放:ConcurrencyLimit 被 drop,Option<OwnedSemaphorePermit> 跟着 drop,许可归还。
  • poll_acquire 返回 None 只在信号量被关闭时发生。ConcurrencyLimit 从不关闭信号量,所以这里只有 debug_assert!。不过 with_semaphore 接受外部传进来的信号量,外部要是把它关了,release 构建里 self.permit 会是 None,接着去问内层,call 时再 panic。

Error 类型直接是内层的 S::Error,ConcurrencyLimit 自己不产生错误,也不装箱。

📝NOTE
冷知识:tower 曾经自己包装信号量,每次排队都分配一个 Box

按 PR #556 的描述,tokio::sync::Semaphore 没有提供能在 async/await 之外取 owned 许可的 poll_acquire。tower 0.4 早期的 ConcurrencyLimit 和 Buffer 用的是 tower 自己写的包装,它「creating a new Semaphore::acquire_owned future and boxing it every time the semaphore starts acquiring a new permit」,每开始取一次许可就装箱一个新的 future。

后来 tokio-util 加了 PollSemaphore,用 ReusableBoxFuture 反复复用同一块分配。PR 里的说法是:改用它之后,「there's a single allocation for each clone of the service. Unless services are cloned per-request, this means that the allocation is moved out of the request path in most cases.」作者当初想过自己这么做,但不想为此往 tower 里加 unsafe 代码(「tower (which presently contains no unsafe code)」),既然 tokio-util 有了现成的实现,就直接用。代价是开 limit 或 buffer feature 时多依赖一个 tokio-util。这个改动在 tower 0.4.5 发布,本页 semaphore: PollSemaphore 这个字段就是这么来的。

call:把许可移进 future

call:

    fn call(&mut self, request: Request) -> Self::Future {
        // Take the permit
        let permit = self
            .permit
            .take()
            .expect("max requests in-flight; poll_ready must be called first");

        // Call the inner service
        let future = self.inner.call(request);

        ResponseFuture::new(future, permit)
    }

take() 之后 self.permit 回到 None,下一个请求必须重新 poll_ready。没 poll_ready 就 call,expect 会 panic,信息是 max requests in-flight; poll_ready must be called first。

ResponseFuture 只是把许可放进一个字段:

    pub struct ResponseFuture<T> {
        #[pin]
        inner: T,
        // Keep this around so that it is dropped when the future completes
        _permit: OwnedSemaphorePermit,
    }

poll 原样转发给 inner,没有任何「完成时归还」的代码。归还靠的是 drop:OwnedSemaphorePermit 的 Drop 调用信号量的 release。这意味着许可的寿命是响应 future 的寿命,不是「内层返回 Ready」的那一刻:

  • 调用方 await 到结果,再 drop future,许可归还。
  • 外层 Timeout 超时、调用方取消请求,future 被 drop,许可同样归还。
  • 调用方拿到 Ready 之后迟迟不 drop future,许可就一直占着。

Clone:共享信号量,不带许可

OwnedSemaphorePermit 不能 Clone,所以 Clone 是手写的(第 95 行):

impl<T: Clone> Clone for ConcurrencyLimit<T> {
    fn clone(&self) -> Self {
        // Since we hold an `OwnedSemaphorePermit`, we can't derive `Clone`.
        // Instead, when cloning the service, create a new service with the
        // same semaphore, but with the permit in the un-acquired state.
        Self {
            inner: self.inner.clone(),
            semaphore: self.semaphore.clone(),
            permit: None,
        }
    }
}

PollSemaphore::clone 克隆的是里面的 Arc<Semaphore>,所以所有副本共用一个上限。新副本的 permit 是 None:已经就绪的服务 clone() 一份去 call,副本里没有许可,会 panic。

下面的程序用 GlobalConcurrencyLimitLayer::with_semaphore 把信号量握在手里,每一步打印剩余许可数。上限是 2。

tower = { version = "=0.5.3", features = ["limit", "util"] }
tokio = { version = "=1.54.1", features = ["rt", "macros", "sync"] }
use std::{
  convert::Infallible,
  future::poll_fn,
  panic::{catch_unwind, AssertUnwindSafe},
  sync::Arc,
  task::Poll,
};
use tokio::sync::Semaphore;
use tower::{limit::GlobalConcurrencyLimitLayer, service_fn, Layer, Service};

async fn poll_ready_once<S: Service<u32>>(svc: &mut S) -> &'static str {
  poll_fn(|cx| {
    Poll::Ready(match svc.poll_ready(cx) {
      Poll::Ready(Ok(())) => "Ready",
      Poll::Ready(Err(_)) => "Err",
      Poll::Pending => "Pending",
    })
  })
  .await
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let sem = Arc::new(Semaphore::new(2));
  let inner = service_fn(|x: u32| async move { Ok::<_, Infallible>(x) });
  let mut a = GlobalConcurrencyLimitLayer::with_semaphore(sem.clone()).layer(inner);

  println!("1 start:                available = {}", sem.available_permits());
  println!("  a.poll_ready -> {}", poll_ready_once(&mut a).await);
  println!("2 a ready, no call:     available = {}", sem.available_permits());
  println!("  a.poll_ready -> {}", poll_ready_once(&mut a).await);
  println!("3 a polled again:       available = {}", sem.available_permits());
  let fut = a.call(1);
  println!("4 a called, fut alive:  available = {}", sem.available_permits());

  let mut b = a.clone();
  println!("  b.poll_ready -> {}", poll_ready_once(&mut b).await);
  println!("5 clone b ready:        available = {}", sem.available_permits());
  let mut c = a.clone();
  println!("  c.poll_ready -> {}", poll_ready_once(&mut c).await);

  drop(fut);
  println!("6 fut dropped:          available = {}", sem.available_permits());
  println!("  c.poll_ready -> {}", poll_ready_once(&mut c).await);

  let mut d = a.clone();
  std::panic::set_hook(Box::new(|_| {})); // keep the default hook quiet
  let r = catch_unwind(AssertUnwindSafe(|| {
    let _ = d.call(2);
  }));
  let p = r.unwrap_err();
  let msg = p
    .downcast_ref::<String>()
    .cloned()
    .or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()));
  println!("7 d.call without poll_ready panics: {msg:?}");
}
❗IMPORTANT
动脑筋:第 6 行的 available 是几?

到第 5 步,a 的响应 future fut 和 b 各占一个许可,c 的 poll_ready 返回 Pending。然后 drop(fut),一个许可回到信号量。第 6 行打印的剩余许可数是 0 还是 1?紧接着 c.poll_ready 会返回什么?完整输出在文末。

跨服务共享上限:GlobalConcurrencyLimitLayer

ConcurrencyLimitLayer 只存一个 max,layer 每次调用都走 ConcurrencyLimit::new,也就是每次都新建一个信号量。GlobalConcurrencyLimitLayer 存的是 Arc<Semaphore>,layer 走 with_semaphore,同一个 layer 套出来的所有服务共用一个信号量:

#[derive(Debug, Clone)]
pub struct GlobalConcurrencyLimitLayer {
    semaphore: Arc<Semaphore>,
}

它是 tower 0.4.8 加的(PR #574),PR 描述里的用途是让多个 listener 共享同一个「最大连接数」。

📝NOTE
没有蠢问题

问: 我用一个 ConcurrencyLimitLayer::new(10) 去包了两个不同的服务,总上限是 10 还是 20?

答: 20。上限的范围由信号量决定,不由 layer 决定。ConcurrencyLimitLayer::layer 每次都 Semaphore::new(max),两个服务各有一个信号量。同一个服务 clone 出来的副本才共享(Clone 克隆的是 Arc)。要让不同的服务共享,用 GlobalConcurrencyLimitLayer,或者自己建一个 Arc<Semaphore> 传给 with_semaphore。

和其他中间件叠在一起

  • 在 Timeout 里外都一样:等许可发生在 poll_ready,Timeout 只从 call 开始计时,所以排队时间永远不算超时,见 Timeout。
  • 想超限时直接拒绝而不是等:在外面套 LoadShed。顺序反了不起作用。
  • 在 Buffer 里面:Buffer 的 worker 是唯一调用 ConcurrencyLimit 的人,所有调用方共用一个上限;ConcurrencyLimit 返回 Pending 时 worker 停下,通道慢慢填满,Buffer::poll_ready 再对调用方返回 Pending,背压就这样传出去。见 Buffer。

动脑筋答案

M2 Max、rustc 1.99.0、--release,连跑 3 次,输出相同:

1 start:                available = 2
  a.poll_ready -> Ready
2 a ready, no call:     available = 1
  a.poll_ready -> Ready
3 a polled again:       available = 1
4 a called, fut alive:  available = 1
  b.poll_ready -> Ready
5 clone b ready:        available = 0
  c.poll_ready -> Pending
6 fut dropped:          available = 0
  c.poll_ready -> Ready
7 d.call without poll_ready panics: Some("max requests in-flight; poll_ready must be called first")
  • 2:还没 call,许可已经被拿走了,它在 a.permit 里。
  • 3:第二次 poll_ready 不再取许可,还是 1。
  • 4:call 把许可从 a 移进 fut,信号量看不出区别,还是 1。
  • 5:b 是 a 的副本,共用信号量,取走最后一个。c 取不到,Pending,它的取许可请求排进了信号量的等待队列。
  • 6:0。 fut 被 drop,许可归还时信号量发现有人在排队,直接把许可记到 c 的等待节点上,没有加回计数。这就是 batch_semaphore 里「有人排队时计数恒为 0」的不变量。所以紧接着 c.poll_ready 是 Ready:许可早就是它的了。
  • 7:d 是新副本,permit 为 None,call 时 expect panic。

小结

  • ConcurrencyLimit = 内层 + PollSemaphore + Option<OwnedSemaphorePermit>。
  • poll_ready 没许可就先取,取到存进 self.permit,再问内层;内层 Pending 时许可留着,不会重复取。
  • call 把许可 take 出来放进 ResponseFuture,归还靠 drop:响应结束、超时、取消都会还;没 poll_ready 就 call 会 panic。
  • Clone 共享信号量,副本不带许可。ConcurrencyLimitLayer 每次 layer 都新建信号量,跨服务共享用 GlobalConcurrencyLimitLayer。
  • 排队交给 tokio 的公平信号量:归还的许可直接交给最早排队的人。

💬 关联讨论

发起讨论
加载中...