1. 程式人生 > >TiKV 原始碼解析系列文章(三)Prometheus(上)

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

開發十年,就只剩下這套架構體系了! >>>   

作者:Breezewish

本文為 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

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.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 <= *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 可以在編譯期杜絕這種明顯的使用錯誤。

相關推薦

TiKV 原始碼解析系列文章Prometheus

開發十年,就只剩下這套架構體系了! >>>   

TiKV 原始碼解析系列文章gRPC Server 的初始化和啟動流程

作者:屈鵬 本篇 TiKV 原始碼解析將為大家介紹 TiKV 的另一週邊元件—— grpc-rs。grpc-rs 是 PingCA

TiKV 原始碼解析系列文章十一Storage

作者:張金鵬 背景知識 TiKV 是一個強一致的支援事務的分散式 KV 儲存。TiKV 通過 raft 來保證多副本之間的強一致,

TiKV 原始碼解析系列 - Raft 的優化

本系列文章主要面向 TiKV 社群開發者,重點介紹 TiKV 的系統架構,原始碼結構,流程解析。目的是使得開發者閱讀之後,能對 TiKV 專案有一個初步瞭解,更好的參與進入 TiKV 的開發中。本文是本系列文章的第六章節。重點介紹 TiKV 中 Raft 的優化。 在分散式領域,為了保證資料的一致性,通常都

TiKV 原始碼解析系列——如何使用 Raft

本系列文章主要面向 TiKV 社群開發者,重點介紹 TiKV 的系統架構,原始碼結構,流程解析。目的是使得開發者閱讀之後,能對 TiKV 專案有一個初步瞭解,更好的參與進入 TiKV 的開發中。 需要注意,TiKV 使用 Rust 語言編寫,使用者需要對 Rust 語言有一個大概的瞭解。另外,本系列文章並不

TiDB 原始碼閱讀系列文章十九tikv-client

上篇文章 中,我們介紹了資料讀寫過程中 tikv-client 需要解決的幾個具體問題,本文將繼續介紹 tikv-client 裡的兩個主要的模組——負責處理分散式計算的 copIterator 和執行二階段提交的 twoPhaseCommitter。 copIterator cop

TiDB 原始碼閱讀系列文章二十Table Partition

作者:肖亮亮 Table Partition 什麼是 Table Partition Table Partition 是指根據一定規則,將資料庫中的一張表分解成多個更小的容易管理的部分。從邏輯上看只有一張表,但是底層卻是由多個物理分割槽組成。相信對有關係型資料庫使用背景的使用者來

Java原始碼解析系列ArrayList原始碼解析

備註:以下都是基於JDK8 原始碼分析 ArrayList簡介        ArrayList 是一個數組佇列,相當於 動態陣列。與Java中的陣列相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Clonea

TiDB 原始碼閱讀系列文章二十一基於規則的優化 II

在 TiDB 原始碼閱讀系列文章(七)基於規則的優化 一文中,我們介紹了幾種 TiDB 中的邏輯優化規則,包括列剪裁,最大最小消除,投影消除,謂詞下推和構建節點屬性,本篇將繼續介紹更多的優化規則:聚合消除、外連線消除和子查詢優化。 聚合消除 聚合消除會檢查 SQL 查詢中 Group By 語句所使用的列是否

TiDB 原始碼閱讀系列文章初識 TiDB 原始碼

本文為 TiDB 原始碼閱讀系列文章的第二篇,第一篇文章介紹了 TiDB 整體的架構,知道 TiDB 有哪些模組,分別是做什麼的,從哪裡入手比較好,哪些可以忽略,哪些需要仔細閱讀。 這篇文章是一篇入門文件,難度係數比較低,其中部分內容可能大家在其他渠道已經看過

Tomcat總體架構Tomcat原始碼解析系列

         Tomcat即是一個HTTP伺服器,也是一個servlet容器,主要目的就是包裝servlet,並對請求響應相應的servlet,純servlet的web應用似乎很好理解Tomcat是如何裝載servlet的,但,當使用一些MVC框架時,如spring M

TiKV 原始碼解析fail-rs 介紹

開發十年,就只剩下這套架構體系了! >>>   

DM 原始碼閱讀系列文章定製化資料同步功能的實現

作者:王相 本文為 DM 原始碼閱讀系列文章的第七篇,在 上篇文章 中我們介紹了 relay log 的實現,主要包括 relay

DM 原始碼閱讀系列文章Online Schema Change 同步支援

作者:lan 本文為 DM 原始碼閱讀系列文章的第八篇,上篇文章 對 DM 中的定製化資料同步功能進行詳細的講解,包括庫表路由(T

DM 原始碼閱讀系列文章shard DDL 與 checkpoint 機制的實現

作者:張學程 本文為 DM 原始碼閱讀系列文章的第九篇,在 上篇文章 中我們詳細介紹了 DM 對 online schema ch

DM 原始碼閱讀系列文章測試框架的實現

作者:楊非 本文為 DM 原始碼閱讀系列文章的第十篇,之前的文章已經詳細介紹過 DM 資料同步各元件的實現原理和程式碼解析,相信大

TiDB Binlog 原始碼閱讀系列文章Pump server 介紹

作者: satoru 在 上篇文章 中,我們介紹了 TiDB 如何通過 Pump client 將 binlog 發往 Pump,

Android原始碼解析之應用程式資源管理器Asset Manager的建立過程分析

轉載自:https://blog.csdn.net/luoshengyang/article/details/8791064 我們分析了Android應用程式資源的編譯和打包過程,最終得到的應用程式資源就與應用程式程式碼一起打包在一個APK檔案中。Android應用程式在執行的過程中,是通過一個

介面測試系列:工作中所用:__read_config.py檔案

import os from common import fileUtil def __read_config(): base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) settings_file

一步步實現windows版ijkplayer系列文章——Ijkplayer播放器原始碼分析之音視訊輸出——音訊篇

一步步實現windows版ijkplayer系列文章之三——Ijkplayer播放器原始碼分析之音視訊輸出——音訊篇 這篇文章的ijkplayer音訊原始碼研究我們還是選擇Android平臺,它的音訊解碼是不支援硬解的,音訊播放使用的API是OpenSL ES或AudioTrack。 OpenSL ES