做者:Breezewishhtml
本文为 TiKV 源码解析系列的第三篇,继续为你们介绍 TiKV 依赖的周边库 rust-prometheus,本篇主要介绍基础知识以及最基本的几个指标的内部工做机制,下篇会介绍一些高级功能的实现原理。git
rust-prometheus 是监控系统 Prometheus 的 Rust 客户端库,由 TiKV 团队实现。TiKV 使用 rust-prometheus 收集各类指标(metric)到 Prometheus 中,从然后续能再利用 Grafana 等可视化工具将其展现出来做为仪表盘监控面板。这些监控指标对于了解 TiKV 当前或历史的状态具备很是关键的做用。TiKV 提供了丰富的监控指标数据,而且代码中也处处穿插了监控指标的收集片断,所以了解 rust-prometheus 颇有必要。github
感兴趣的小伙伴还能够观看我司同窗在 FOSDEM 2019 会议上关于 rust-prometheus 的技术分享。golang
Prometheus 支持四种指标:Counter、Gauge、Histogram、Summary。rust-prometheus 库目前还只实现了前三种。TiKV 大部分指标都是 Counter 和 Histogram,少部分是 Gauge。api
Counter 是最简单、经常使用的指标,适用于各类计数、累计的指标,要求单调递增。Counter 指标提供基本的 inc()
或 inc_by(x)
接口,表明增长计数值。数组
在可视化的时候,此类指标通常会展现为各个时间内增长了多少,而不是各个时间计数器值是多少。例如 TiKV 收到的请求数量就是一种 Counter 指标,在监控上展现为 TiKV 每时每刻收到的请求数量图表(QPS)。安全
Gauge 适用于上下波动的指标。Gauge 指标提供 inc()
、dec()
、add(x)
、sub(x)
和 set(x)
接口,都是用于更新指标值。app
这类指标可视化的时候,通常就是直接按照时间展现它的值,从而展现出这个指标按时间是如何变化的。例如 TiKV 占用的 CPU 率是一种 Gauge 指标,在监控上所展现的直接就是 CPU 率的上下波动图表。函数
Histogram 即直方图,是一种相对复杂但同时也很强大的指标。Histogram 除了基本的计数之外,还能计算分位数。Histogram 指标提供 observe(x)
接口,表明观测到了某个值。工具
举例来讲,TiKV 收到请求后处理的耗时就是一种 Histogram 指标,经过 Histogram 类型指标,监控上能够观察 99%、99.9%、平均请求耗时等。这里显然不能用一个 Counter 存储耗时指标,不然展现出来的只是每时每刻中 TiKV 一共花了多久处理,而非单个请求处理的耗时状况。固然,机智的你可能想到了能够另外开一个 Counter 存储请求数量指标,这样累计请求处理时间除以请求数量就是各个时刻平均请求耗时了。
实际上,这也正是 Prometheus 中 Histogram 的内部工做原理。Histogram 指标实际上最终会提供一系列时序数据:
(-∞, 0.1]
、(-∞, 0.2]
、(-∞, 0.4]
、(-∞, 0.8]
、(-∞, 1.6]
、(-∞, +∞)
各个区间上的数量。bucket 是 Prometheus 对于 Histogram 观测值的一种简化处理方式。Prometheus 并不会具体记录下每一个观测值,而是只记录落在配置的各个 bucket 区间上的观测值的数量,这样以牺牲一部分精度的代价大大提升了效率。
Summary 与 Histogram 相似,针对观测值进行采样,但分位数是在客户端进行计算。该类型的指标目前在 rust-prometheus 中没有实现,所以这里不做进一步详细介绍。你们能够阅读 Prometheus 官方文档中的介绍了解详细状况。感兴趣的同窗也能够参考其余语言 Client Library 的实现为 rust-prometheus 贡献代码。
Prometheus 的每一个指标支持定义和指定若干组标签(Label),指标的每一个标签值独立计数,表现了指标的不一样维度。例如,对于一个统计 HTTP 服务请求耗时的 Histogram 指标来讲,能够定义并指定诸如 HTTP Method(GET / POST / PUT / ...)、服务 URL、客户端 IP 等标签。这样能够轻易知足如下类型的查询:
普通的查询诸如全部请求 99.9% 耗时也能正常工做。
须要注意的是,不一样标签值都是一个独立计数的时间序列,所以应当避免标签值或标签数量过多,不然实际上客户端会向 Prometheus 服务端传递大量指标,影响效率。
与 Prometheus Golang client 相似,在 rust-prometheus 中,具备标签的指标被称为 Metric Vector。例如 Histogram 指标对应的数据类型是 Histogram
,而具备标签的 Histogram 指标对应的数据类型是 HistogramVec
。对于一个 HistogramVec
,提供它的各个标签取值后,可得到一个 Histogram
实例。不一样标签取值会得到不一样的 Histogram
实例,各个 Histogram
实例独立计数。
本节主要介绍如何在项目中使用 rust-prometheus 进行各类指标收集。使用基本分为三步:
定义想要收集的指标。
在代码特定位置调用指标提供的接口收集记录指标值。
实现 HTTP Pull Service 使得 Prometheus 能够按期访问收集到的指标,或使用 rust-prometheus 提供的 Push 功能按期将收集到的指标上传到 Pushgateway。
注意,如下样例代码都是基于本文发布时最新的 rust-prometheus 0.5 版本 API。咱们目前正在设计并实现 1.0 版本,使用上会进一步简化,但如下样例代码可能在 1.0 版本发布后过期、再也不工做,届时请读者参考最新的文档。
为了简化使用,通常将指标声明为一个全局可访问的变量,从而能在代码各处自由地操纵它。rust-prometheus 提供的各个指标(包括 Metric Vector)都知足 Send + Sync
,能够被安全地全局共享。
如下样例代码借助 lazy_static 库定义了一个全局的 Histogram 指标,该指标表明 HTTP 请求耗时,而且具备一个标签名为 method
:
#[macro_use]
extern crate prometheus;
lazy_static! {
static ref REQUEST_DURATION: HistogramVec = register_histogram_vec!(
"http_requests_duration",
"Histogram of HTTP request duration in seconds",
&["method"],
exponential_buckets(0.005, 2.0, 20).unwrap()
).unwrap();
}
复制代码
有了一个全局可访问的指标变量后,就能够在代码中经过它提供的接口记录指标值了。在“基础知识”中介绍过,Histogram
最主要的接口是 observe(x)
,能够记录一个观测值。若想了解 Histogram
其余接口或其余类型指标提供的接口,能够参阅 rust-prometheus 文档。
如下样例在上段代码基础上展现了如何记录指标值。代码模拟了一些随机值用做指标,装做是用户产生的。在实际程序中,这些固然得改为真实数据 :)
fn thread_simulate_requests() {
let mut rng = rand::thread_rng();
loop {
// Simulate duration 0s ~ 2s
let duration = rng.gen_range(0f64, 2f64);
// Simulate HTTP method
let method = ["GET", "POST", "PUT", "DELETE"].choose(&mut rng).unwrap();
// Record metrics
REQUEST_DURATION.with_label_values(&[method]).observe(duration);
// One request per second
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
复制代码
到目前为止,代码还仅仅是将指标记录了下来。最后还须要让 Prometheus 服务端能获取到记录下来的指标数据。这里通常有两种方式,分别是 Push 和 Pull。
如下样例代码基于 hyper HTTP 库实现了一个能够供 Prometheus Server pull 指标数据的接口,核心是使用 rust-prometheus 提供的 TextEncoder
将全部指标数据序列化供 Prometheus 解析:
fn metric_service(_req: Request<Body>) -> Response<Body> {
let encoder = TextEncoder::new();
let mut buffer = vec![];
let mf = prometheus::gather();
encoder.encode(&mf, &mut buffer).unwrap();
Response::builder()
.header(hyper::header::CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer))
.unwrap()
}
复制代码
对于如何使用 Push 感兴趣的同窗能够自行参考 rust-prometheus 代码内提供的 Push 示例,这里限于篇幅就不详细介绍了。
上述三段样例的完整代码可参见这里。
如下内部实现都基于本文发布时最新的 rust-prometheus 0.5 版本代码,该版本主干 API 的设计和实现 port 自 Prometheus Golang client,但为 Rust 的使用习惯进行了一些修改,所以接口上与 Golang client 比较接近。
目前咱们正在开发 1.0 版本,API 设计上再也不主要参考 Golang client,而是力求提供对 Rust 使用者最友好、简洁的 API。实现上为了效率考虑也会和这里讲解的略微有一些出入,且会去除一些目前已被抛弃的特性支持,简化实现,所以请读者注意甄别。
Counter 与 Gauge 是很是简单的指标,只要支持线程安全的数值更新便可。读者能够简单地认为 Counter 和 Gauge 的核心实现都是 Arc<Atomic>
。但因为 Prometheus 官方规定指标数值须要支持浮点数,所以咱们基于 std::sync::atomic::AtomicU64
和 CAS 操做实现了 AtomicF64
,其具体实现位于 src/atomic64/nightly.rs。核心片断以下:
impl Atomic for AtomicF64 {
type T = f64;
// Some functions are omitted.
fn inc_by(&self, delta: Self::T) {
loop {
let current = self.inner.load(Ordering::Acquire);
let new = u64_to_f64(current) + delta;
let swapped = self
.inner
.compare_and_swap(current, f64_to_u64(new), Ordering::Release);
if swapped == current {
return;
}
}
}
}
复制代码
另外因为 0.5 版本发布时 AtomicU64
仍然是一个 nightly 特性,所以为了支持 Stable Rust,咱们还基于自旋锁提供了 AtomicF64
的 fallback,位于 src/atomic64/fallback.rs。
注:
AtomicU64
所需的 integer_atomics 特性最近已在 rustc 1.34.0 stabilize。咱们将在 rustc 1.34.0 发布后为 Stable Rust 也使用上原生的原子操做从而提升效率。
根据 Prometheus 的要求,Histogram 须要进行的操做是在得到一个观测值之后,为观测值处在的桶增长计数值。另外还有总观测值、观测值数量须要累加。
注意,Prometheus 中的 Histogram 是累积直方图,其每一个桶的含义是 (-∞, x]
,所以对于每一个观测值均可能要更新多个连续的桶。例如,假设用户定义了 5 个桶边界,分别是 0.一、0.二、0.四、0.八、1.6,则每一个桶对应的数值范围是 (-∞, 0.1]
、(-∞, 0.2]
、(-∞, 0.4]
、(-∞, 0.8]
、(-∞, 1.6]
、(-∞, +∞)
,对于观测值 0.4 来讲须要更新(-∞, 0.4]
、(-∞, 0.8]
、(-∞, 1.6]
、(-∞, +∞)
四个桶。
通常来讲 observe(x)
会被频繁地调用,而将收集到的数据反馈给 Prometheus 则是个相对很低频率的操做,所以用数组实现“桶”的时候,咱们并不将各个桶与数组元素直接对应,而将数组元素定义为非累积的桶,如 (-∞, 0.1)
、[0.1, 0.2)
、[0.2, 0.4)
、[0.4, 0.8)
、[0.8, 1.6)
、[1.6, +∞)
,这样就大大减小了须要频繁更新的数据量;最后在上报数据给 Prometheus 的时候将数组元素累积,获得累积直方图,这样就获得了 Prometheus 所须要的桶的数据。
固然,因而可知,若是给定的观测值超出了桶的范围,则最终记录下的最大值只有桶的上界了,然而这并非实际的最大值,所以使用的时候须要多加注意。
Histogram
的核心实现见 src/histogram.rs:
pub struct HistogramCore {
// Some fields are omitted.
sum: AtomicF64,
count: AtomicU64,
upper_bounds: Vec<f64>,
counts: Vec<AtomicU64>,
}
impl HistogramCore {
// Some functions are omitted.
pub fn observe(&self, v: f64) {
// Try find the bucket.
let mut iter = self
.upper_bounds
.iter()
.enumerate()
.filter(|&(_, f)| v <= *f);
if let Some((i, _)) = iter.next() {
self.counts[i].inc_by(1);
}
self.count.inc_by(1);
self.sum.inc_by(v);
}
}
#[derive(Clone)]
pub struct Histogram {
core: Arc<HistogramCore>,
}
复制代码
Histogram
还提供了一个辅助结构 HistogramTimer
,它会记录从它建立直到被 Drop 的时候的耗时,将这个耗时做为 Histogram::observe()
接口的观测值记录下来,这样不少时候在想要记录 Duration / Elapsed Time 的场景中,就可使用这个简便的结构来记录时间:
#[must_use]
pub struct HistogramTimer {
histogram: Histogram,
start: Instant,
}
impl HistogramTimer {
// Some functions are omitted.
pub fn observe_duration(self) {
drop(self);
}
fn observe(&mut self) {
let v = duration_to_seconds(self.start.elapsed());
self.histogram.observe(v)
}
}
impl Drop for HistogramTimer {
fn drop(&mut self) {
self.observe();
}
}
复制代码
HistogramTimer
被标记为了 must_use
,缘由很简单,做为一个记录流逝时间的结构,它应该被存在某个变量里,从而记录这个变量所处做用域的耗时(或稍后直接调用相关函数提早记录耗时),而不该该做为一个未使用的临时变量被当即 Drop。标记为 must_use
能够在编译期杜绝这种明显的使用错误。