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 那样「建了几次」的问题。
问: 把顺序反过来,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<_>>。
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放外面,重试会绕过限流。