intervals:迭代器差分
主线位置:两条主线的汇合点。任务那边是
RawMetrics里只增不减的原子变量,运行时那边是 tokio 只增不减的计数器;intervals()返回的迭代器把它们切成一段一段的区间。本页讲任务这边的TaskIntervals,再和运行时的RuntimeIntervals对照。
sequenceDiagram participant U as 调用方 participant I as TaskIntervals participant R as RawMetrics U->>I: next()(第一次) I->>R: metrics():逐个 load I->>R: swap(0) 取出区间最大空闲 I-->>U: 返回累计值,previous = 本次读数 U->>U: sleep 一个采样周期 U->>I: next() I->>R: metrics(),swap(0) I->>I: 与 previous 逐字段 wrapping_sub I-->>U: 返回差值,previous = 本次读数
TaskIntervals:记住上一次
TaskIntervals 只有两个字段:
#[derive(Debug)]
pub struct TaskIntervals<M: AsRef<TaskMonitorCore> + Send + Sync + 'static = TaskMonitor> {
monitor: M,
previous: Option<TaskMetrics>,
}
monitor 可以是 TaskMonitor,也可以是 &'static TaskMonitorCore(见 TaskMonitorCore 与 FutureMonitor)。previous 一开始是 None。它实现的是 Iterator 而不是 Stream,next() 永远返回 Some(self.probe());多久调一次由调用方自己决定,库里没有定时器。
probe 的骨架:
fn probe(&mut self) -> TaskMetrics {
let latest = self.monitor.as_ref().metrics.metrics();
let local_max_idle_duration = self
.monitor
.as_ref()
.metrics
.get_and_reset_local_max_idle_duration();
let next = if let Some(previous) = self.previous {
TaskMetrics {
instrumented_count: latest
.instrumented_count
.wrapping_sub(previous.instrumented_count),
// ... 其余计数同样 wrapping_sub
total_poll_duration: sub(latest.total_poll_duration, previous.total_poll_duration),
// ... 其余时长同样 sub
max_idle_duration: local_max_idle_duration,
// ...
}
} else {
latest
};
self.previous = Some(latest);
next
}
三点:
- 第一次返回累计值。
previous是None时直接返回latest,也就是从 monitor 创建到现在的全部。想让第一个区间从「现在」开始,就先空调一次next(),怎么读这一卷的程序就是这么做的。 - 差分用回绕减法。 计数是
u64::wrapping_sub;时长先用to_nanos转成纳秒的u64(乘法和加法也是wrapping_*),再wrapping_sub,见sub。u64纳秒能表示 584 年,累计值回绕在实践中不会发生;用回绕运算的效果是任何情况下都不 panic,哪怕读到的两个值顺序颠倒,也只是得到一个很大的数。运行时那边不一样:worker 计数用普通减法,运行时级的计数用saturating_sub(上一部分的 Worker 差分)。 - 最大空闲不做差。 最大值没法从两个累计最大值相减得到,所以它走另一条路:直接取一个「区间内」的原子变量,读完清零。
区间最大空闲:swap(0)
任务主线里,每记一次空闲,instrument_poll 就对两个原子变量各做一次 fetch_max(第 2946 行):
metrics
.local_max_idle_duration_ns
.fetch_max(idle_ns, SeqCst);
metrics
.global_max_idle_duration_ns
.fetch_max(idle_ns, SeqCst);
cumulative() 读 global_*;TaskIntervals::probe 读 local_*,用的是 get_and_reset_local_max_idle_duration:
fn get_and_reset_local_max_idle_duration(&self) -> Duration {
Duration::from_nanos(self.local_max_idle_duration_ns.swap(0, SeqCst))
}
swap(0) 把值取出来的同时清零,下一个区间从 0 开始取最大值。问题在于 local_max_idle_duration_ns 在 RawMetrics 里,一个 monitor 只有一份,而 TaskIntervals 可以开任意多个。其他字段每个迭代器各自记 previous,互不干扰;这一个字段,谁先读谁拿走。
另外,第一次 probe 返回的是 latest,里面的 max_idle_duration 来自 global_*;但 swap(0) 照样执行了,local_* 同样被清零。
实验:同一个 monitor 开 a、b 两个迭代器(比如两个看板),每轮让一个被包装的 sleep 空闲一段,然后两个迭代器各取一次;最后再晚开一个 c:
use std::time::Duration;
use tokio::time::sleep;
use tokio_metrics::TaskMonitor;
#[tokio::main(flavor = "current_thread")]
async fn main() {
let m = TaskMonitor::new();
let mut a = m.intervals(); // 看板 A
let mut b = m.intervals(); // 看板 B,同一个 monitor
// 第 1 轮:一段约 100ms 的空闲
m.instrument(sleep(Duration::from_millis(100))).await;
let (x, y) = (a.next().unwrap(), b.next().unwrap());
println!("round1 a: polls={} max_idle={:?} | b: polls={} max_idle={:?}", x.total_poll_count, x.max_idle_duration, y.total_poll_count, y.max_idle_duration);
// 第 2 轮:一段约 60ms 的空闲
m.instrument(sleep(Duration::from_millis(60))).await;
let (x, y) = (a.next().unwrap(), b.next().unwrap());
println!("round2 a: polls={} max_idle={:?} | b: polls={} max_idle={:?}", x.total_poll_count, x.max_idle_duration, y.total_poll_count, y.max_idle_duration);
// 第 3 轮:什么都不做
let (x, y) = (a.next().unwrap(), b.next().unwrap());
println!("round3 a: polls={} max_idle={:?} slow_poll_ratio={} | b: polls={} max_idle={:?}", x.total_poll_count, x.max_idle_duration, x.slow_poll_ratio(), y.total_poll_count, y.max_idle_duration);
// 晚开的迭代器:第一次 next() 是从 monitor 创建起的累计值
let mut c = m.intervals();
let z = c.next().unwrap();
let z2 = c.next().unwrap();
println!("late c first: polls={} max_idle={:?}; second: polls={} | cumulative max_idle={:?}", z.total_poll_count, z.max_idle_duration, z2.total_poll_count, m.cumulative().max_idle_duration);
}
依赖同前(tokio-metrics = "=0.5.2"、tokio = { version = "=1.54.1", features = ["full"] })。M2 Max 笔记本、--release,连跑 4 次,计数和「谁是 0」每次都一样,时长在零点几毫秒内变化。其中一次:
round1 a: polls=2 max_idle=103.019333ms | b: polls=2 max_idle=103.019333ms
round2 a: polls=2 max_idle=62.638916ms | b: polls=2 max_idle=0ns
round3 a: polls=0 max_idle=0ns slow_poll_ratio=NaN | b: polls=0 max_idle=0ns
late c first: polls=4 max_idle=103.019333ms; second: polls=0 | cumulative max_idle=103.019333ms
- 第 1 轮两边相同:都是第一次
next(),返回累计值,max_idle_duration来自全局变量。 - 第 2 轮
b是 0:a先swap(0)拿走了 62.6ms,b读到的是清零后的值。polls=2两边都对,因为普通字段各自做差。哪个看板先取样,哪个就有最大空闲;两个看板的取样周期不同步时,这个值会在两边随机地出现和消失。 - 第 3 轮:没有任何 poll,
slow_poll_ratio是 0/0 = NaN(TaskMetrics 与派生指标)。 - 晚开的
c:第一次next()把 monitor 创建以来的 4 次 poll 全部返回,max_idle_duration是全局最大的 103ms,而不是「c创建以来」的。
每个空闲比 sleep 的时长多出两三毫秒,原因和怎么读这一卷里的实验一样:时间轮的精度是 1ms,woke_at 记的是 wake() 被调用的时刻。
一个 monitor 需要喂给多个消费者时,只开一个 intervals(),把每次得到的 TaskMetrics(它是 Copy)分发出去;或者每个消费者一个 monitor。
RuntimeIntervals:从调用时起算
运行时那边没有 previous: Option。RuntimeMonitor::intervals 在调用时就给每个 worker 记下当前累计值(上一部分的 Worker 差分),所以第一次 next() 已经是差分。两者对照:
TaskIntervals | RuntimeIntervals | |
|---|---|---|
| 基准在什么时候定 | 第一次 next() | 调用 intervals() 时 |
第一次 next() 返回 | 从 monitor 创建起的累计值 | 从 intervals() 起的差分 |
| 计数做差 | wrapping_sub | worker 计数普通 -,运行时级 saturating_sub |
| 区间的长度 | 没有字段,调用方自己记 | elapsed |
| 多个迭代器 | 互相抢 max_idle_duration | 互不干扰,每个迭代器各有一份 worker 快照 |
有没有 cumulative() | 有 | 没有,只能读 tokio 的 Handle::metrics() |
RuntimeIntervals 没有任何「读完清零」的状态,开几个都可以。TaskMetrics 没有 elapsed,算速率(每秒 poll 次数)要自己记取样时刻。
cumulative() 还是 intervals()
TaskMonitor::cumulative 直接返回 RawMetrics::metrics() 的结果,不改任何状态,随便调多少次。两者的区别只在「差分由谁来做」:
- 导出到 Prometheus 这类自己会算速率的系统:计数和时长本来就适合当单调递增的 counter,用
cumulative()原样导出,让rate()去做差,不需要本库的差分,也就没有「两个迭代器抢最大值」的问题。代价是max_idle_duration只有全局最大值,看不到「最近一分钟的最大空闲」。 - 打日志、做单机判断(比如「这 10 秒的慢 poll 比例超过 20% 就告警」):用
intervals(),每个区间的数字直接可读。只开一个迭代器。
本库自带的 metrics-rs 集成走的是第二条路:每个区间调一次 next(),把差值交给 metrics-rs。它怎么把差值映射成 counter 和 gauge,放在下一页 metrics-rs 与 metrique。
小结
TaskIntervals记一个previous: Option<TaskMetrics>:第一次next()返回累计值,之后逐字段与上次相减;计数和时长都用回绕运算,不会 panic。- 区间最大空闲不能做差,靠
local_max_idle_duration_ns.swap(0)读完清零;它一个 monitor 只有一份,同一个 monitor 开两个intervals(),后取样的一方读到 0。 RuntimeIntervals在调用intervals()时记下基准,第一次next()已经是差分,带elapsed,开几个都互不影响。- 导出给会算速率的系统,用
cumulative()让对方做差;要直接可读的区间数字,用intervals(),一个 monitor 只开一个。