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

基礎(chǔ)知識(shí)

指標(biāo)類(lèi)別

Prometheus?支持四種指標(biāo):Counter、Gauge、Histogram、Summary。rust-prometheus?庫(kù)目前還只實(shí)現(xiàn)了前三種。TiKV 大部分指標(biāo)都是 Counter 和 Histogram,少部分是 Gauge。

Counter

Counter?是最簡(jiǎn)單、常用的指標(biāo),適用于各種計(jì)數(shù)、累計(jì)的指標(biāo),要求單調(diào)遞增。Counter 指標(biāo)提供基本的?inc()?或?inc_by(x)?接口,代表增加計(jì)數(shù)值。

在可視化的時(shí)候,此類(lèi)指標(biāo)一般會(huì)展示為各個(gè)時(shí)間內(nèi)增加了多少,而不是各個(gè)時(shí)間計(jì)數(shù)器值是多少。例如 TiKV 收到的請(qǐng)求數(shù)量就是一種 Counter 指標(biāo),在監(jiān)控上展示為 TiKV 每時(shí)每刻收到的請(qǐng)求數(shù)量圖表(QPS)。

Gauge

Gauge?適用于上下波動(dòng)的指標(biāo)。Gauge 指標(biāo)提供?inc()、dec()add(x)、sub(x)?和?set(x)?接口,都是用于更新指標(biāo)值。

這類(lèi)指標(biāo)可視化的時(shí)候,一般就是直接按照時(shí)間展示它的值,從而展示出這個(gè)指標(biāo)按時(shí)間是如何變化的。例如 TiKV 占用的 CPU 率是一種 Gauge 指標(biāo),在監(jiān)控上所展示的直接就是 CPU 率的上下波動(dòng)圖表。

Histogram

Histogram?即直方圖,是一種相對(duì)復(fù)雜但同時(shí)也很強(qiáng)大的指標(biāo)。Histogram 除了基本的計(jì)數(shù)以外,還能計(jì)算分位數(shù)。Histogram 指標(biāo)提供?observe(x)?接口,代表觀測(cè)到了某個(gè)值。

舉例來(lái)說(shuō),TiKV 收到請(qǐng)求后處理的耗時(shí)就是一種 Histogram 指標(biāo),通過(guò) Histogram 類(lèi)型指標(biāo),監(jiān)控上可以觀察 99%、99.9%、平均請(qǐng)求耗時(shí)等。這里顯然不能用一個(gè) Counter 存儲(chǔ)耗時(shí)指標(biāo),否則展示出來(lái)的只是每時(shí)每刻中 TiKV 一共花了多久處理,而非單個(gè)請(qǐng)求處理的耗時(shí)情況。當(dāng)然,機(jī)智的你可能想到了可以另外開(kāi)一個(gè) Counter 存儲(chǔ)請(qǐng)求數(shù)量指標(biāo),這樣累計(jì)請(qǐng)求處理時(shí)間除以請(qǐng)求數(shù)量就是各個(gè)時(shí)刻平均請(qǐng)求耗時(shí)了。

實(shí)際上,這也正是 Prometheus 中 Histogram 的內(nèi)部工作原理。Histogram 指標(biāo)實(shí)際上最終會(huì)提供一系列時(shí)序數(shù)據(jù):

觀測(cè)值落在各個(gè)桶(bucket)上的累計(jì)數(shù)量,如落在?(-∞, 0.1]、(-∞, 0.2]、(-∞, 0.4]、(-∞, 0.8]、(-∞, 1.6]、(-∞, +∞)?各個(gè)區(qū)間上的數(shù)量。

觀測(cè)值的累積和。

觀測(cè)值的個(gè)數(shù)。

bucket 是 Prometheus 對(duì)于 Histogram 觀測(cè)值的一種簡(jiǎn)化處理方式。Prometheus 并不會(huì)具體記錄下每個(gè)觀測(cè)值,而是只記錄落在配置的各個(gè) bucket 區(qū)間上的觀測(cè)值的數(shù)量,這樣以犧牲一部分精度的代價(jià)大大提高了效率。

Summary

Summary?與?Histogram?類(lèi)似,針對(duì)觀測(cè)值進(jìn)行采樣,但分位數(shù)是在客戶(hù)端進(jìn)行計(jì)算。該類(lèi)型的指標(biāo)目前在?rust-prometheus?中沒(méi)有實(shí)現(xiàn),因此這里不作進(jìn)一步詳細(xì)介紹。大家可以閱讀 Prometheus 官方文檔中的介紹了解詳細(xì)情況。感興趣的同學(xué)也可以參考其他語(yǔ)言 Client Library 的實(shí)現(xiàn)為?rust-prometheus?貢獻(xiàn)代碼。

標(biāo)簽

Prometheus 的每個(gè)指標(biāo)支持定義和指定若干組標(biāo)簽(Label),指標(biāo)的每個(gè)標(biāo)簽值獨(dú)立計(jì)數(shù),表現(xiàn)了指標(biāo)的不同維度。例如,對(duì)于一個(gè)統(tǒng)計(jì) HTTP 服務(wù)請(qǐng)求耗時(shí)的 Histogram 指標(biāo)來(lái)說(shuō),可以定義并指定諸如 HTTP Method(GET / POST / PUT / ...)、服務(wù) URL、客戶(hù)端 IP 等標(biāo)簽。這樣可以輕易滿(mǎn)足以下類(lèi)型的查詢(xún):

查詢(xún) Method 分別為 POST、PUT、GET 的 99.9% 耗時(shí)(利用單一 Label)

查詢(xún) POST /api 的平均耗時(shí)(利用多個(gè) Label 組合)

普通的查詢(xún)諸如所有請(qǐng)求 99.9% 耗時(shí)也能正常工作。

需要注意的是,不同標(biāo)簽值都是一個(gè)獨(dú)立計(jì)數(shù)的時(shí)間序列,因此應(yīng)當(dāng)避免標(biāo)簽值或標(biāo)簽數(shù)量過(guò)多,否則實(shí)際上客戶(hù)端會(huì)向 Prometheus 服務(wù)端傳遞大量指標(biāo),影響效率。

與 Prometheus?Golang client?類(lèi)似,在?rust-prometheus?中,具有標(biāo)簽的指標(biāo)被稱(chēng)為 Metric Vector。例如 Histogram 指標(biāo)對(duì)應(yīng)的數(shù)據(jù)類(lèi)型是?Histogram,而具有標(biāo)簽的 Histogram 指標(biāo)對(duì)應(yīng)的數(shù)據(jù)類(lèi)型是?HistogramVec。對(duì)于一個(gè)?HistogramVec,提供它的各個(gè)標(biāo)簽取值后,可獲得一個(gè)?Histogram?實(shí)例。不同標(biāo)簽取值會(huì)獲得不同的?Histogram?實(shí)例,各個(gè)?Histogram?實(shí)例獨(dú)立計(jì)數(shù)。

基本用法

本節(jié)主要介紹如何在項(xiàng)目中使用?rust-prometheus?進(jìn)行各種指標(biāo)收集。使用基本分為三步:

定義想要收集的指標(biāo)。

在代碼特定位置調(diào)用指標(biāo)提供的接口收集記錄指標(biāo)值。

實(shí)現(xiàn) HTTP Pull Service 使得 Prometheus 可以定期訪問(wèn)收集到的指標(biāo),或使用 rust-prometheus 提供的 Push 功能定期將收集到的指標(biāo)上傳到?Pushgateway。

注意,以下樣例代碼都是基于本文發(fā)布時(shí)最新的 rust-prometheus 0.5 版本 API。我們目前正在設(shè)計(jì)并實(shí)現(xiàn) 1.0 版本,使用上會(huì)進(jìn)一步簡(jiǎn)化,但以下樣例代碼可能在 1.0 版本發(fā)布后過(guò)時(shí)、不再工作,屆時(shí)請(qǐng)讀者參考最新的文檔。

定義指標(biāo)

為了簡(jiǎn)化使用,一般將指標(biāo)聲明為一個(gè)全局可訪問(wèn)的變量,從而能在代碼各處自由地操縱它。rust-prometheus 提供的各個(gè)指標(biāo)(包括 Metric Vector)都滿(mǎn)足?Send + Sync,可以被安全地全局共享。

以下樣例代碼借助?lazy_static?庫(kù)定義了一個(gè)全局的 Histogram 指標(biāo),該指標(biāo)代表 HTTP 請(qǐng)求耗時(shí),并且具有一個(gè)標(biāo)簽名為?method:

#[macro_use]externcrate prometheus;lazy_static! {staticrefREQUEST_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();}

記錄指標(biāo)值

有了一個(gè)全局可訪問(wèn)的指標(biāo)變量后,就可以在代碼中通過(guò)它提供的接口記錄指標(biāo)值了。在“基礎(chǔ)知識(shí)”中介紹過(guò),Histogram?最主要的接口是?observe(x),可以記錄一個(gè)觀測(cè)值。若想了解?Histogram?其他接口或其他類(lèi)型指標(biāo)提供的接口,可以參閱?rust-prometheus 文檔。

以下樣例在上段代碼基礎(chǔ)上展示了如何記錄指標(biāo)值。代碼模擬了一些隨機(jī)值用作指標(biāo),裝作是用戶(hù)產(chǎn)生的。在實(shí)際程序中,這些當(dāng)然得改成真實(shí)數(shù)據(jù) :)

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

到目前為止,代碼還僅僅是將指標(biāo)記錄了下來(lái)。最后還需要讓 Prometheus 服務(wù)端能獲取到記錄下來(lái)的指標(biāo)數(shù)據(jù)。這里一般有兩種方式,分別是 Push 和 Pull。

Pull 是 Prometheus 標(biāo)準(zhǔn)的獲取指標(biāo)方式,Prometheus Server 通過(guò)定期訪問(wèn)應(yīng)用程序提供的 HTTP 接口獲取指標(biāo)數(shù)據(jù)。

Push 是基于 Prometheus?Pushgateway?服務(wù)提供的另一種獲取指標(biāo)方式,指標(biāo)數(shù)據(jù)由應(yīng)用程序主動(dòng)定期推送給?Pushgateway,然后 Prometheus 再定期從 Pushgateway 獲取。這種方式主要適用于應(yīng)用程序不方便開(kāi)端口或應(yīng)用程序生命周期比較短的場(chǎng)景。

以下樣例代碼基于?hyper?HTTP 庫(kù)實(shí)現(xiàn)了一個(gè)可以供 Prometheus Server pull 指標(biāo)數(shù)據(jù)的接口,核心是使用?rust-prometheus?提供的?TextEncoder?將所有指標(biāo)數(shù)據(jù)序列化供 Prometheus 解析:

fn metric_service(_req:Request) -> Response {? 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()}

對(duì)于如何使用 Push 感興趣的同學(xué)可以自行參考 rust-prometheus 代碼內(nèi)提供的?Push 示例,這里限于篇幅就不詳細(xì)介紹了。

內(nèi)部實(shí)現(xiàn)

以下內(nèi)部實(shí)現(xiàn)都基于本文發(fā)布時(shí)最新的 rust-prometheus 0.5 版本代碼,該版本主干 API 的設(shè)計(jì)和實(shí)現(xiàn) port 自 Prometheus?Golang client,但為 Rust 的使用習(xí)慣進(jìn)行了一些修改,因此接口上與 Golang client 比較接近。

目前我們正在開(kāi)發(fā) 1.0 版本,API 設(shè)計(jì)上不再主要參考 Golang client,而是力求提供對(duì) Rust 使用者最友好、簡(jiǎn)潔的 API。實(shí)現(xiàn)上為了效率考慮也會(huì)和這里講解的略微有一些出入,且會(huì)去除一些目前已被拋棄的特性支持,簡(jiǎn)化實(shí)現(xiàn),因此請(qǐng)讀者注意甄別。

Counter / Gauge

Counter 與 Gauge 是非常簡(jiǎn)單的指標(biāo),只要支持線程安全的數(shù)值更新即可。讀者可以簡(jiǎn)單地認(rèn)為 Counter 和 Gauge 的核心實(shí)現(xiàn)都是?Arc<Atomic>。但由于 Prometheus 官方規(guī)定指標(biāo)數(shù)值需要支持浮點(diǎn)數(shù),因此我們基于?std::sync::atomic::AtomicU64?和 CAS 操作實(shí)現(xiàn)了?AtomicF64,其具體實(shí)現(xiàn)位于?src/atomic64/nightly.rs。核心片段如下:

impl AtomicforAtomicF64 {? type T = f64;// Some functions are omitted.fn inc_by(&self, delta:Self::T) {? ? ? loop {? ? ? ? ? let current =self.inner.load(Ordering::Acquire);? ? ? ? ? letnew= u64_to_f64(current) + delta;? ? ? ? ? let swapped =self.inner? ? ? ? ? ? ? .compare_and_swap(current, f64_to_u64(new), Ordering::Release);ifswapped == current {return;? ? ? ? ? }? ? ? }? }}

另外由于 0.5 版本發(fā)布時(shí)?AtomicU64?仍然是一個(gè) nightly 特性,因此為了支持 Stable Rust,我們還基于自旋鎖提供了?AtomicF64?的 fallback,位于?src/atomic64/fallback.rs

注:AtomicU64?所需的?integer_atomics?特性最近已在 rustc 1.34.0 stabilize。我們將在 rustc 1.34.0 發(fā)布后為 Stable Rust 也使用上原生的原子操作從而提高效率。

Histogram

根據(jù) Prometheus 的要求,Histogram 需要進(jìn)行的操作是在獲得一個(gè)觀測(cè)值以后,為觀測(cè)值處在的桶增加計(jì)數(shù)值。另外還有總觀測(cè)值、觀測(cè)值數(shù)量需要累加。

注意,Prometheus 中的 Histogram 是累積直方圖,其每個(gè)桶的含義是?(-∞, x],因此對(duì)于每個(gè)觀測(cè)值都可能要更新多個(gè)連續(xù)的桶。例如,假設(shè)用戶(hù)定義了 5 個(gè)桶邊界,分別是 0.1、0.2、0.4、0.8、1.6,則每個(gè)桶對(duì)應(yīng)的數(shù)值范圍是?(-∞, 0.1]、(-∞, 0.2]、(-∞, 0.4]、(-∞, 0.8]、(-∞, 1.6]、(-∞, +∞),對(duì)于觀測(cè)值 0.4 來(lái)說(shuō)需要更新(-∞, 0.4]、(-∞, 0.8]、(-∞, 1.6]、(-∞, +∞)?四個(gè)桶。

一般來(lái)說(shuō)?observe(x)?會(huì)被頻繁地調(diào)用,而將收集到的數(shù)據(jù)反饋給 Prometheus 則是個(gè)相對(duì)很低頻率的操作,因此用數(shù)組實(shí)現(xiàn)“桶”的時(shí)候,我們并不將各個(gè)桶與數(shù)組元素直接對(duì)應(yīng),而將數(shù)組元素定義為非累積的桶,如?(-∞, 0.1)、[0.1, 0.2)、[0.2, 0.4)、[0.4, 0.8)、[0.8, 1.6)、[1.6, +∞),這樣就大大減少了需要頻繁更新的數(shù)據(jù)量;最后在上報(bào)數(shù)據(jù)給 Prometheus 的時(shí)候?qū)?shù)組元素累積,得到累積直方圖,這樣就得到了 Prometheus 所需要的桶的數(shù)據(jù)。

當(dāng)然,由此可見(jiàn),如果給定的觀測(cè)值超出了桶的范圍,則最終記錄下的最大值只有桶的上界了,然而這并不是實(shí)際的最大值,因此使用的時(shí)候需要多加注意。

Histogram?的核心實(shí)現(xiàn)見(jiàn)?src/histogram.rs

pubstructHistogramCore {// Some fields are omitted.sum: AtomicF64,? count: AtomicU64,? upper_bounds: Vec,? counts: Vec,}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);iflet Some((i, _)) = iter.next() {self.counts[i].inc_by(1);? ? ? }self.count.inc_by(1);self.sum.inc_by(v);? }}#[derive(Clone)]pubstructHistogram {? core: Arc,}

Histogram?還提供了一個(gè)輔助結(jié)構(gòu)?HistogramTimer,它會(huì)記錄從它創(chuàng)建直到被 Drop 的時(shí)候的耗時(shí),將這個(gè)耗時(shí)作為?Histogram::observe()?接口的觀測(cè)值記錄下來(lái),這樣很多時(shí)候在想要記錄 Duration / Elapsed Time 的場(chǎng)景中,就可以使用這個(gè)簡(jiǎn)便的結(jié)構(gòu)來(lái)記錄時(shí)間:

#[must_use]pubstructHistogramTimer {? histogram: Histogram,? start: Instant,}impl HistogramTimer {// Some functions are omitted.pub fn observe_duration(self) {? ? ? drop(self);? }? fn observe(&mutself) {? ? ? let v = duration_to_seconds(self.start.elapsed());self.histogram.observe(v)? }}impl DropforHistogramTimer {? fn drop(&mutself) {self.observe();? }}

HistogramTimer?被標(biāo)記為了?must_use,原因很簡(jiǎn)單,作為一個(gè)記錄流逝時(shí)間的結(jié)構(gòu),它應(yīng)該被存在某個(gè)變量里,從而記錄這個(gè)變量所處作用域的耗時(shí)(或稍后直接調(diào)用相關(guān)函數(shù)提前記錄耗時(shí)),而不應(yīng)該作為一個(gè)未使用的臨時(shí)變量被立即 Drop。標(biāo)記為?must_use?可以在編譯期杜絕這種明顯的使用錯(cuò)誤。

感興趣的可以自己來(lái)我的Java架構(gòu)群,可以獲取免費(fèi)的學(xué)習(xí)資料,群號(hào):798891710對(duì)Java技術(shù),架構(gòu)技術(shù)感興趣的同學(xué),歡迎加群,一起學(xué)習(xí),相互討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容