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

2024-04-08 03:08

本文主要是介绍TiKV 源码解析系列文章(三)Prometheus(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者:Breezewish

本文为 TiKV 源码解析系列的第三篇,继续为大家介绍 TiKV 依赖的周边库 rust-prometheus,本篇主要介绍基础知识以及最基本的几个指标的内部工作机制,下篇会介绍一些高级功能的实现原理。

rust-prometheus 是监控系统 Prometheus 的 Rust 客户端库,由 TiKV 团队实现。TiKV 使用 rust-prometheus 收集各种指标(metric)到 Prometheus 中,从而后续能再利用 Grafana 等可视化工具将其展示出来作为仪表盘监控面板。这些监控指标对于了解 TiKV 当前或历史的状态具有非常关键的作用。TiKV 提供了丰富的监控指标数据,并且代码中也到处穿插了监控指标的收集片段,因此了解 rust-prometheus 很有必要。

感兴趣的小伙伴还可以观看我司同学在 FOSDEM 2019 会议上关于 rust-prometheus 的技术分享。

基础知识

指标类别

Prometheus 支持四种指标:Counter、Gauge、Histogram、Summary。rust-prometheus 库目前还只实现了前三种。TiKV 大部分指标都是 Counter 和 Histogram,少部分是 Gauge。

Counter

Counter 是最简单、常用的指标,适用于各种计数、累计的指标,要求单调递增。Counter 指标提供基本的 inc()inc_by(x) 接口,代表增加计数值。

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

Gauge

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

这类指标可视化的时候,一般就是直接按照时间展示它的值,从而展示出这个指标按时间是如何变化的。例如 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

Summary 与 Histogram 类似,针对观测值进行采样,但分位数是在客户端进行计算。该类型的指标目前在 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 ~ 2slet duration = rng.gen_range(0f64, 2f64);// Simulate HTTP methodlet method = ["GET", "POST", "PUT", "DELETE"].choose(&mut rng).unwrap();// Record metricsREQUEST_DURATION.with_label_values(&[method]).observe(duration);// One request per secondstd::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.1、0.2、0.4、0.8、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 可以在编译期杜绝这种明显的使用错误。

这篇关于TiKV 源码解析系列文章(三)Prometheus(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/884402

相关文章

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

一文解析C#中的StringSplitOptions枚举

《一文解析C#中的StringSplitOptions枚举》StringSplitOptions是C#中的一个枚举类型,用于控制string.Split()方法分割字符串时的行为,核心作用是处理分割后... 目录C#的StringSplitOptions枚举1.StringSplitOptions枚举的常用

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

MyBatis延迟加载与多级缓存全解析

《MyBatis延迟加载与多级缓存全解析》文章介绍MyBatis的延迟加载与多级缓存机制,延迟加载按需加载关联数据提升性能,一级缓存会话级默认开启,二级缓存工厂级支持跨会话共享,增删改操作会清空对应缓... 目录MyBATis延迟加载策略一对多示例一对多示例MyBatis框架的缓存一级缓存二级缓存MyBat

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码

Java集合之Iterator迭代器实现代码解析

《Java集合之Iterator迭代器实现代码解析》迭代器Iterator是Java集合框架中的一个核心接口,位于java.util包下,它定义了一种标准的元素访问机制,为各种集合类型提供了一种统一的... 目录一、什么是Iterator二、Iterator的核心方法三、基本使用示例四、Iterator的工

Java JDK Validation 注解解析与使用方法验证

《JavaJDKValidation注解解析与使用方法验证》JakartaValidation提供了一种声明式、标准化的方式来验证Java对象,与框架无关,可以方便地集成到各种Java应用中,... 目录核心概念1. 主要注解基本约束注解其他常用注解2. 核心接口使用方法1. 基本使用添加依赖 (Maven