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 @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?

99%的人都选错了! 路由器WiFi双频合一还是分开好的专业解析与适用场景探讨

《99%的人都选错了!路由器WiFi双频合一还是分开好的专业解析与适用场景探讨》关于双频路由器的“双频合一”与“分开使用”两种模式,用户往往存在诸多疑问,本文将从多个维度深入探讨这两种模式的优缺点,... 在如今“没有WiFi就等于与世隔绝”的时代,越来越多家庭、办公室都开始配置双频无线路由器。但你有没有注

Python中的sort()和sorted()用法示例解析

《Python中的sort()和sorted()用法示例解析》本文给大家介绍Python中list.sort()和sorted()的使用区别,详细介绍其参数功能及Timsort排序算法特性,涵盖自适应... 目录一、list.sort()参数说明常用内置函数基本用法示例自定义函数示例lambda表达式示例o