Rust in Everything
基于 tower-http 0.7.1

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());
}
❗IMPORTANT
动脑筋: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 的一生)。metrics feature 拉进 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 PendingRequeststower-http InFlightRequests
计数方式Arc::strong_countAtomicUsize + 守卫
默认什么时候算完响应 future 完成(CompleteOnResponse 立即丢掉句柄)响应 body 被 drop
能不能改换一个 TrackCompletion 把句柄放进响应不能
用途P2C 负载均衡比较两个后端给监控系统上报

tower 是协议无关的,它不知道响应有 body,只能在 future 完成时算完;tower-http 知道 HTTP 响应的工作要到 body 发完才结束,所以默认就把守卫交给 body。

📝NOTE
冷知识:计划中的四个指标,只做了一个

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 模块至今只有这一个中间件。

💬 关联讨论

发起讨论
加载中...