TiKV 源码解析系列文章(三)Prometheus(上)

做者: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 是最简单、经常使用的指标,适用于各类计数、累计的指标,要求单调递增。Counter 指标提供基本的 inc()inc_by(x) 接口,表明增长计数值。数组

在可视化的时候,此类指标通常会展现为各个时间内增长了多少,而不是各个时间计数器值是多少。例如 TiKV 收到的请求数量就是一种 Counter 指标,在监控上展现为 TiKV 每时每刻收到的请求数量图表(QPS)。安全

Gauge

Gauge 适用于上下波动的指标。Gauge 指标提供 inc()dec()add(x)sub(x)set(x) 接口,都是用于更新指标值。app

这类指标可视化的时候,通常就是直接按照时间展现它的值,从而展现出这个指标按时间是如何变化的。例如 TiKV 占用的 CPU 率是一种 Gauge 指标,在监控上所展现的直接就是 CPU 率的上下波动图表。函数

Histogram

Histogram 即直方图,是一种相对复杂但同时也很强大的指标。Histogram 除了基本的计数之外,还能计算分位数。Histogram 指标提供 observe(x) 接口,表明观测到了某个值。工具

举例来讲,TiKV 收到请求后处理的耗时就是一种 Histogram 指标,经过 Histogram 类型指标,监控上能够观察 99%、99.9%、平均请求耗时等。这里显然不能用一个 Counter 存储耗时指标,不然展现出来的只是每时每刻中 TiKV 一共花了多久处理,而非单个请求处理的耗时状况。固然,机智的你可能想到了能够另外开一个 Counter 存储请求数量指标,这样累计请求处理时间除以请求数量就是各个时刻平均请求耗时了。

实际上,这也正是 Prometheus 中 Histogram 的内部工做原理。Histogram 指标实际上最终会提供一系列时序数据:

  • 观测值落在各个桶(bucket)上的累计数量,如落在 (-∞, 0.1](-∞, 0.2](-∞, 0.4](-∞, 0.8](-∞, 1.6](-∞, +∞) 各个区间上的数量。
  • 观测值的累积和。
  • 观测值的个数。

bucket 是 Prometheus 对于 Histogram 观测值的一种简化处理方式。Prometheus 并不会具体记录下每一个观测值,而是只记录落在配置的各个 bucket 区间上的观测值的数量,这样以牺牲一部分精度的代价大大提升了效率。

Summary

SummaryHistogram 相似,针对观测值进行采样,但分位数是在客户端进行计算。该类型的指标目前在 rust-prometheus 中没有实现,所以这里不做进一步详细介绍。你们能够阅读 Prometheus 官方文档中的介绍了解详细状况。感兴趣的同窗也能够参考其余语言 Client Library 的实现为 rust-prometheus 贡献代码。

标签

Prometheus 的每一个指标支持定义和指定若干组标签(Label),指标的每一个标签值独立计数,表现了指标的不一样维度。例如,对于一个统计 HTTP 服务请求耗时的 Histogram 指标来讲,能够定义并指定诸如 HTTP Method(GET / POST / PUT / ...)、服务 URL、客户端 IP 等标签。这样能够轻易知足如下类型的查询:

  • 查询 Method 分别为 POST、PUT、GET 的 99.9% 耗时(利用单一 Label)
  • 查询 POST /api 的平均耗时(利用多个 Label 组合)

普通的查询诸如全部请求 99.9% 耗时也能正常工做。

须要注意的是,不一样标签值都是一个独立计数的时间序列,所以应当避免标签值或标签数量过多,不然实际上客户端会向 Prometheus 服务端传递大量指标,影响效率。

与 Prometheus Golang client 相似,在 rust-prometheus 中,具备标签的指标被称为 Metric Vector。例如 Histogram 指标对应的数据类型是 Histogram,而具备标签的 Histogram 指标对应的数据类型是 HistogramVec。对于一个 HistogramVec,提供它的各个标签取值后,可得到一个 Histogram 实例。不一样标签取值会得到不一样的 Histogram 实例,各个 Histogram 实例独立计数。

基本用法

本节主要介绍如何在项目中使用 rust-prometheus 进行各类指标收集。使用基本分为三步:

  1. 定义想要收集的指标。

  2. 在代码特定位置调用指标提供的接口收集记录指标值。

  3. 实现 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));
   }
}

Push / Pull

到目前为止,代码还仅仅是将指标记录了下来。最后还须要让 Prometheus 服务端能获取到记录下来的指标数据。这里通常有两种方式,分别是 Push 和 Pull。

  • Pull 是 Prometheus 标准的获取指标方式,Prometheus Server 经过按期访问应用程序提供的 HTTP 接口获取指标数据。
  • Push 是基于 Prometheus Pushgateway 服务提供的另外一种获取指标方式,指标数据由应用程序主动按期推送给 Pushgateway,而后 Prometheus 再按期从 Pushgateway 获取。这种方式主要适用于应用程序不方便开端口或应用程序生命周期比较短的场景。

如下样例代码基于 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 是很是简单的指标,只要支持线程安全的数值更新便可。读者能够简单地认为 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 也使用上原生的原子操做从而提升效率。

Histogram

根据 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 能够在编译期杜绝这种明显的使用错误。

相关文章
相关标签/搜索