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;它做的事是决定什么时候拿许可、许可放在哪、什么时候还。
结构:信号量加一个可选的许可
#[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 用传进来的那个。
寄存处有 max 块号牌(信号量里的许可)。服务员先去领一块号牌揣在兜里(poll_ready 把许可存进 self.permit),领不到就在队尾等。有客人来,服务员把号牌挂在客人的包上(call 把许可移进 ResponseFuture),兜里又空了,下一位客人来之前得再去领。客人取走包,或者包被扔了(响应完成,或 future 被 drop),号牌回到柜台。新来一个服务员(clone),他和别人共用同一个柜台,但兜里是空的。
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 自己不产生错误,也不装箱。
按 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:?}");
}
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 共享同一个「最大连接数」。
问: 我用一个 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时expectpanic。
小结
ConcurrencyLimit= 内层 +PollSemaphore+Option<OwnedSemaphorePermit>。poll_ready没许可就先取,取到存进self.permit,再问内层;内层Pending时许可留着,不会重复取。call把许可take出来放进ResponseFuture,归还靠 drop:响应结束、超时、取消都会还;没poll_ready就call会 panic。Clone共享信号量,副本不带许可。ConcurrencyLimitLayer每次layer都新建信号量,跨服务共享用GlobalConcurrencyLimitLayer。- 排队交给 tokio 的公平信号量:归还的许可直接交给最早排队的人。