Rust in Everything
基于 tower 0.5.3

Clone is not satisfied:RateLimit 放不进 axum 和 Retry

主线位置:两条主线开始之前的装配阶段。编译器在这里拦下的,是运行时会被 clone 的那个服务:axum 每个请求 clone 一份路由服务,Retry 每个请求把内层 clone 进响应 future。

flowchart TB
  need["要求内层 Clone"] --> ax["axum Router::layer"]
  need --> rt["Retry"]
  lack["没有 Clone"] --> rl[RateLimit]
  lack --> sr[SpawnReady]
  lack --> rc[Reconnect]
  rl --> buf["外面套 Buffer"]
  buf --> ok["Buffer 总能 Clone"]
  ok --> scope["共享的范围:建了几次 Buffer"]

tower 里没有 Clone 的中间件有三个:RateLimit、SpawnReady、Reconnect(见 单服务中间件 的对照表)。最常被放进需要 Clone 的地方的是 RateLimit。它为什么不能 Clone,见 RateLimit:状态全在实例身上,复制出来的每一份各数各的。

报错:axum

tower = { version = "=0.5.3", features = ["limit"] }
tokio = { version = "=1.54.1", features = ["full"] }
axum = "=0.8.9"
use axum::{Router, routing::get};
use std::time::Duration;
use tower::limit::RateLimitLayer;

#[tokio::main]
async fn main() {
  let app: Router = Router::new()
    .route("/", get(|| async { "hi" }))
    .layer(RateLimitLayer::new(5, Duration::from_secs(1)));
  let _ = app;
}

rustc 1.99.0:

error[E0277]: the trait bound `RateLimit<Route>: Clone` is not satisfied
   --> src/bin/clone_axum_bad.rs:9:12
    |
  9 |     .layer(RateLimitLayer::new(5, Duration::from_secs(1)));
    |      ----- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `RateLimit<Route>`
    |      |
    |      required by a bound introduced by this call
    |
note: required by a bound in `Router::<S>::layer`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.8.9/src/routing/mod.rs:306:40
    |
303 |     pub fn layer<L>(self, layer: L) -> Router<S>
    |            ----- required by a bound in this associated function
...
306 |         L::Service: Service<Request> + Clone + Send + Sync + 'static,
    |                                        ^^^^^ required by this bound in `Router::<S>::layer`

两边的源码:

  • RateLimit 只 derive 了 Debug。
  • axum 的 Router::layer 要求 L::Service: ... + Clone。原因在 Route::oneshot_inner:每个请求都是 self.0.clone().oneshot(req):先 clone 一份服务,在这份副本上 poll_ready 再 call,用完就丢。原件从来不 poll_ready,不占任何资源,所以这里的副本自己 ready 不会出 poll_ready must be called first 里那种卡死。

修法:套 Buffer,再处理错误

Buffer 总能 Clone(副本只是多一个通道发送端,见 Buffer),所有副本把请求送进同一个 worker,worker 里只有一个 RateLimit。只加 Buffer 还不够:

  let app: Router = Router::new()
    .route("/", get(|| async { "hi" }))
    .layer(
      ServiceBuilder::new()
        .buffer(64)
        .rate_limit(5, Duration::from_secs(1)),
    );
error[E0277]: the trait bound `Infallible: From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not satisfied
   --> src/bin/clone_axum_buffer.rs:10:7
    |
  9 |       .layer(
    |        ----- required by a bound introduced by this call
 10 | /       ServiceBuilder::new()
 11 | |         .buffer(64)
 12 | |         .rate_limit(5, Duration::from_secs(1)),
    | |______________________________________________^ the trait `From<Box<(dyn std::error::Error + Send + Sync + 'static)>>` is not implemented for `Infallible`
    |
    = note: required for `Box<(dyn std::error::Error + Send + Sync + 'static)>` to implement `Into<Infallible>`
note: required by a bound in `Router::<S>::layer`
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/axum-0.8.9/src/routing/mod.rs:308:50
    |
308 |         <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
    |                                                  ^^^^^^^^^^^^^^^^ required by this bound in `Router::<S>::layer`

(中间几行 help 和 ... 省略。)Buffer 的 Error 是 BoxError(worker 可能没了,见 buffer's worker closed unexpectedly),axum 要的是 Infallible。在最外面加 axum 的 HandleErrorLayer,把错误变成一个响应:

tower = { version = "=0.5.3", features = ["buffer", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["full", "test-util"] }
axum = "=0.8.9"
use axum::{
  Router,
  body::Body,
  error_handling::HandleErrorLayer,
  http::{Request, StatusCode},
  routing::get,
};
use std::time::Duration;
use tokio::time::Instant;
use tower::{BoxError, ServiceBuilder, ServiceExt};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let app: Router = Router::new()
    .route("/a", get(|| async { "a" }))
    .route("/b", get(|| async { "b" }))
    .layer(
      ServiceBuilder::new()
        .layer(HandleErrorLayer::new(|e: BoxError| async move {
          (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
        }))
        .buffer(64)
        .rate_limit(2, Duration::from_secs(1)),
    );

  let start = Instant::now();
  for uri in ["/a", "/a", "/a", "/b", "/b"] {
    let req = Request::builder().uri(uri).body(Body::empty()).unwrap();
    let rsp = app.clone().oneshot(req).await.unwrap();
    println!("{uri}: {} at {}ms", rsp.status(), start.elapsed().as_millis());
  }
}

能编译,也能跑。每秒 2 个,第三个 /a 应该等到 1000ms。M2 Max、rustc 1.99.0、--release,连跑 3 次输出相同:

/a: 200 OK at 0ms
/a: 200 OK at 0ms
/a: 200 OK at 0ms
/b: 200 OK at 0ms
/b: 200 OK at 0ms

一个都没被限住。

修好以后:限流的范围变了

RateLimit 计数的范围就是 Buffer 的范围:同一个 Buffer 的副本共享一个 worker,各自 Buffer::new 出来的互不相干。所以要问的是,axum 在什么时候调用 BufferLayer::layer。

MethodEndpoint::map 显示,.route("/a", get(handler)) 存下的是一个还没变成服务的 BoxedHandler,layer 只是被记下来(handler.map(f)),要等这个 handler 变成 Route 时才套上。变成 Route 发生在两个地方:

场景源码套 layer 的次数
直接把 Router<()> 当 Service 调(上面的程序、测试里常这么写)call_with_state 里 handler.clone().into_route(state)每个请求一次
axum::serve(listener, app)Service<IncomingStream> 的 call 里 self.clone().with_state(())每个连接一次
自己先调一次 app.with_state(())MethodEndpoint::with_state 把 BoxedHandler 换成 Route每条路由一次

每套一次 layer,就是一次 Buffer::new:一个新 worker,一个新 RateLimit,从满额开始数。上面的程序每个请求都是新的限流器,所以全部 0ms。在 let start 前面加一行 let app: Router = app.with_state(());,其余不变,连跑 3 次输出相同:

/a: 200 OK at 0ms
/a: 200 OK at 0ms
/a: 200 OK at 1000ms
/b: 200 OK at 1000ms
/b: 200 OK at 1000ms

/a 第三次被限住了;/b 跟着到 1000ms,是因为它在时间线上排在 /a 后面,它自己的限流器每秒 2 个,两次都没被限。

真正上线时用的是 axum::serve,范围是每个连接。用真实的 TCP 连接验证:

tower = { version = "=0.5.3", features = ["buffer", "limit"] }
tokio = { version = "=1.54.1", features = ["full"] }
axum = "=0.8.9"
use axum::{Router, error_handling::HandleErrorLayer, http::StatusCode, routing::get};
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tower::{BoxError, ServiceBuilder};

// Send one GET on an open connection and return the status line.
async fn http_get(conn: &mut TcpStream, path: &str) -> String {
  let req = format!("GET {path} HTTP/1.1\r\nHost: x\r\n\r\n");
  conn.write_all(req.as_bytes()).await.unwrap();
  let mut buf = Vec::new();
  let mut byte = [0u8; 1];
  while !buf.ends_with(b"\r\n\r\n") {
    conn.read_exact(&mut byte).await.unwrap();
    buf.push(byte[0]);
  }
  let head = String::from_utf8(buf).unwrap();
  let len: usize = head
    .lines()
    .find_map(|l| l.strip_prefix("content-length: "))
    .unwrap()
    .parse()
    .unwrap();
  let mut body = vec![0u8; len];
  conn.read_exact(&mut body).await.unwrap();
  head.lines().next().unwrap().to_string()
}

#[tokio::main]
async fn main() {
  let app: Router = Router::new().route("/a", get(|| async { "a" })).layer(
    ServiceBuilder::new()
      .layer(HandleErrorLayer::new(|e: BoxError| async move {
        (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
      }))
      .buffer(64)
      .rate_limit(2, Duration::from_secs(1)),
  );
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });

  let start = Instant::now();
  println!("one connection per request:");
  for _ in 0..3 {
    let mut conn = TcpStream::connect(addr).await.unwrap();
    let status = http_get(&mut conn, "/a").await;
    println!("  {status} at {}ms", start.elapsed().as_millis());
  }

  let start = Instant::now();
  println!("three requests on one connection:");
  let mut conn = TcpStream::connect(addr).await.unwrap();
  for _ in 0..3 {
    let status = http_get(&mut conn, "/a").await;
    println!("  {status} at {}ms", start.elapsed().as_millis());
  }
}

这次用的是真实时钟。连跑 3 次,毫秒数在 0–2ms 之内浮动,结论相同,其中一次:

one connection per request:
  HTTP/1.1 200 OK at 0ms
  HTTP/1.1 200 OK at 0ms
  HTTP/1.1 200 OK at 1ms
three requests on one connection:
  HTTP/1.1 200 OK at 0ms
  HTTP/1.1 200 OK at 0ms
  HTTP/1.1 200 OK at 1001ms

同一个连接上的第三个请求等了 1 秒;换一个连接就是一个新的限流器。客户端每个请求开一个新连接,限流就不存在。axum 在 with_state(()) 那一行的注释里说了这么做的目的:「call Router::with_state such that everything is turned into Route eagerly rather than doing that per request」,它要省的是每个请求一次转换,代价是每个连接一份 layer 状态。

要全局一份,就在 Router 外面建一次,交给 axum::serve 时用 tower 的 Shared(每个连接都拿到同一个服务的 clone,需要 make feature):

tower = { version = "=0.5.3", features = ["buffer", "limit", "make"] }
use tower::{BoxError, ServiceBuilder, make::Shared};

// http_get 同上

#[tokio::main]
async fn main() {
  let app: Router = Router::new()
    .route("/a", get(|| async { "a" }))
    .route("/b", get(|| async { "b" }));
  // Build the limited service once, around the whole router.
  let svc = ServiceBuilder::new()
    .layer(HandleErrorLayer::new(|e: BoxError| async move {
      (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
    }))
    .buffer(64)
    .rate_limit(2, Duration::from_secs(1))
    .service(app);
  let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
  let addr = listener.local_addr().unwrap();
  tokio::spawn(async move { axum::serve(listener, Shared::new(svc)).await.unwrap() });

  let start = Instant::now();
  for path in ["/a", "/b", "/a"] {
    let mut conn = TcpStream::connect(addr).await.unwrap();
    let status = http_get(&mut conn, path).await;
    println!("{path} on a new connection: {status} at {}ms", start.elapsed().as_millis());
  }
}

连跑 3 次,毫秒数浮动 0–3ms,其中一次:

/a on a new connection: HTTP/1.1 200 OK at 0ms
/b on a new connection: HTTP/1.1 200 OK at 0ms
/a on a new connection: HTTP/1.1 200 OK at 1003ms

三个请求、三个连接、两条路由,共用一个窗口。这仍然是整个服务一个计数,不是按客户端限流。按 IP 分别限流 tower 没有现成的中间件,本站的做法见 在本站中:中间件栈。

报错:Retry

Retry 的 Service 实现要求 S: Service<Request> + Clone,因为 call 要把 self.clone() 放进响应 future,失败时用它重新 poll_ready 再发(见 重试)。把 RateLimit 放在 Retry 里面:

tower = { version = "=0.5.3", features = ["retry", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["full"] }
use std::future::{Ready, ready};
use std::time::Duration;
use tower::retry::Policy;
use tower::{BoxError, ServiceBuilder, ServiceExt};

// Retry every error up to `left` more times.
#[derive(Clone)]
struct Attempts(usize);

impl<Req: Clone, Res, E> Policy<Req, Res, E> for Attempts {
  type Future = Ready<()>;

  fn retry(&mut self, _: &mut Req, result: &mut Result<Res, E>) -> Option<Self::Future> {
    if result.is_err() && self.0 > 0 {
      self.0 -= 1;
      Some(ready(()))
    } else {
      None
    }
  }

  fn clone_request(&mut self, req: &Req) -> Option<Req> {
    Some(req.clone())
  }
}

#[tokio::main]
async fn main() {
  let svc = ServiceBuilder::new()
    .retry(Attempts(2))
    .rate_limit(10, Duration::from_secs(1))
    .service_fn(|x: u32| async move { Ok::<_, BoxError>(x) });
  let rsp = svc.oneshot(1).await.unwrap();
  println!("{rsp}");
}
error[E0599]: the method `oneshot` exists for struct `Retry<Attempts, RateLimit<ServiceFn<{closure@src/bin/clone_retry_bad.rs:32:17: 32:25}>>>`, but its trait bounds were not satisfied
   --> src/bin/clone_retry_bad.rs:33:17
    |
 33 |     let rsp = svc.oneshot(1).await.unwrap();
    |                   ^^^^^^^ method cannot be called due to unsatisfied trait bounds
    |
    = note: the following trait bounds were not satisfied:
            `Retry<Attempts, RateLimit<ServiceFn<{closure@src/bin/clone_retry_bad.rs:32:17: 32:25}>>>: Service<_>`
            which is required by `Retry<Attempts, RateLimit<ServiceFn<{closure@src/bin/clone_retry_bad.rs:32:17: 32:25}>>>: ServiceExt<_>`

(指向 pin_project! 宏内部的几行和 &Retry、&mut Retry 两条 note 省略。)这条错误里根本没有 Clone。oneshot 是 ServiceExt 的方法,编译器只告诉你「Retry<...> 不是 Service」,不往下说为什么。让它说出来的办法是直接问「这是不是 Service」:

// Asks the compiler the exact question: is this a Service?
fn assert_service<S: Service<u32>>(_: &S) {}

// in main, before oneshot:
  assert_service(&svc);
error[E0277]: the trait bound `RateLimit<ServiceFn<{closure@src/bin/clone_retry_assert.rs:35:17: 35:25}>>: Clone` is not satisfied
  --> src/bin/clone_retry_assert.rs:36:18
   |
36 |   assert_service(&svc);
   |   -------------- ^^^^ the trait `Clone` is not implemented for `RateLimit<ServiceFn<{closure@src/bin/clone_retry_assert.rs:35:17: 35:25}>>`
   |   |
   |   required by a bound introduced by this call
   |
help: the trait `Service<Request>` is conditionally implemented for `Retry<P, S>`
  --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-0.5.3/src/retry/mod.rs:72:1
   |
72 | / impl<P, S, Request> Service<Request> for Retry<P, S>
73 | | where
74 | |     P: Policy<Request, S::Response, S::Error> + Clone,
75 | |     S: Service<Request> + Clone,
   | |___________________________-----^
   |                             |
   |                             unsatisfied requirement introduced here: `RateLimit<ServiceFn<{closure@src/bin/clone_retry_assert.rs:35:17: 35:25}>>: Clone`
   = note: required for `Retry<Attempts, RateLimit<ServiceFn<{closure@src/bin/clone_retry_assert.rs:35:17: 35:25}>>>` to implement `Service<u32>`

ServiceBuilder 自带的 check_service 系列做的是同一件事,见 ServiceBuilder。

Retry 的文档(第 22–28 行)直接给了修法:「If the inner Service type does not implement Clone, the [Buffer] middleware can be added to make any Service cloneable.」中间插一个 buffer。下面的内层前两次失败、第三次成功,每秒限 2 次:

tower = { version = "=0.5.3", features = ["retry", "buffer", "limit", "util"] }
tokio = { version = "=1.54.1", features = ["full", "test-util"] }
// Attempts 同上;另外 use std::sync::Arc、std::sync::atomic::{AtomicU32, Ordering}、tokio::time::Instant

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
  let start = Instant::now();
  let tries = Arc::new(AtomicU32::new(0));
  let svc = ServiceBuilder::new()
    .retry(Attempts(2))
    .buffer(8)
    .rate_limit(2, Duration::from_secs(1))
    .service_fn(move |x: u32| {
      let n = tries.fetch_add(1, Ordering::SeqCst) + 1;
      async move {
        println!("attempt {n} at {}ms", start.elapsed().as_millis());
        if n < 3 { Err::<u32, BoxError>("flaky".into()) } else { Ok(x) }
      }
    });
  let rsp = svc.oneshot(7).await.unwrap();
  println!("response {rsp} at {}ms", start.elapsed().as_millis());
}

连跑 3 次输出相同:

attempt 1 at 0ms
attempt 2 at 0ms
attempt 3 at 1000ms
response 7 at 1000ms

重试也经过同一个 RateLimit:第三次尝试等到了下一个窗口。这里一个 Retry 只有一个 Buffer,不存在 axum 那样「建了几次」的问题。

📝NOTE
没有蠢问题

问: 把顺序反过来,RateLimit 在外、Retry 在里,Retry 的内层就是能 Clone 的 service_fn,不用 Buffer 也能编译。为什么不这么写?

答: 能编译,但重试绕过了限流。RateLimit 只在 call 时扣次数,一个请求进来扣一次,之后 Retry 在它里面重发几次它都看不见。把上面程序的两行换成 .rate_limit(2, Duration::from_secs(1)) 在前、.retry(Attempts(2)) 在后(去掉 buffer),连跑 3 次都是:

attempt 1 at 0ms
attempt 2 at 0ms
attempt 3 at 0ms
response 7 at 0ms

三次尝试都在 0ms。想让每次重试都算进限流,RateLimit 只能在 Retry 里面,也就只能加 Buffer。

为什么不给 RateLimit 加上 Clone

一个直接的想法是让 RateLimit 的副本共享计数,比如把状态放进 Arc<Mutex<_>>。

📝NOTE
冷知识:给 RateLimit 加 Clone 的 PR 被作者自己撤回了

issue #794(2024 年 9 月,至今未关闭)指出 ConcurrencyLimit 实现了 Clone 而 RateLimit 没有。PR #833「Make RateLimit Clone (#794)」的做法是「wrap an inner implementation in a Mutex allowing us to clone the type as much as we want without allowing callers to circumvent rate-limiting」。

评审时有人提出不该让所有用户为一把锁付费。之后作者自己发现了一个更要命的问题:「my newly Clonable RateLimit instances will share a reference to one instance of RateLimitInner, which when polled will park the caller's Waker with it's Sleep future ... So if more than one RateLimit instance calls ready() when rate-limited, the last one "wins", overwriting the other's Waker, leaving the task stranded.」这就是 RateLimit 的结构:它只有一个 sleep(第 17 行),一个 Sleep 只记一个 waker。多个副本同时在 Limited 状态下等,只有最后一个会被叫醒。

PR 于 2026 年 1 月关闭,没有合并,作者改为另写一个基于 GCRA 算法的 crate。所以 0.5.3 里 RateLimit 仍然没有 Clone,共享计数仍然要靠 Buffer:Buffer 让只有一个 worker 去 poll 那个 Sleep。

小结

  • axum 的 Router::layer 和 tower 的 Retry 都要求服务 Clone;RateLimit、SpawnReady、Reconnect 没有。
  • 套一层 Buffer 就能 Clone;在 axum 里还要加 HandleErrorLayer,把 Buffer 的 BoxError 变成响应。
  • 共享的范围等于 Buffer::new 的次数。axum 在 handler 变成 Route 时才套 layer:直接把 Router 当服务调是每个请求一份,axum::serve 是每个连接一份。要全局一份,在 Router 外面建一次,用 Shared 交给 axum::serve。
  • Retry<RateLimit<_>> 的报错里没有 Clone 字样;用一个 S: Service<Req> 约束的函数(或 check_service)问一次,编译器才会指出缺的是 Clone。修法是中间插 buffer;反过来把 RateLimit 放外面,重试会绕过限流。

💬 关联讨论

发起讨论
加载中...