做者: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 指標提供基本的 inc()
或 inc_by(x)
接口,表明增長計數值。數組
在可視化的時候,此類指標通常會展現爲各個時間內增長了多少,而不是各個時間計數器值是多少。例如 TiKV 收到的請求數量就是一種 Counter 指標,在監控上展現爲 TiKV 每時每刻收到的請求數量圖表(QPS)。安全
Gauge 適用於上下波動的指標。Gauge 指標提供 inc()
、dec()
、add(x)
、sub(x)
和 set(x)
接口,都是用於更新指標值。app
這類指標可視化的時候,通常就是直接按照時間展現它的值,從而展現出這個指標按時間是如何變化的。例如 TiKV 佔用的 CPU 率是一種 Gauge 指標,在監控上所展現的直接就是 CPU 率的上下波動圖表。函數
Histogram 即直方圖,是一種相對複雜但同時也很強大的指標。Histogram 除了基本的計數之外,還能計算分位數。Histogram 指標提供 observe(x)
接口,表明觀測到了某個值。工具
舉例來講,TiKV 收到請求後處理的耗時就是一種 Histogram 指標,經過 Histogram 類型指標,監控上能夠觀察 99%、99.9%、平均請求耗時等。這裏顯然不能用一個 Counter 存儲耗時指標,不然展現出來的只是每時每刻中 TiKV 一共花了多久處理,而非單個請求處理的耗時狀況。固然,機智的你可能想到了能夠另外開一個 Counter 存儲請求數量指標,這樣累計請求處理時間除以請求數量就是各個時刻平均請求耗時了。
實際上,這也正是 Prometheus 中 Histogram 的內部工做原理。Histogram 指標實際上最終會提供一系列時序數據:
(-∞, 0.1]
、(-∞, 0.2]
、(-∞, 0.4]
、(-∞, 0.8]
、(-∞, 1.6]
、(-∞, +∞)
各個區間上的數量。bucket 是 Prometheus 對於 Histogram 觀測值的一種簡化處理方式。Prometheus 並不會具體記錄下每一個觀測值,而是隻記錄落在配置的各個 bucket 區間上的觀測值的數量,這樣以犧牲一部分精度的代價大大提升了效率。
Summary 與 Histogram 相似,針對觀測值進行採樣,但分位數是在客戶端進行計算。該類型的指標目前在 rust-prometheus 中沒有實現,所以這裏不做進一步詳細介紹。你們能夠閱讀 Prometheus 官方文檔中的介紹瞭解詳細狀況。感興趣的同窗也能夠參考其餘語言 Client Library 的實現爲 rust-prometheus 貢獻代碼。
Prometheus 的每一個指標支持定義和指定若干組標籤(Label),指標的每一個標籤值獨立計數,表現了指標的不一樣維度。例如,對於一個統計 HTTP 服務請求耗時的 Histogram 指標來講,能夠定義並指定諸如 HTTP Method(GET / POST / PUT / ...)、服務 URL、客戶端 IP 等標籤。這樣能夠輕易知足如下類型的查詢:
普通的查詢諸如全部請求 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)); } }
到目前爲止,代碼還僅僅是將指標記錄了下來。最後還須要讓 Prometheus 服務端能獲取到記錄下來的指標數據。這裏通常有兩種方式,分別是 Push 和 Pull。
如下樣例代碼基於 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 的核心實現都是 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 也使用上原生的原子操做從而提升效率。
根據 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
能夠在編譯期杜絕這種明顯的使用錯誤。