Rust in Everything
基于 tower 0.5.3

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 不满足,得先把错误变成响应。

📝NOTE
冷知识: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 起公开,测试里能直接造。

💬 关联讨论

发起讨论
加载中...