service overloaded
主线位置:就绪主线的终点和请求主线的起点。
LoadShed把内层在就绪主线上的Pending记下来,到请求主线上换成一个立即失败的 future,错误就是这一行字。
flowchart TB
got["收到 service overloaded"] --> q1{call 前 ready 了吗}
q1 -->|没有| a1["is_ready 还是 false"]
q1 -->|有| q2{里面谁 Pending}
q2 -->|ConcurrencyLimit| a2[真过载,预期行为]
q2 -->|"RateLimit 等"| a3[限流也被当成过载]
q2 -->|系统空闲| a4[被拒实例攥着许可]
这行字从哪来
Overloaded 的 Display 写的就是 service overloaded。整个 tower 里只有 LoadShed 产生它:call 发现 is_ready 是 false,就返回 ResponseFuture::overloaded(),这个 future 第一次被 poll 就给出 Err(Overloaded::new().into())。is_ready 是上一次 poll_ready 时内层有没有就绪,机制见 LoadShed。
所以看到这行字,要问的只有一件事:call 之前,LoadShed 里面那一摞服务为什么没就绪?
复现:四种情况
tower = { version = "=0.5.3", features = ["load-shed", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["full", "test-util"] }
use std::time::Duration;
use tokio::time::Instant;
use tower::load_shed::error::Overloaded;
use tower::{BoxError, Service, ServiceBuilder, ServiceExt};
async fn echo(x: u32) -> Result<u32, BoxError> {
Ok(x)
}
fn show(start: Instant, what: &str, r: Result<u32, BoxError>) {
let ms = start.elapsed().as_millis();
match r {
Ok(v) => println!(" {what}: ok {v} at {ms}ms"),
Err(e) => println!(" {what}: err \"{e}\" overloaded={} at {ms}ms", e.is::<Overloaded>()),
}
}
// What an HTTP layer would do with the error.
fn status(e: &BoxError) -> u16 {
if e.is::<Overloaded>() { 503 } else { 500 }
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let start = Instant::now();
println!("1. call without ready()");
let mut svc = ServiceBuilder::new().load_shed().concurrency_limit(10).service_fn(echo);
show(start, "call(1)", svc.call(1).await);
show(start, "ready + call(2)", svc.ready().await.unwrap().call(2).await);
println!("2. LoadShed > RateLimit(1 per 100ms), one request at a time");
let mut svc = ServiceBuilder::new()
.load_shed()
.rate_limit(1, Duration::from_millis(100))
.service_fn(echo);
for i in 1..=3 {
show(start, &format!("req {i}"), svc.ready().await.unwrap().call(i).await);
}
println!("3. RateLimit > LoadShed > ConcurrencyLimit");
let mut svc = ServiceBuilder::new()
.rate_limit(1, Duration::from_millis(100))
.load_shed()
.concurrency_limit(10)
.service_fn(echo);
for i in 1..=3 {
show(start, &format!("req {i}"), svc.ready().await.unwrap().call(i).await);
}
println!("4. mapping to a status code");
let shed: BoxError = Overloaded::new().into();
let other: BoxError = "boom".into();
println!(" Overloaded::new() -> {}, \"boom\" -> {}", status(&shed), status(&other));
}
M2 Max、rustc 1.99.0、--release,时钟暂停,连跑 3 次输出相同:
1. call without ready()
call(1): err "service overloaded" overloaded=true at 0ms
ready + call(2): ok 2 at 0ms
2. LoadShed > RateLimit(1 per 100ms), one request at a time
req 1: ok 1 at 0ms
req 2: err "service overloaded" overloaded=true at 0ms
req 3: err "service overloaded" overloaded=true at 0ms
3. RateLimit > LoadShed > ConcurrencyLimit
req 1: ok 1 at 0ms
req 2: ok 2 at 100ms
req 3: ok 3 at 200ms
4. mapping to a status code
Overloaded::new() -> 503, "boom" -> 500
第 1、2 组里没有任何并发,请求一个接一个地发,照样收到 service overloaded。
逐条看
没 ready 就 call
第 1 组的并发上限是 10,一个请求都没在跑。is_ready 初始是 false,没有 poll_ready 就 call,LoadShed 直接返回 Overloaded。换成没有 LoadShed 的 ConcurrencyLimit,这样做会 panic(见 poll_ready must be called first),套了 LoadShed 就变成一个看起来像过载的错误,更难往「忘了 ready」上想。
clone 一份服务再 call 是同一回事:LoadShed 的 Clone 手写成副本 is_ready: false。修法是 svc.ready().await?.call(req),第 1 组第二行就是。
里面有别的 Pending 来源
LoadShed 不知道 Pending 是哪一层给的。第 2 组里面是 RateLimit,一个窗口 100ms 只放 1 个。第 1 个请求用掉额度以后,RateLimit::poll_ready 处于 State::Limited,sleep 没到期就返回 Pending:
State::Limited => {
if Pin::new(&mut self.sleep).poll(cx).is_pending() {
tracing::trace!("rate limit exceeded; sleeping.");
return Poll::Pending;
}
}
LoadShed 把它记成没就绪,后面的请求全部被拒,直到窗口结束。Buffer 通道满了、Reconnect 还没连上,也都会这样变成 service overloaded。
如果本意是「并发满了就拒绝,限流就排队」,把 RateLimit 挪到 LoadShed 外面,让 LoadShed 里只剩 ConcurrencyLimit。第 3 组就是这么摆的:限流的等待发生在 LoadShed 之外,请求 2、3 分别排到 100ms、200ms,没有一个被拒。
空闲时也报
系统里没有在途请求,service overloaded 还在出。最可能的原因是一个被拒绝过、但还活着的服务实例:它的 ConcurrencyLimit 排在信号量队列里,许可归还时被它截走,别的实例取不到许可,也被拒绝。复现和逐行解释见 LoadShed 一章的动脑筋。修法:每个请求用一份新 clone、用完就丢,或者被拒绝后照常继续 poll_ready。
反过来:从来不报
套了 LoadShed,压满了也没见过这个错误,而是请求越来越慢:LoadShed 写在了 ConcurrencyLimit 里面,等许可发生在它外面,它管不到。ServiceBuilder 里 .load_shed() 要写在 .concurrency_limit() 前面,实验见 LoadShed;axum Router::layer 的顺序是反的,见 层的顺序反了。
和别的错误分开
LoadShed 的错误类型是 BoxError,Overloaded 被装在里面。用 e.is::<Overloaded>() 或 downcast_ref::<Overloaded>() 把它挑出来,映射成 503,其余的另行处理。第 4 组的 status 函数就是这么做的。放进 axum 时要先做这一步:Router::layer 要求服务的错误类型满足 Into<Infallible>,BoxError 不满足,得先把错误变成响应。
Overloaded::new 原来不是公开的Overloaded 的构造函数原来是 pub(crate) fn new(),tower 外面造不出这个错误。#661 把它改成 pub,并加了 #[derive(Default)],理由是「This allows for mocking. This also matches what is done for the timeout Elapsed error.」CHANGELOG 把它列在 tower 0.4.13 的 Added 下:「load_shed: Public constructor for Overloaded error」。
第 4 组的 Overloaded::new().into() 用的就是它:不用真的把服务压满,就能测「过载 → 503」这条映射。
小结
service overloaded只来自LoadShed:call时is_ready是false,也就是上一次poll_ready时里面那一摞没就绪,或者根本没poll_ready。- 没
ready()就call(包括 clone 之后直接call)得到的是Overloaded,不是 panic。 LoadShed里面任何一层的Pending都算过载:RateLimit窗口用完、Buffer通道满了都会被拒。只想对并发满拒绝,就让LoadShed里只剩ConcurrencyLimit。- 系统空闲也报,查有没有被拒绝过、还活着的实例攥着许可;从来不报,查
LoadShed是不是在ConcurrencyLimit里面。 - 用
is::<Overloaded>()把它和别的错误分开,映射成 503;Overloaded::new()从 0.4.13 起公开,测试里能直接造。