摘要:指標實際上最終會提供一系列時序數據觀測值落在各個桶上的累計數量,如落在各個區間上的數量。我們將在發布后為也使用上原生的原子操作從而提高效率。例如,假設用戶定義了個桶邊界,分別是,則每個桶對應的數值范圍是,對于觀測值來說需要更新四個桶。
作者: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[Summary] 與 [Histogram] 類似,針對觀測值進行采樣,但分位數是在客戶端進行計算。該類型的指標目前在 [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] 進行各種指標收集。使用基本分為三步:
定義想要收集的指標。
在代碼特定位置調用指標提供的接口收集記錄指標值。
實現 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) -> 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() }
對于如何使用 Push 感興趣的同學可以自行參考 rust-prometheus 代碼內提供的 Push 示例,這里限于篇幅就不詳細介紹了。
上述三段樣例的完整代碼可參見這里。
內部實現以下內部實現都基于本文發布時最新的 rust-prometheus 0.5 版本代碼,該版本主干 API 的設計和實現 port 自 Prometheus [Golang client],但為 Rust 的使用習慣進行了一些修改,因此接口上與 Golang client 比較接近。
目前我們正在開發 1.0 版本,API 設計上不再主要參考 Golang client,而是力求提供對 Rust 使用者最友好、簡潔的 API。實現上為了效率考慮也會和這里講解的略微有一些出入,且會去除一些目前已被拋棄的特性支持,簡化實現,因此請讀者注意甄別。
Counter / GaugeCounter 與 Gauge 是非常簡單的指標,只要支持線程安全的數值更新即可。讀者可以簡單地認為 Counter 和 Gauge 的核心實現都是 Arc
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, 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); 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 , }
[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 可以在編譯期杜絕這種明顯的使用錯誤。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17946.html
摘要:而源碼解析系列文章則是會從源碼層面給大家抽絲剝繭,讓大家知道我們內部到底是如何實現的。我們希望通過該源碼解析系列,能讓大家對有一個更深刻的理解。 作者:唐劉 TiKV 是一個支持事務的分布式 Key-Value 數據庫,有很多社區開發者基于 TiKV 來開發自己的應用,譬如 titan、tidis。尤其是在 TiKV 成為 CNCF 的 Sandbox 項目之后,吸引了越來越多開發者的...
摘要:由上述代碼可見,為了在線程安全的條件下實現各個具有獨立的時間序列,內部采用了進行同步,也就是說及類似函數內部是具有鎖的。 作者: Breezewish 本文為 TiKV 源碼解析系列的第四篇,接上篇繼續為大家介紹 [rust-prometheus]。上篇 主要介紹了基礎知識以及最基本的幾個指標的內部工作機制,本篇會進一步介紹更多高級功能的實現原理。 與上篇一樣,以下內部實現都基于本文發...
閱讀 3128·2021-11-08 13:18
閱讀 2275·2019-08-30 15:55
閱讀 3600·2019-08-30 15:44
閱讀 3062·2019-08-30 13:07
閱讀 2772·2019-08-29 17:20
閱讀 1941·2019-08-29 13:03
閱讀 3402·2019-08-26 10:32
閱讀 3217·2019-08-26 10:15