TiKV 源码解析系列文章(三)Prometheus(上)
本文为 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 进行各种指标收集。使用基本分为三步:
定义想要收集的指标。
在代码特定位置调用指标提供的接口收集记录指标值。
实现 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();
www.gcyl159.com/ let mut buffer = vec![];
let mf = prometheus::gather();
encoder.encode(&mf,www.mren2yule.com &mut buffer).unwrap();
Response::builder(www.yongshiyule178.com)
.header(hyper::header::CONTENT_TYPE, encoder.format_type())
.body(Body::from(buffer)www.mtyl127.com)
.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(www.ysptvip1.com current) + delta;
let swapped = self
.inner
.compare_and_swap(current, f64_to_u64(new), Ordering::Release);
if swapped =www.dfgjpt.com= 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 <www.yongshi123.cn= *f);
if let Some((i, _)) = iter.next() {
self.counts[i]www.120xh.cn.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: www.dfgjyl.cn Histogram,
start: Instant,
}
impl HistogramTimer {
// Some functions are omitted.
pub fn observe_duration(self) {
drop(self);
}
fn observe(www.yongxin7.com &mut self) {
let v = duration_to_seconds(self.start.elapsed());
self.histogram.observe(www.mhylpt.com)
}
}
impl Drop for HistogramTimer {
fn drop(&mut self) {
self.observe();
}
}
HistogramTimer 被标记为了 must_use,原因很简单,作为一个记录流逝时间的结构,它应该被存在某个变量里,从而记录这个变量所处作用域的耗时(或稍后直接调用相关函数提前记录耗时),而不应该作为一个未使用的临时变量被立即 Drop。标记为 must_use 可以在编译期杜绝这种明显的使用错误。
config_file="/etc/sysconfig/vncservers"
port_file="/root/.vnc_port"
vncserver_file=`which vncserver`
#sed -i 's/5900\ +/12000\ +/g' $vncserver_file
#sed -i 's/12000\ +/5900\ +/g' $vncserver_file
###端口修改成起始端口
function port_init {
last_port=5900
if [[ -e $port_file ]];then
old_port=`cat $port_file`
sed -i "s/$old_port\ +/5900\ +/g" $vncserver_file
echo "5900" > $port_file
fi
}
###修改端口
function modfy_port {
last_port=$1
if [[ $last_port == "www.tiaotiaoylzc.com" ]];then
echo "Please input the port you want "
else
if [[ -e $port_file ]];then
old_port=`cat $port_file`
sed -i "s/$old_port\ +/$last_port\ +/g" $vncserver_file
echo $last_port > $port_file
else
sed -i "s/5900\ +/$last_port\ +/g" $vncserver_file
echo $last_port > $port_file
fi
fi
}
###添加用户
function adduser {
if [[ $1 == "" ]];then
echo "Please input a user name"
else
user_num=`grep -w VNCSERVERS $config_file|awk '{print $NF}'|cut -d":" -f1`
latest_user_num=`expr $user_num + 1`
last_user=`grep -w VNCSERVERS $config_file|awk '{print $NF}'|awk -F'"' '{print $1}'`
sed -i "s/$last_user/$last_user\ $latest_user_num:$1/" $config_file
echo "VNCSERVERSVNCSERVERARGS[$latest_user_num]=\"-geometry 1600x900 -alwaysshared\"" >> $config_file
useradd $1
echo "****"|passwd --stdin $1
fi
}
###删除用户
function deleteuser {
if [[ $1 == "" ]];then
echo "Please input the username to delete"
else
delete_user_match=$(egrep -w "[0-9]{1,3}:$1" $config_file|wc -l)
if [[ $delete_user_match -ne 1 ]];then
echo "Please make sure the user name is correct"
else
delete_user_num=`egrep -w "[0-9]{1,3}:$1" $config_file|awk -F":$1" '{print $1}'|awk '{print $NF}'`
sed -i "s/\ $delete_user_num:$1//" $config_file
sed -i "s/.*\[$delete_user_num\].*//" $config_file
userdel -r $1
fi
fi
}
###服务启动
function start {
#user_list=`grep -w VNCSERVERS $config_file|awk -F'"' '{print $2}'|sed 's/[0-9]\+://g' `
user_list=`grep -w VNCSERVERS $config_file|awk -F'"' '{print $2}'`
echo $user_list
for i in $user_list
do
user_port=$(echo $i|cut -d":" -f1)
user=$(echo $i|cut -d":" -f2)
#su - $user -c "vncserver -kill :$user_port"
expect -c "
set timeout 1200;
spawn /usr/bin/su - $user -c "vncserver\ \:$user_port"
expect {
"Password:" {send "***"\r;exp_continue}
"Verify:" {send "***"\r;}
}
expect eof;"
done
}
function stop {
user_list=`ps aux |grep vnc |grep rfbport |awk '{print $1 $12}'`
for i in $user_list
do
user_port=$(echo $i|cut -d":" -f2)
user=$(echo $i|cut -d":" -f1)
su - $user -c "vncserver -kill :$user_port"
done
}
case $1 in
adduser)
adduser $2
;;
deleteuser)
deleteuser $2
;;
stop)
stop
;;
start)
start
;;
default_port)
port_init
;;
modify_port)
modfy_port $2
;;
*)
echo -e "Usage:\ncommands:\n adduser <username> #define:please input a user name,Don't use pure numbers\n deleteuser <username> #define:please input a complete user name\n start #define:start all service\n stop #define:stop all service\n default_port #define:Modify port to default start segment\n modify_port <port> #define:Modify the port to the segment you want"
TiKV 源码解析系列文章(三)Prometheus(上)的更多相关文章
- TiKV 源码解析系列 - Raft 的优化
本系列文章主要面向 TiKV 社区开发者,重点介绍 TiKV 的系统架构,源码结构,流程解析.目的是使得开发者阅读之后,能对 TiKV 项目有一个初步了解,更好的参与进入 TiKV 的开发中.本文是本 ...
- SpringBoot源码解析系列文章汇总
相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的SpringBoot源码解析系列文章的汇总,当你使用SpringBoot不仅仅满足于基本使用时.或者出去面试被面试官虐了时.或者说想要深入了解一下 ...
- TiKV 源码解析系列——如何使用 Raft
本系列文章主要面向 TiKV 社区开发者,重点介绍 TiKV 的系统架构,源码结构,流程解析.目的是使得开发者阅读之后,能对 TiKV 项目有一个初步了解,更好的参与进入 TiKV 的开发中. 需要注 ...
- Eureka源码解析系列文章汇总
先看一张图 0 这个图是Eureka官方提供的架构图,整张图基本上把整个Eureka的核心功能给列出来了,当你要阅读Eureka的源码时可以参考着这个图和下方这些文章 EurekaServer Eur ...
- tensorflow源码解析系列文章索引
文章索引 framework解析 resource allocator tensor op node kernel graph device function shape_inference 拾遗 c ...
- Abp Vnext源码解析系列文章01---EventBus
一.简介 BP vNext 封装了两种事件总线结构,第一种是 ABP vNext 自己实现的本地事件总线,这种事件总线无法跨项目发布和订阅.第二种则是分布式事件总线,ABP vNext 自己封装了一个 ...
- TiKV 源码解析系列——Placement Driver
https://zhuanlan.zhihu.com/p/24809131?refer=newsql
- Android源码解析系列
转载请标明出处:一片枫叶的专栏 知乎上看了一篇非常不错的博文:有没有必要阅读Android源码 看完之后痛定思过,平时所学往往是知其然然不知其所以然,所以为了更好的深入Android体系,决定学习an ...
- Spring源码解析系列汇总
相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...
随机推荐
- .NET Core 3.0 跟踪
Preview1: https://blogs.msdn.microsoft.com/dotnet/2018/12/04/announcing-net-core-3-preview-1-and-ope ...
- git log 的常用选项
- WPF中TreeView.BringIntoView方法的替代方案
原文:WPF中TreeView.BringIntoView方法的替代方案 WPF中TreeView.BringIntoView方法的替代方案 周银辉 WPF中TreeView.BringIntoVie ...
- KMeans算法分析以及实现
KMeans KMeans是一种无监督学习聚类方法, 目的是发现数据中数据对象之间的关系,将数据进行分组,组内的相似性越大,组间的差别越大,则聚类效果越好. 无监督学习,也就是没有对应的标签,只有数据 ...
- vue-router 注意事项
1.vue-router 两种模式 (1)mode:hash,hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件.vue默认为hash模式 window.onh ...
- php ajax登录注册
用户登录与退出功能应用在很多地方,而在有些项目中,我们需要使用Ajax方式进行登录,登录成功后只刷新页面局部,从而提升了用户体验度.本文将使用PHP和jQuery来实现登录和退出功能. 准备数据库 本 ...
- 【ML】ICLR2016_Delving Deeper into Convolutional Networks
ICLR2016_DELVING DEEPER INTO CONVOLUTIONAL NETWORKS Note here: Ballas recently proposed a novel fram ...
- 使用thinkphp框架实现Excel导入数据库
之前讲过php实现Excel导出数据库的随笔,链接:https://www.cnblogs.com/nuanai/p/6727711.html 之前的项目用到较多的就是Excel导出,现在用到了Exc ...
- Particle filter for visual tracking
Kalman Filter Cons: Kalman filtering is inadequate because it is based on the unimodal Gaussian dist ...
- python 函数及变量作用域及装饰器decorator @详解
一.函数及变量的作用 在python程序中,函数都会创建一个新的作用域,又称为命名空间,当函数遇到变量时,Python就会到该函数的命名空间来寻找变量,因为Python一切都是对象,而在命名空间中 ...