TiKV 源碼解析系列文章(四)Prometheus(下)

做者: Breezewishhtml

本文爲 TiKV 源碼解析系列的第四篇,接上篇繼續爲你們介紹 rust-prometheus上篇 主要介紹了基礎知識以及最基本的幾個指標的內部工做機制,本篇會進一步介紹更多高級功能的實現原理。git

與上篇同樣,如下內部實現都基於本文發佈時最新的 rust-prometheus 0.5 版本代碼,目前咱們正在開發 1.0 版本,API 設計上會進行一些簡化,實現上出於效率考慮也會和這裏講解的略微有一些出入,所以請讀者注意甄別。github

指標向量(Metric Vector)

Metric Vector 用於支持帶 Label 的指標。因爲各類指標均可以帶上 Label,所以 Metric Vector 自己實現爲了一種泛型結構體,CounterGaugeHistogram 在這之上實現了 CounterVecGaugeVecHistogramVec。Metric Vector 主要實現位於 src/vec.rs緩存

HistogramVec 爲例,調用 HistogramVec::with_label_values 可得到一個 Histogram 實例,而 HistogramVec 定義爲:安全

pub type HistogramVec = MetricVec<HistogramVecBuilder>;

pub struct MetricVec<T: MetricVecBuilder> {
   pub(crate) v: Arc<MetricVecCore<T>>,
}

impl<T: MetricVecBuilder> MetricVec<T> {
   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<T> 後再提供給用戶。進一步能夠觀察 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<T: MetricVecBuilder> {
   pub children: RwLock<HashMap<u64, T::M>>,
   // Some fields are omitted.
}

impl<T: MetricVecBuilder> MetricVecCore<T> {
   // Some functions are omitted.

   pub fn get_metric_with_label_values(&self, vals: &[&str]) -> Result<T::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<T::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<HashMap<u64, T::M>> 中查找,若是找到了,說明給定的這個 Label Values 以前已經出現過、相應的 Metric 指標結構體已經初始化過,所以直接返回對應的實例;若是不存在,則要利用給定的 MetricVecBuilder 構造新的指標加入哈希表,並返回這個新的指標。併發

由上述代碼可見,爲了在線程安全的條件下實現 Metric Vector 各個 Label Values 具備獨立的時間序列,Metric Vector 內部採用了 RwLock 進行同步,也就是說 with_label_values() 及相似函數內部是具備鎖的。這在多線程環境下會有必定的效率影響,不過由於大部分狀況下都是讀鎖,所以影響不大。固然,還能夠發現其實給定 Label Values 以後調用 with_label_values() 獲得的指標實例是能夠被緩存起來的,只訪問緩存起來的這個指標實例是不會有任何同步開銷的,也繞開了計算哈希值等比較佔 CPU 的操做。基於這個思想,就有了 Static Metrics,讀者能夠在本文的後半部分了解 Static Metrics 的詳細狀況。async

另外讀者也能夠發現,Label Values 的取值應當是一個有限的、封閉的小集合,不該該是一個開放的或取值空間很大的集合,由於每個值都會對應一個內存中指標實例,而且不會被釋放。例如 HTTP Method 是一個很好的 Label,由於它只多是 GET / POST / PUT / DELETE 等;而 Client Address 則不少狀況下並不適合做爲 Label,由於它是一個開放的集合,或者有很是巨大的取值空間,若是將它做爲 Label 極可能會有容易 OOM 的風險。這個風險在 Prometheus 官方文檔中也明確指出了。ide

整型指標(Integer Metric)

在講解 Counter / Gauge 的實現時咱們提到,rust-prometheus 使用 CAS 操做實現 AtomicF64 中的原子遞增和遞減,若是改用 atomic fetch-and-add 操做則通常能夠取得更高效率。考慮到大部分狀況下指標均可以是整數而不須要是小數,例如對於簡單的次數計數器來講它只多是整數,所以 rust-prometheus 額外地提供了整型指標,容許用戶自由地選擇,針對整數指標狀況提供更高的效率。函數

爲了加強代碼的複用,rust-prometheus 實際上採用了泛型來實現 CounterGauge。經過對不一樣的 Atomic(如 AtomicF64AtomicI64)進行泛化,就能夠採用同一份代碼實現整數的指標和(傳統的)浮點數指標。

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);
}

原生的 AtomicU64AtomicI64 及咱們自行實現的 AtomicF64 都實現了 Atomic trait。進而,CounterGauge 均可以利用上 Atomic trait:

pub struct Value<P: Atomic> {
   pub val: P,
   // Some fields are omitted.
}

pub struct GenericCounter<P: Atomic> {
   v: Arc<Value<P>>,
}

pub type Counter = GenericCounter<AtomicF64>;
pub type IntCounter = GenericCounter<AtomicI64>;

本地指標(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() 精度也足夠了。

普通的全局指標使用流程以下圖所示,多個線程直接利用原子操做更新全局指標:

normal_metrics

本地指標使用流程以下圖所示,每一個要用到該指標的線程都保存一份本地指標。更新本地指標操做開銷很小,能夠在頻繁的操做中使用。隨後,只需再按期將這個本地指標 flush 到全局指標,就能使得指標的更新操做真正生效。

local_metrics

TiKV 中大量運用了本地指標提高性能。例如,TiKV 的線程池通常都提供 Context 變量,Context 中存儲了本地指標。線程池上運行的任務都能訪問到一個和當前 worker thread 綁定的 Context,所以它們均可以安全地更新 Context 中的這些本地指標。最後,線程池通常提供 tick() 函數,容許以必定間隔觸發任務,tick() 中 TiKV 會對這些 Context 中的本地指標進行 flush()

Local Counter

Counter 的本地指標 LocalCounter 實現很簡單,它是一個包含了計數器的結構體,該結構體提供了與 Counter 一致的接口方便用戶使用。該結構體額外提供了 flush(),將保存的計數器的值做爲增量值更新到全局指標:

pub struct GenericLocalCounter<P: Atomic> {
   counter: GenericCounter<P>,
   val: P::T,
}

pub type LocalCounter = GenericLocalCounter<AtomicF64>;
pub type LocalIntCounter = GenericLocalCounter<AtomicI64>;

impl<P: Atomic> GenericLocalCounter<P> {
   // 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<u64>,
   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<T: Debug + Send + 'static>() -> (Box<FnBox(T) + Send>, oneshot:

impl<T: RaftStoreRouter + 'static> tikvpb_grpc::Tikv for Service<T> {
    fn kv_get(&self, ctx: RpcContext, mut req: GetRequest, sink: UnarySink<GetResponse>) {
-        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,裏面詳細地介紹瞭如何從零開始寫出一個這樣的宏(的簡化版本)。

相關文章
相關標籤/搜索