LoadShed
主线位置:就绪主线的终点。内层的
Pending传到这里就停了,LoadShed把它换成Ready,再在请求主线上换成一个立即失败的 future。
flowchart TB
pr["poll_ready"] --> ip{问内层}
ip -->|"Ready(Ok)"| t["is_ready = true"]
ip -->|Pending| f["is_ready = false"]
ip -->|"Ready(Err)"| err[返回错误]
t --> ok["返回 Ready(Ok)"]
f --> ok
call["call(req)"] --> chk{is_ready?}
chk -->|是| inner["置回 false,调内层"]
chk -->|否| ov[Overloaded future]
tower 的默认行为是背压:内层忙,poll_ready 就 Pending,调用方等。等待本身要占资源(连接、任务、内存),过载时排队越来越长,每个请求都慢。LoadShed 换了一种回答:忙就立刻拒绝,让调用方马上知道,而不是让它排着。
结构:一个布尔值
#[derive(Debug)]
pub struct LoadShed<S> {
inner: S,
is_ready: bool,
}
is_ready 记录上一次 poll_ready 时内层是否就绪。LoadShedLayer 没有任何参数。
poll_ready:问一次,总说 Ready
poll_ready 和 call:
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
// We check for readiness here, so that we can know in `call` if
// the inner service is overloaded or not.
self.is_ready = match self.inner.poll_ready(cx) {
Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())),
r => r.is_ready(),
};
// But we always report Ready, so that layers above don't wait until
// the inner service is ready (the entire point of this layer!)
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Req) -> Self::Future {
if self.is_ready {
// readiness only counts once, you need to check again!
self.is_ready = false;
ResponseFuture::called(self.inner.call(req))
} else {
ResponseFuture::overloaded()
}
}
- 内层报错照样传出去。 只有
Pending被改写,Ready(Err)原样返回(装箱成BoxError)。服务坏了和服务忙是两回事。 - 就绪只算一次。
call用掉is_ready后立刻置回false。下一个请求必须重新poll_ready,否则直接Overloaded。这和 Service trait 的规则一致:一次Ready只换一次call。 - 没
poll_ready就call不会 panic。is_ready初始是false,结果是一个Overloaded错误。 - 内层还是被问过了。 内层返回
Pending时已经用cx登记了 waker,可能还占了排队位置。LoadShed不再等它,但这些副作用留在内层身上,后面会看到它的后果。
ResponseFuture 是两个状态的枚举:
enum ResponseState<F> {
Called {
#[pin]
fut: F
},
Overloaded,
}
Called 转发内层 future 并把错误装箱;Overloaded 第一次 poll 就返回 Err(Overloaded::new().into())。Overloaded 的 Display 是 service overloaded,调用方用 downcast_ref::<Overloaded>() 和别的错误区分,比如在 HTTP 服务里转成 503。
Clone 是手写的,副本的 is_ready 是 false。源码注释的理由是「a cloneable inner service likely tracks readiness per clone」:内层的就绪状态多半是跟着每个副本走的(比如 ConcurrencyLimit 的许可),副本不能继承一个它没有的就绪。
顺序:LoadShed 要在外面
LoadShed 只能看见它里面的 Pending。最常见的搭配是 ConcurrencyLimit:并发满了就拒绝,而不是排队。两种摆法,四个任务同时发请求,内层每个请求耗时 100ms,并发上限 2:
tower = { version = "=0.5.3", features = ["load-shed", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["rt", "macros", "time", "test-util"] }
use std::time::Duration;
use tokio::time::{sleep, Instant};
use tower::{BoxError, Service, ServiceBuilder, ServiceExt};
async fn work(id: u32) -> Result<u32, BoxError> {
sleep(Duration::from_millis(100)).await;
Ok(id)
}
async fn run<S>(name: &str, svc: S)
where
S: Service<u32, Response = u32, Error = BoxError> + Clone + Send + 'static,
S::Future: Send,
{
let start = Instant::now();
let mut tasks = Vec::new();
for id in 1..=4 {
let mut svc = svc.clone();
tasks.push(tokio::spawn(async move {
let r = svc.ready().await?.call(id).await;
Ok::<_, BoxError>((id, r, start.elapsed().as_millis()))
}));
}
println!("{name}");
for t in tasks {
let (id, r, ms) = t.await.unwrap().unwrap();
match r {
Ok(v) => println!(" req {id}: ok {v} at {ms}ms"),
Err(e) => println!(" req {id}: err \"{e}\" at {ms}ms"),
}
}
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let a = ServiceBuilder::new()
.load_shed()
.concurrency_limit(2)
.service_fn(work);
run("A: LoadShed > ConcurrencyLimit(2)", a).await;
let b = ServiceBuilder::new()
.concurrency_limit(2)
.load_shed()
.service_fn(work);
run("B: ConcurrencyLimit(2) > LoadShed", b).await;
}
M2 Max、rustc 1.99.0、--release,时钟暂停,连跑 3 次输出相同:
A: LoadShed > ConcurrencyLimit(2)
req 1: ok 1 at 100ms
req 2: ok 2 at 100ms
req 3: err "service overloaded" at 0ms
req 4: err "service overloaded" at 0ms
B: ConcurrencyLimit(2) > LoadShed
req 1: ok 1 at 100ms
req 2: ok 2 at 100ms
req 3: ok 3 at 200ms
req 4: ok 4 at 200ms
- A:
LoadShed在外。请求 3、4 的ConcurrencyLimit取不到许可,返回Pending,LoadShed记下来、照样说Ready,call时立即失败,0ms 就拿到service overloaded。 - B:
ConcurrencyLimit在外。等许可发生在它自己的poll_ready里,调用方在ready().await上排队;轮到LoadShed被问的时候,里面的service_fn永远就绪,它没有东西可拒绝。请求 3、4 排到 100ms 才开始,200ms 完成。LoadShed等于不存在。
所以记住一句话:LoadShed 把它里面的等待变成拒绝,管不到它外面的等待。 在 ServiceBuilder 里,先写的在外层,.load_shed() 要写在 .concurrency_limit() 前面(见 ServiceBuilder)。
反过来也要注意:LoadShed 不知道 Pending 是谁给的。它里面任何一层返回 Pending(比如 RateLimit 在等窗口、Buffer 的通道满了),请求都会被拒绝。
LoadShed 在 2019 年 3 月以独立的 tower-load-shed crate 加入(PR #204,随 tower-load-shed 0.1.0 发布,tower 0.1.0 重新导出了它)。PR 描述只有一段:「Provides a middleware that immediately fails any request if the underlying service isn't ready yet. This is useful when used in conjunction with InFlightLimit or others, so that requests are rejected immediately, instead of keeping track of extra pending requests.」InFlightLimit 就是后来改名的 ConcurrencyLimit。第一版用的还是 futures 0.1 的 Poll<(), Error>,但 is_ready 字段、call 里「用掉就置回 false」、Clone 时 is_ready: false,这几处和本页的 0.5.3 代码是同一个写法。
被拒绝之后,内层还在排队
A 里被拒绝的任务随后就结束了,它手里那份服务被 drop,看不出问题。但如果同一个服务实例活得比请求长(比如一个连接上复用一份服务),被拒绝的那次 poll_ready 留下的东西就要算一算了。
ConcurrencyLimit 取不到许可时,PollSemaphore 里那个取许可的 future 已经排进了信号量的等待队列;LoadShed 不等它,但也没有把它撤下来。
tower = { version = "=0.5.3", features = ["load-shed", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["rt", "macros", "sync"] }
use std::{convert::Infallible, future::poll_fn, sync::Arc, task::Poll};
use tokio::sync::Semaphore;
use tower::{
limit::GlobalConcurrencyLimitLayer, load_shed::LoadShedLayer, service_fn, Service,
ServiceBuilder,
};
async fn ready_now<S: Service<u32>>(svc: &mut S) -> bool {
poll_fn(|cx| Poll::Ready(svc.poll_ready(cx).is_ready())).await
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let sem = Arc::new(Semaphore::new(1));
let svc = ServiceBuilder::new()
.layer(LoadShedLayer::new())
.layer(GlobalConcurrencyLimitLayer::with_semaphore(sem.clone()))
.service(service_fn(|x: u32| async move { Ok::<_, Infallible>(x) }));
let (mut a, mut b, mut c) = (svc.clone(), svc.clone(), svc.clone());
ready_now(&mut a).await;
let fut_a = a.call(1); // a holds the only permit
ready_now(&mut b).await; // LoadShed says Ready, inner was Pending
println!("b.call -> {:?}", b.call(2).await.map_err(|e| e.to_string()));
drop(fut_a); // permit released
println!("after fut_a dropped: available = {}", sem.available_permits());
ready_now(&mut c).await;
println!("c.call -> {:?}", c.call(3).await.map_err(|e| e.to_string()));
ready_now(&mut b).await;
println!("b.call -> {:?}", b.call(4).await.map_err(|e| e.to_string()));
}
fut_a 被 drop 以后,c 能拿到许可吗?并发上限 1。a 占着唯一的许可,b 被拒绝了一次,但 b 这个实例还活着。然后 fut_a 被 drop,许可归还,此时没有任何请求在处理。第二行打印的 available 是几?c.call 成功还是 service overloaded?最后 b.call 呢?答案在文末。
动脑筋答案
M2 Max、rustc 1.99.0、--release,连跑 3 次,输出相同:
b.call -> Err("service overloaded")
after fut_a dropped: available = 0
c.call -> Err("service overloaded")
b.call -> Ok(4)
- 第一行:
b的ConcurrencyLimit取不到许可,LoadShed记下is_ready = false,call返回Overloaded。但b取许可的请求留在了信号量的等待队列里。 available = 0:fut_a归还许可时,信号量发现队列里有b,直接把许可交给了b的等待节点(见 batch_semaphore),计数没有加回来。这时没有任何请求在跑,许可却被一个已经被拒绝过的实例占着。c.call被拒绝:c取不到许可,于是c也被 shed 了,而系统其实是空闲的。b.call成功:b再次poll_ready,PollSemaphore发现许可早已到手,b就绪,请求通过。
被拒绝的实例只要不被 drop 也不再 poll_ready,就会一直占着那个许可。每个请求用一份新 clone、用完就丢,或者被拒绝后照常继续 poll_ready,都能避开这种情况。
小结
LoadShed只有一个is_ready:poll_ready问内层并记下结果,自己总是返回Ready(Ok)(内层报错除外)。call时内层就绪就调用并把is_ready置回false,否则返回一个立即失败的Overloaded(service overloaded)。不poll_ready就call不 panic,而是得到Overloaded。- 它只能把里面的
Pending变成拒绝:要配合ConcurrencyLimit,LoadShed必须在外层;里面任何一层的Pending都会导致拒绝。 - 内层被问过的副作用还在:
ConcurrencyLimit的取许可请求留在信号量队列里,被拒绝但仍存活的实例会截走归还的许可。