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

做者:Breezewishhtml

本文爲 TiKV 源碼解析系列的第三篇,繼續爲你們介紹 TiKV 依賴的周邊庫 rust-prometheus,本篇主要介紹基礎知識以及最基本的幾個指標的內部工做機制,下篇會介紹一些高級功能的實現原理。git

rust-prometheus 是監控系統 Prometheus 的 Rust 客戶端庫,由 TiKV 團隊實現。TiKV 使用 rust-prometheus 收集各類指標(metric)到 Prometheus 中,從然後續能再利用 Grafana 等可視化工具將其展現出來做爲儀表盤監控面板。這些監控指標對於瞭解 TiKV 當前或歷史的狀態具備很是關鍵的做用。TiKV 提供了豐富的監控指標數據,而且代碼中也處處穿插了監控指標的收集片斷,所以瞭解 rust-prometheus 頗有必要。github

感興趣的小夥伴還能夠觀看我司同窗在 FOSDEM 2019 會議上關於 rust-prometheus 的技術分享golang

基礎知識

指標類別

Prometheus 支持四種指標:Counter、Gauge、Histogram、Summary。rust-prometheus 庫目前還只實現了前三種。TiKV 大部分指標都是 Counter 和 Histogram,少部分是 Gauge。api

Counter

Counter 是最簡單、經常使用的指標,適用於各類計數、累計的指標,要求單調遞增。Counter 指標提供基本的 inc()inc_by(x) 接口,表明增長計數值。數組

在可視化的時候,此類指標通常會展現爲各個時間內增長了多少,而不是各個時間計數器值是多少。例如 TiKV 收到的請求數量就是一種 Counter 指標,在監控上展現爲 TiKV 每時每刻收到的請求數量圖表(QPS)。安全

Gauge

Gauge 適用於上下波動的指標。Gauge 指標提供 inc()dec()add(x)sub(x)set(x) 接口,都是用於更新指標值。app

這類指標可視化的時候,通常就是直接按照時間展現它的值,從而展現出這個指標按時間是如何變化的。例如 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

SummaryHistogram 相似,針對觀測值進行採樣,但分位數是在客戶端進行計算。該類型的指標目前在 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 ~ 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();
   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.一、0.二、0.四、0.八、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 能夠在編譯期杜絕這種明顯的使用錯誤。

相關文章
相關標籤/搜索