InFlightRequests:在途计数
主线位置:结局主线的第三问「什么算放弃」的最简单形式:不区分正常结束和放弃,只用
Drop回答「这个请求还在不在」。作用点上三个时刻都有:call时加一,响应 future 完成时把守卫移进 body,body 被 drop 时减一。
sequenceDiagram participant C as 调用方 participant I as InFlightRequests participant N as 计数器 C->>I: call(req) I->>N: fetch_add 1 造守卫 Note over I: 守卫在 ResponseFuture 里 I-->>C: Response 守卫移进 ResponseBody C->>C: 读 body C->>I: drop(body) I->>N: 守卫 drop fetch_sub 1
一个原子数加一个守卫
metrics/in_flight_requests.rs 的核心只有二十行:
pub struct InFlightRequestsCounter {
count: Arc<AtomicUsize>,
}
impl InFlightRequestsCounter {
pub fn get(&self) -> usize {
self.count.load(Ordering::Relaxed)
}
fn increment(&self) -> IncrementGuard {
self.count.fetch_add(1, Ordering::Relaxed);
IncrementGuard {
count: self.count.clone(),
}
}
// ...
}
struct IncrementGuard {
count: Arc<AtomicUsize>,
}
impl Drop for IncrementGuard {
fn drop(&mut self) {
self.count.fetch_sub(1, Ordering::Relaxed);
}
}
计数是 Relaxed 的:它只是一个给监控看的数,不用来同步任何别的内存。InFlightRequestsLayer::pair() 返回 layer 和一个计数器的副本,两者共享同一个 Arc。
守卫怎么走,就是四件套那一页开头画的图:call 加一并把守卫放进 ResponseFuture;poll 等内层出响应后,把守卫从 future 里 take() 出来,放进 ResponseBody:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let response = ready!(this.inner.poll(cx))?;
let guard = this.guard.take().unwrap();
let response = response.map(move |body| ResponseBody { inner: body, guard });
Poll::Ready(Ok(response))
}
注意 ?:内层返回 Err 时直接返回,守卫留在 future 里,future 被 drop 时才减一。三种结局都会归零:body 被 drop、future 出错后被 drop、future 没完成就被 drop(调用方超时、连接断开)。它分不清这三种,OnEarlyDrop 才分得清。
实测:什么时候减一
tower-http = { version = "=0.7.1", features = ["metrics"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt", "time", "test-util"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
futures = "0.3"
use bytes::Bytes;
use http::{Request, Response};
use http_body::Frame;
use http_body_util::{combinators::UnsyncBoxBody, BodyExt, Full, StreamBody};
use std::{convert::Infallible, time::Duration};
use tokio::time::{sleep, Instant};
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::metrics::InFlightRequestsLayer;
type Body = UnsyncBoxBody<Bytes, BoxError>;
async fn handler(req: Request<()>) -> Result<Response<Body>, Infallible> {
sleep(Duration::from_millis(100)).await;
let body = if req.uri().path() == "/sse" {
// 像 SSE 一样:发一帧,然后一直不结束
let first = futures::stream::iter([Ok::<_, BoxError>(Frame::data(Bytes::from("data: hi\n\n")))]);
StreamBody::new(futures::StreamExt::chain(first, futures::stream::pending())).boxed_unsync()
} else {
Full::from("hello").map_err(|e| match e {}).boxed_unsync()
};
Ok(Response::new(body))
}
#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
let start = Instant::now();
let (layer, counter) = InFlightRequestsLayer::pair();
// 每秒上报一次;所有别的 counter 副本都没了,它自己退出
let emitter = tokio::spawn(counter.clone().run_emitter(Duration::from_secs(1), move |n| async move {
println!(" [emit {:>4}ms] in flight = {n}", start.elapsed().as_millis());
}));
let svc = ServiceBuilder::new().layer(layer).service_fn(handler);
let t = || start.elapsed().as_millis();
let fut = svc.clone().oneshot(Request::get("/").body(()).unwrap());
let mut fut = Box::pin(fut);
let _ = tokio::time::timeout(Duration::from_millis(10), &mut fut).await;
println!("{:>4}ms future pending: {}", t(), counter.get());
let res = fut.await.unwrap();
println!("{:>4}ms response head: {}", t(), counter.get());
let mut body = res.into_body();
while body.frame().await.is_some() {}
println!("{:>4}ms body read to the end: {} (is_end_stream: {})", t(), counter.get(), http_body::Body::is_end_stream(&body));
drop(body);
println!("{:>4}ms body dropped: {}", t(), counter.get());
let res = svc.clone().oneshot(Request::get("/sse").body(()).unwrap()).await.unwrap();
let mut body = res.into_body();
let _ = body.frame().await;
println!("{:>4}ms sse first event read: {}", t(), counter.get());
sleep(Duration::from_millis(2500)).await;
drop(body);
println!("{:>4}ms sse body dropped: {}", t(), counter.get());
sleep(Duration::from_millis(1000)).await;
drop(svc);
drop(counter);
emitter.await.unwrap();
println!("{:>4}ms emitter returned", t());
}
body read to the end 那一行,在途数是 0 还是 1?body 的每一帧都读完了,poll_frame 已经返回 None,is_end_stream() 为真,但 body 这个值还在作用域里。平台 M2 Max、rustc 1.99.0。
InFlightRequests 不看帧,也不看 is_end_stream,ResponseBody 的三个方法都是原样转发。能让计数减一的只有守卫的 drop。
动脑筋答案
[emit 0ms] in flight = 1
10ms future pending: 1
100ms response head: 1
100ms body read to the end: 1 (is_end_stream: true)
100ms body dropped: 0
200ms sse first event read: 1
[emit 1000ms] in flight = 1
[emit 2000ms] in flight = 1
2700ms sse body dropped: 0
[emit 3000ms] in flight = 0
[emit 4000ms] in flight = 0
4000ms emitter returned
跑 3 次相同。是 1。body 读完了,守卫还在 body 里,drop(body) 之后才变成 0。模块文档(第 5 行)说请求「is considered complete when the response body is consumed, dropped, or an error happens」,「consumed」这一条代码并没有单独实现,它成立是因为通常消费完 body 的那一方会顺手丢掉它(hyper 发完响应就丢,collect() 会拿走所有权)。自己手里攥着读完的 body 不放,计数就一直多一个。
SSE 那一段更直观:第一个事件发出去以后,连接一直开着,body 永远不结束,2.5 秒里每次上报都是 1。所以在一个有长连接(SSE、长轮询、大文件下载)的服务上,这个数是「当前打开着的响应数」,不是「正在处理的请求数」。
run_emitter
run_emitter 是一个永远循环的 async fn,通常 tokio::spawn 出去:
let mut interval = tokio::time::interval(interval);
loop {
// if all producers have gone away we don't need to emit anymore
match Arc::try_unwrap(self.count) {
Ok(_) => return,
Err(shared_count) => {
self = Self {
count: shared_count,
}
}
}
interval.tick().await;
emit(self.get()).await;
}
tokio::time::interval的第一次tick立刻完成,所以 0ms 就上报了一次(见 tokio 卷 Sleep 的一生)。metricsfeature 拉进tokio/time就是为了它。- 退出条件用
Arc::try_unwrap:别的副本(layer、服务、pair()给你的那个计数器)都没了,它自己是最后一个,就返回。检查发生在tick之前,所以上面在 3700ms 丢掉所有副本后,它还会在 4000ms 那次 tick 后再上报一次 0,下一轮循环才退出。 emit是await的:上报函数慢,下一次 tick 就推迟;interval默认的错过策略是Burst,之后会连着补上。
和 tower 的 PendingRequests 对照
tower 的 load 模块里有一个同样思路的 PendingRequests(见 tower 卷 Load):每个请求持有一个 Arc<()> 的副本,数 Arc::strong_count。两者的区别在「什么时候算完」:
tower PendingRequests | tower-http InFlightRequests | |
|---|---|---|
| 计数方式 | Arc::strong_count | AtomicUsize + 守卫 |
| 默认什么时候算完 | 响应 future 完成(CompleteOnResponse 立即丢掉句柄) | 响应 body 被 drop |
| 能不能改 | 换一个 TrackCompletion 把句柄放进响应 | 不能 |
| 用途 | P2C 负载均衡比较两个后端 | 给监控系统上报 |
tower 是协议无关的,它不知道响应有 body,只能在 future 完成时算完;tower-http 知道 HTTP 响应的工作要到 body 发完才结束,所以默认就把守卫交给 body。
InFlightRequests 是 2021 年 5 月的 #86(「Add InFlightRequests - The first metrics middleware」)加的,比 tower-http 0.1.0(2021-05-28)还早一周。PR 说明里写:「I think the two killer features of tower-http is going to be easy setup of tracing/logging and metrics」,计划覆盖四个指标:每秒/每分钟的请求数、在途请求数、错误率、每秒/每分钟的字节数,「Each of those will be a separate middleware, starting with in-flight requests as I think its the simplest」。模块结构也是按多个中间件设计的:tower_http::metrics 放常用类型,metrics::in_flight_requests 放 future、body 这些底层类型,「similar to how std::collections do it」。五年后的 0.7.1,metrics 模块里还是只有这一个。
小结
InFlightRequests是一个Arc<AtomicUsize>加一个Drop守卫:call时加一,守卫从响应 future 移进 body,守卫 drop 时减一。- 只有 drop 能减一:body 读完不丢仍算在途;内层出错或 future 被丢,守卫随 future 一起 drop。
- 长连接(SSE、下载)一直算在途,这个数更接近「打开着的响应数」。
run_emitter用tokio::time::interval定期上报,所有别的副本消失后退出(之前还会再报一次)。- 和 tower 的
PendingRequests同一思路,区别是默认在 body 被丢时才算完;metrics模块至今只有这一个中间件。