prometheus变量_TiKV 源码解析系列文章(四)Prometheus(下)
本文為 TiKV 源碼解析系列的第四篇,接上篇繼續為大家介紹?rust-prometheus。上篇主要介紹了基礎知識以及最基本的幾個指標的內部工作機制,本篇會進一步介紹更多高級功能的實現原理。
與上篇一樣,以下內部實現都基于本文發布時最新的 rust-prometheus 0.5 版本代碼,目前我們正在開發 1.0 版本,API 設計上會進行一些簡化,實現上出于效率考慮也會和這里講解的略微有一些出入,因此請讀者注意甄別。
指標向量(Metric Vector)
Metric Vector 用于支持帶 Label 的指標。由于各種指標都可以帶上 Label,因此 Metric Vector 本身實現為了一種泛型結構體,Counter、Gauge?和?Histogram?在這之上實現了?CounterVec、GaugeVec?和?HistogramVec。Metric Vector 主要實現位于?src/vec.rs。
以?HistogramVec?為例,調用?HistogramVec::with_label_values?可獲得一個?Histogram?實例,而?HistogramVec?定義為:
pub type HistogramVec = MetricVec;pub struct MetricVec {pub(crate) v: Arc<MetricVecCore<T>>,
}implMetricVec {pub fn with_label_values(&self, vals: &[&str]) -> T::M {self.get_metric_with_label_values(vals).unwrap()
}
}
因此?HistogramVec::with_label_values?的核心邏輯其實在?MetricVecCore::get_metric_with_label_values。這么做的原因是為了讓?MetricVec?是一個線程安全、可以被全局共享但又不會在共享的時候具有很大開銷的結構,因此將內部邏輯實現在?MetricVecCore,外層(即在?MetricVec)套一個?Arc?后再提供給用戶。進一步可以觀察?MetricVecCore?的實現,其核心邏輯如下:
pub trait MetricVecBuilder: Send + Sync + Clone {type M: Metric;type P: Describer + Sync + Send + Clone;fn build(&self, &Self::P, &[&str]) -> Result<Self::M>;
}pub(crate) struct MetricVecCore {pub children: RwLock<HashMap, T::M>>,// Some fields are omitted.
}implMetricVecCore {// Some functions are omitted.pub fn get_metric_with_label_values(&self, vals: &[&str]) -> Result<:m> {let h = self.hash_label_values(vals)?;if let Some(metric) = self.children.read().get(&h).cloned() {return Ok(metric);
}self.get_or_create_metric(h, vals)
}pub(crate) fn hash_label_values(&self, vals: &[&str]) -> Result<u64> {if vals.len() != self.desc.variable_labels.len() {return Err(Error::InconsistentCardinality(self.desc.variable_labels.len(),
vals.len(),
));
}let mut h = FnvHasher::default();for val in vals {
h.write(val.as_bytes());
}Ok(h.finish())
}fn get_or_create_metric(&self, hash: u64, label_values: &[&str]) -> Result<:m> {let mut children = self.children.write();// Check exist first.if let Some(metric) = children.get(&hash).cloned() {return Ok(metric);
}let metric = self.new_metric.build(&self.opts, label_values)?;
children.insert(hash, metric.clone());Ok(metric)
}
}
現在看代碼就很簡單了,它首先會依據所有 Label Values 構造一個 Hash,接下來用這個 Hash 在?RwLock>?中查找,如果找到了,說明給定的這個 Label Values 之前已經出現過、相應的 Metric 指標結構體已經初始化過,因此直接返回對應的實例;如果不存在,則要利用給定的?MetricVecBuilder?構造新的指標加入哈希表,并返回這個新的指標。
由上述代碼可見,為了在線程安全的條件下實現 Metric Vector 各個 Label Values 具有獨立的時間序列,Metric Vector 內部采用了?RwLock?進行同步,也就是說?with_label_values()?及類似函數內部是具有鎖的。這在多線程環境下會有一定的效率影響,不過因為大部分情況下都是讀鎖,因此影響不大。當然,還可以發現其實給定 Label Values 之后調用?with_label_values()?得到的指標實例是可以被緩存起來的,只訪問緩存起來的這個指標實例是不會有任何同步開銷的,也繞開了計算哈希值等比較占 CPU 的操作。基于這個思想,就有了 Static Metrics,讀者可以在本文的后半部分了解 Static Metrics 的詳細情況。
另外讀者也可以發現,Label Values 的取值應當是一個有限的、封閉的小集合,不應該是一個開放的或取值空間很大的集合,因為每一個值都會對應一個內存中指標實例,并且不會被釋放。例如 HTTP Method 是一個很好的 Label,因為它只可能是 GET / POST / PUT / DELETE 等;而 Client Address 則很多情況下并不適合作為 Label,因為它是一個開放的集合,或者有非常巨大的取值空間,如果將它作為 Label 很可能會有容易 OOM 的風險。這個風險在?Prometheus 官方文檔中也明確指出了。
整型指標(Integer Metric)
在講解 Counter / Gauge 的實現時我們提到,rust-prometheus?使用 CAS 操作實現?AtomicF64?中的原子遞增和遞減,如果改用 atomic fetch-and-add 操作則一般可以取得更高效率。考慮到大部分情況下指標都可以是整數而不需要是小數,例如對于簡單的次數計數器來說它只可能是整數,因此?rust-prometheus?額外地提供了整型指標,允許用戶自由地選擇,針對整數指標情況提供更高的效率。
為了增強代碼的復用,rust-prometheus?實際上采用了泛型來實現?Counter?和?Gauge。通過對不同的 Atomic(如?AtomicF64、AtomicI64)進行泛化,就可以采用同一份代碼實現整數的指標和(傳統的)浮點數指標。
Atomic?trait 定義如下(src/atomic64/mod.rs):
pub trait Atomic: Send + Sync {/// The numeric type associated with this atomic.type T: Number;/// Create a new atomic value.fn new(val: Self::T) -> Self;/// Set the value to the provided value.fn set(&self, val: Self::T);/// Get the value.fn get(&self) -> Self::T;/// Increment the value by a given amount.fn inc_by(&self, delta: Self::T);/// Decrement the value by a given amount.fn dec_by(&self, delta: Self::T);
}
原生的?AtomicU64、AtomicI64?及我們自行實現的?AtomicF64?都實現了?Atomic?trait。進而,Counter?和?Gauge?都可以利用上?Atomic?trait:
pub struct Value {pub val: P,// Some fields are omitted.
}pub struct GenericCounter {
v: Arc<Value<P>>,
}pub type Counter = GenericCounter;pub type IntCounter = GenericCounter;
本地指標(Local Metrics)
由前面這些源碼解析可以知道,指標內部的實現是原子變量,用于支持線程安全的并發更新,但這在需要頻繁更新指標的場景下相比簡單地更新本地變量仍然具有顯著的開銷(大約有 10 倍的差距)。為了進一步優化、支持高效率的指標更新操作,rust-prometheus?提供了 Local Metrics 功能。
rust-prometheus 中 Counter 和 Histogram 指標支持?local()?函數,該函數會返回一個該指標的本地實例。本地實例是一個非線程安全的實例,不能多個線程共享。例如,Histogram::local()?會返回?LocalHistogram。由于 Local Metrics 使用是本地變量,開銷極小,因此可以放心地頻繁更新 Local Metrics。用戶只需定期調用 Local Metrics 的?flush()?函數將其數據定期同步到全局指標即可。一般來說 Prometheus 收集數據的間隔是 15s 到 1 分鐘左右(由用戶自行配置),因此即使是以 1s 為間隔進行?flush()?精度也足夠了。
普通的全局指標使用流程如下圖所示,多個線程直接利用原子操作更新全局指標:
本地指標使用流程如下圖所示,每個要用到該指標的線程都保存一份本地指標。更新本地指標操作開銷很小,可以在頻繁的操作中使用。隨后,只需再定期將這個本地指標 flush 到全局指標,就能使得指標的更新操作真正生效。
TiKV 中大量運用了本地指標提升性能。例如,TiKV 的線程池一般都提供?Context?變量,Context?中存儲了本地指標。線程池上運行的任務都能訪問到一個和當前 worker thread 綁定的?Context,因此它們都可以安全地更新?Context?中的這些本地指標。最后,線程池一般提供?tick()?函數,允許以一定間隔觸發任務,在?tick()?中 TiKV 會對這些?Context?中的本地指標進行?flush()。
Local Counter
Counter?的本地指標?LocalCounter?實現很簡單,它是一個包含了計數器的結構體,該結構體提供了與?Counter?一致的接口方便用戶使用。該結構體額外提供了?flush(),將保存的計數器的值作為增量值更新到全局指標:
pub struct GenericLocalCounter {
counter: GenericCounter<P>,
val: P::T,
}pub type LocalCounter = GenericLocalCounter;pub type LocalIntCounter = GenericLocalCounter;implGenericLocalCounter
{// Some functions are omitted.pub fn flush(&mut self) {if self.val == P::T::from_i64(0) {return;
}self.counter.inc_by(self.val);self.val = P::T::from_i64(0);
}
}
Local Histogram
由于?Histogram?本質也是對各種計數器進行累加操作,因此?LocalHistogram?的實現也很類似,例如?observe(x)?的實現與?Histogram?如出一轍,除了它不是原子操作;flush()?也是將所有值累加到全局指標上去:
pub struct LocalHistogramCore {
histogram: Histogram,
counts: Vec,
count: u64,
sum: f64,
}impl LocalHistogramCore {// Some functions are omitted.pub fn observe(&mut self, v: f64) {// Try find the bucket.let mut iter = self
.histogram
.core
.upper_bounds
.iter()
.enumerate()
.filter(|&(_, f)| v <= *f);if let Some((i, _)) = iter.next() {self.counts[i] += 1;
}self.count += 1;self.sum += v;
}pub fn flush(&mut self) {// No cached metric, return.if self.count == 0 {return;
}
{let h = &self.histogram;for (i, v) in self.counts.iter().enumerate() {if *v > 0 {
h.core.counts[i].inc_by(*v);
}
}
h.core.count.inc_by(self.count);
h.core.sum.inc_by(self.sum);
}self.clear();
}
}
靜態指標(Static Metrics)
之前解釋過,對于 Metric Vector 來說,由于每一個 Label Values 取值都是獨立的指標實例,因此為了線程安全實現上采用了 HashMap + RwLock。為了提升效率,可以將?with_label_values?訪問獲得的指標保存下來,以后直接訪問。另外使用姿勢正確的話,Label Values 取值是一個有限的、確定的、小的集合,甚至大多數情況下在編譯期就知道取值內容(例如 HTTP Method)。綜上,我們可以直接寫代碼將各種已知的 Label Values 提前保存下來,之后可以以靜態的方式訪問,這就是靜態指標。
以 TiKV 為例,有 Contributor 為 TiKV 提過這個 PR:#2765 server: precreate some labal metrics。這個 PR 改進了 TiKV 中統計各種 gRPC 接口消息次數的指標,由于 gRPC 接口是固定的、已知的,因此可以提前將它們緩存起來:
struct Metrics {
kv_get: Histogram,
kv_scan: Histogram,
kv_prewrite: Histogram,
kv_commit: Histogram,// ...
}impl Metrics {fn new() -> Metrics {
Metrics {
kv_get: GRPC_MSG_HISTOGRAM_VEC.with_label_values(&["kv_get"]),
kv_scan: GRPC_MSG_HISTOGRAM_VEC.with_label_values(&["kv_scan"]),
kv_prewrite: GRPC_MSG_HISTOGRAM_VEC.with_label_values(&["kv_prewrite"]),
kv_commit: GRPC_MSG_HISTOGRAM_VEC.with_label_values(&["kv_commit"]),// ...
}
}
}
使用的時候也很簡單,直接訪問即可:
@@ -102,10 +155,8 @@ fn make_callback() -> (Box, oneshot:
impl tikvpb_grpc::Tikv for Service {
fn kv_get(&self, ctx: RpcContext, mut req: GetRequest, sink: UnarySink) {- let label = "kv_get";- let timer = GRPC_MSG_HISTOGRAM_VEC- .with_label_values(&[label])- .start_coarse_timer();+ const LABEL: &str = "kv_get";+ let timer = self.metrics.kv_get.start_coarse_timer();
let (cb, future) = make_callback();
let res = self.storage.async_get(
這樣一個簡單的優化可以為 TiKV 提升 7% 的 Raw Get 效率,可以說是很超值了(主要原因是 Raw Get 本身開銷極小,因此在指標上花費的時間就顯得有一些顯著了)。但這個優化方案其實還有一些問題:
1. 代碼繁瑣,有大量重復的、或滿足某些 pattern 的代碼;
2. 如果還有另一個 Label 維度,那么需要維護的字段數量就會急劇膨脹(因為每一種值的組合都需要分配一個字段)。
為了解決以上兩個問題,rust-prometheus?提供了?Static Metric 宏。例如對于剛才的 TiKV 改進 PR #2765 來說,使用 Static Metric 宏可以簡化為:
make_static_metric! {pub struct GrpcMsgHistogram: Histogram {"type" => {
kv_get,
kv_scan,
kv_prewrite,
kv_commit,// ...
},
}
}let metrics = GrpcMsgHistogram::from(GRPC_MSG_HISTOGRAM_VEC);// Usage:
metrics.kv_get.start_coarse_timer();
可以看到,使用宏之后,需要維護的繁瑣的代碼量大大減少了。這個宏也能正常地支持多個 Label 同時存在的情況。
限于篇幅,這里就不具體講解這個宏是如何寫的了,感興趣的同學可以觀看我司同學最近在 FOSDEM 2019 上的技術分享視頻(進度條 19:54 開始介紹 Static Metrics)和?Slide,里面詳細地介紹了如何從零開始寫出一個這樣的宏(的簡化版本)。
?TiKV 源碼解析系列文章?
(一)序
(二)raft-rs proposal 示例情景分析
(三)Prometheus(上)
??關注 TiKV 的?Rust 語言愛好者們注意啦!??
第一屆 RustCon Asia 將于 4 月 20 日在北京舉辦,目前報名通道已經開放。大會為期 4 天,包括 20 日全天和 21 日上午的主題演講以及 22-23 日的多個主題?workshop 環節。
大會官網:
https://rustcon.asia/
報名參會:
http://www.huodongxing.com/event/6479456003900
了解【更多大會信息】
總結
以上是生活随笔為你收集整理的prometheus变量_TiKV 源码解析系列文章(四)Prometheus(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 做游戏开发需要学什么
- 下一篇: 3nm贵出天际 多家客户推迟订单 台积电