日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Rocksdb 的优秀代码(一) -- 工业级分桶算法实现分位数p50,p99,p9999

發(fā)布時(shí)間:2023/11/27 生活经验 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Rocksdb 的优秀代码(一) -- 工业级分桶算法实现分位数p50,p99,p9999 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

    • 基本概念
    • 普通的分位數(shù)計(jì)算
    • Rocksdb中的應(yīng)用
      • rocksdb中的分桶算法結(jié)果展示
      • rocksdb 分桶算法實(shí)現(xiàn)
    • 一些總結(jié) 和 相關(guān)論文


我們知道一個(gè)完整的監(jiān)控系統(tǒng)必須存在p99/p999等分位數(shù)指標(biāo),作為系統(tǒng)可用性的評(píng)判標(biāo)準(zhǔn)之一。而像開源監(jiān)控系統(tǒng)中做的很不錯(cuò)的grafanaprometheus一定需要工業(yè)級(jí)的分位數(shù)算法。

本文中涉及到的rocksdb源代碼都是基于rocksdb 6.4.6版本

基本概念

所謂分位數(shù)(quantile),比如p99,表示percent of 99,即99% 的數(shù)據(jù)小于當(dāng)前p99的指標(biāo)。使用分位數(shù)來統(tǒng)計(jì)系統(tǒng)的長(zhǎng)尾延時(shí),也是系統(tǒng)可用性的一種衡量指標(biāo)。
由分位數(shù)的基本描述中我們能夠看到分位數(shù)的結(jié)果計(jì)算是需要有排序的過程,我們想要知道99%的指標(biāo)小于某一個(gè)值,即需要針對(duì)當(dāng)前數(shù)據(jù)集進(jìn)行從小到大的排序,取到第99%的數(shù)據(jù)才能拿到p99的指標(biāo)。

所謂工業(yè)級(jí) ,由以上針對(duì)p99的計(jì)算過程我們知道這個(gè)過程需要排序,同時(shí)肯定也需要占用存儲(chǔ)空間,當(dāng)系統(tǒng)吞吐達(dá)到數(shù)十萬級(jí)更高量級(jí) ,分位數(shù)的精確度 和 其計(jì)算過程中的空間占用 trade off 也需要重點(diǎn)關(guān)注。那么一個(gè)高性能,節(jié)省空間,以及精確度較高的分位數(shù)實(shí)現(xiàn)算法就需要仔細(xì)揣摩。

普通的分位數(shù)計(jì)算

我們常見的分位數(shù)為p50,p99,p999,p9999 ,在計(jì)算不同分位數(shù)數(shù)值的過程一般分位三步:

  • 將數(shù)據(jù)從小到大排列
  • 確定p 分位數(shù)位置
  • 確定p 分位數(shù)的具體數(shù)值

數(shù)據(jù)從小達(dá)到的排序就是我們正常的排序算法。
確定p分位數(shù)的位置,比如p99,則確定整個(gè)數(shù)組中99%的那個(gè)元素所處的位置,一般通過

pos = 1 + (n - 1) * p 

為了保證得到的是第99%個(gè)元素,將計(jì)算時(shí)的元素位置前移 并 在最后的結(jié)果+1。

確定分位數(shù)的具體數(shù)值則通過如下公式:
在數(shù)據(jù)已經(jīng)完成從小到大的排列之后,通過如下公式完成計(jì)算。
p分位數(shù)位置的值 = 位于p分位數(shù)取整后位置的值 + (位于p分位數(shù)取整下一位位置的值 - 位于p分位數(shù)取整后位置的值)*(p分位數(shù)位置 - p分位數(shù)位置取整)

Rocksdb中的應(yīng)用

作為高性能單機(jī)引擎,rocksdb內(nèi)部有數(shù)量龐大的metrics,也有對(duì)應(yīng)的分位數(shù)。在單機(jī)引擎數(shù)十萬的qps下,針對(duì)請(qǐng)求耗時(shí)的分位數(shù)計(jì)算 必須滿足工業(yè)級(jí)需求。既需要較高的性能和精確度,又需要較少的空間占用。

如果按章上文普通方式的算法完成分位數(shù)的計(jì)算:

  • 空間占用的消耗 :需要保存所有的指標(biāo)數(shù)據(jù),假如每秒鐘產(chǎn)生20w的qps,那么就需要保存20w對(duì)應(yīng)的uint64_t 的指標(biāo)數(shù)據(jù)。而且,rocksdb中指標(biāo)數(shù)十甚至上百個(gè),每個(gè)指標(biāo) 每秒都需要保存20w的指標(biāo)數(shù)據(jù),這對(duì)內(nèi)存是一個(gè)巨大的消耗。
  • 計(jì)算資源的消耗: 因?yàn)榉治粩?shù)的獲取是由用戶來指定時(shí)間的,也就是在用戶指定獲取分位數(shù)之前所有的指標(biāo)都需要累加,如果這一段累計(jì)的時(shí)間較長(zhǎng),那么獲取的時(shí)候進(jìn)行數(shù)據(jù)全排的計(jì)算代價(jià)就非常大了。

接下來我們一起看看工業(yè)級(jí)的rocksdb是如何解決以上兩方面的問題的。

rocksdb中的分桶算法結(jié)果展示

先簡(jiǎn)單展示一下rocksdb的一秒鐘level0 讀耗時(shí)的分位數(shù)統(tǒng)計(jì)結(jié)果指標(biāo):

** Level 0 read latency histogram (micros):
Count: 360578 Average: 2.8989  StdDev: 116.15
Min: 1  Median: 1.6243  Max: 38033
Percentiles: P50: 1.62 P75: 1.95 P99: 3.95 P99.9: 16.61 P99.99: 290.83
------------------------------------------------------
[       0,       1 ]     6994   1.940%   1.940%
(       1,       2 ]   277573  76.980%  78.920% ###############
(       2,       3 ]    70608  19.582%  98.502% ####
(       3,       4 ]     1884   0.522%  99.024%
(       4,       6 ]      454   0.126%  99.150%
(       6,      10 ]     2110   0.585%  99.735%
(      10,      15 ]      507   0.141%  99.876%
(      15,      22 ]      380   0.105%  99.981%
(      22,      34 ]       20   0.006%  99.987%
(      34,      51 ]        6   0.002%  99.988%
(     110,     170 ]        4   0.001%  99.989%
(     170,     250 ]        1   0.000%  99.990%
(     250,     380 ]        3   0.001%  99.991%
(     380,     580 ]        2   0.001%  99.991%
(     580,     870 ]        4   0.001%  99.992%
(     870,    1300 ]        5   0.001%  99.994%
(    1300,    1900 ]        4   0.001%  99.995%
(    1900,    2900 ]        6   0.002%  99.996%
(    2900,    4400 ]        5   0.001%  99.998%
(    9900,   14000 ]        3   0.001%  99.999%
(   14000,   22000 ]        3   0.001%  99.999%
(   22000,   33000 ]        1   0.000% 100.000%
(   33000,   50000 ]        2   0.001% 100.000%

非常清晰得看到 各個(gè)層級(jí)的分位數(shù)指標(biāo),包括p50,p75,p99,p99.9,p99.99
Percentiles之下 總共有四列(這里將做括號(hào)和右方括號(hào)算作一列,是一個(gè)hash桶)

  • 第一列 : 看作一個(gè)hash桶,這個(gè)hash桶表示一個(gè)耗時(shí)區(qū)間,單位是us
  • 第二列:一秒內(nèi)產(chǎn)生的請(qǐng)求耗時(shí)命中當(dāng)前耗時(shí)區(qū)間的有多少個(gè)
  • 第三列:一秒內(nèi)產(chǎn)生的請(qǐng)求耗時(shí)命中當(dāng)前耗時(shí)區(qū)間的個(gè)數(shù)占總請(qǐng)求個(gè)數(shù)的百分比
  • 第四列:累加之前所有請(qǐng)求的百分比

通過hash桶完整得展示了 耗時(shí)主體命中在了(1,2]us的耗時(shí)區(qū)間,占了整個(gè)耗時(shí)比例的78.9%。

以上僅僅是L0的一個(gè)分位數(shù)統(tǒng)計(jì),rocksdb為每一層都維護(hù)了類似這樣的耗時(shí)桶。同時(shí)實(shí)際測(cè)試過程中,累加每秒的耗時(shí)統(tǒng)計(jì)結(jié)果即上面的Count統(tǒng)計(jì), 和實(shí)際的每秒的qps進(jìn)行對(duì)比發(fā)現(xiàn)并沒有太大的差異,也就是這里的耗時(shí)統(tǒng)計(jì)和實(shí)際的請(qǐng)求統(tǒng)計(jì)接近,且并沒有太多的資源消耗。(打開opstions.statistics和關(guān)閉這個(gè)指標(biāo),系統(tǒng)CPU資源的消耗并沒有太大的差異),也就是說單從rocksdb實(shí)現(xiàn)的分位數(shù)算法的計(jì)算資源消耗角度來看已經(jīng)滿足工業(yè)級(jí)的標(biāo)準(zhǔn)了。

rocksdb 分桶算法實(shí)現(xiàn)

按照我們之前描述的分位數(shù)計(jì)算的三步來看rocksdb的代碼

  • 將數(shù)據(jù)從小到達(dá)排列
  • 確定p 分位數(shù)位置
  • 計(jì)算p 分位數(shù)的值

第一步,數(shù)據(jù)從小到大進(jìn)行排列。在rocksdb中,我們打開statistics選項(xiàng)之后相關(guān)的耗時(shí)指標(biāo)會(huì)動(dòng)態(tài)增加,也就是分位數(shù)計(jì)算的數(shù)據(jù)集是在不斷增加且動(dòng)態(tài)變化的。

rocksdb的數(shù)據(jù)集中元素的增加邏輯如下:

void HistogramStat::Add(uint64_t value) {// This function is designed to be lock free, as it's in the critical path// of any operation. Each individual value is atomic and the order of updates// by concurrent threads is tolerable.const size_t index = bucketMapper.IndexForValue(value); // 通過hash桶計(jì)算value所處的 耗時(shí)范圍assert(index < num_buckets_);// index 所在的hash桶 bucket_[index]元素個(gè)數(shù)自增buckets_[index].store(buckets_[index].load(std::memory_order_relaxed) + 1,std::memory_order_relaxed);uint64_t old_min = min();if (value < old_min) {min_.store(value, std::memory_order_relaxed);}uint64_t old_max = max();if (value > old_max) {max_.store(value, std::memory_order_relaxed);}num_.store(num_.load(std::memory_order_relaxed) + 1,std::memory_order_relaxed);sum_.store(sum_.load(std::memory_order_relaxed) + value,std::memory_order_relaxed);sum_squares_.store(sum_squares_.load(std::memory_order_relaxed) + value * value,std::memory_order_relaxed);
}

以上添加的過程整體的邏輯如下幾步

  1. 計(jì)算該value 代表的耗時(shí) 所處hash桶的范圍。假如傳入的是2.5us, 則該耗時(shí)處于上文中打印出來的耗時(shí)范圍(2,3],返回該范圍代表的索引號(hào)
    [       0,       1 ]     6994   1.940%   1.940%
    (       1,       2 ]   277573  76.980%  78.920% ###############
    (       2,       3 ]    70608  19.582%  98.502% ####
    (       3,       4 ]     1884   0.522%  99.024%
    (       4,       6 ]      454   0.126%  99.150%
    
  2. 拿到索引號(hào),更新該hash桶的元素個(gè)數(shù)。 即bucket_自增
  3. 更新當(dāng)前層的當(dāng)前讀耗時(shí)其他指標(biāo):最小值,最大值,值的總和,值平方的總和

也就是整個(gè)添加過程并不會(huì)將新的value數(shù)據(jù)保存下來,而是維護(hù)該value所處的bucket_大小,這個(gè)bucket_一個(gè)std::atomic_uint_fast64_t的數(shù)組,初始化整個(gè)hash桶的過程就已經(jīng)完成了整個(gè)hash桶耗時(shí)范圍的映射。

其中計(jì)算索引的邏輯如下:
主要是通過變量valueIndexMap_來查找的,這個(gè)變量在HistogramBucketMapper構(gòu)造函數(shù)中進(jìn)行的初始化。是一個(gè)map類型,key-value代表的是一個(gè)個(gè)已經(jīng)完成初始化的耗時(shí)區(qū)間。

IndexForValue函數(shù)中拿著當(dāng)前耗時(shí)數(shù)據(jù)value 在valueIndexMap_中查找到第一個(gè)小于等于(lower_bound)當(dāng)前指標(biāo)的索引位置。

size_t HistogramBucketMapper::IndexForValue(const uint64_t value) const {if (value >= maxBucketValue_) {return bucketValues_.size() - 1;} else if ( value >= minBucketValue_ ) {std::map<uint64_t, uint64_t>::const_iterator lowerBound =valueIndexMap_.lower_bound(value);if (lowerBound != valueIndexMap_.end()) {return static_cast<size_t>(lowerBound->second);} else {return 0;}} else {return 0;}
}

到此,類似于計(jì)數(shù)排序的方式,通過范圍hash桶動(dòng)態(tài)維護(hù)了一個(gè)個(gè)元素所處的bucket_ size,而非整個(gè)元素。
hash桶 以map方式實(shí)現(xiàn),本事有序,key-value代表范圍,保證相鄰的桶的耗時(shí)區(qū)間不會(huì)重疊,從而達(dá)到統(tǒng)計(jì)的過程中在范圍之間是有序的。

這個(gè)時(shí)候很多同學(xué)會(huì)有疑惑,返回之間有序,當(dāng)分位數(shù)的某一個(gè)指標(biāo)比如p20落在了一個(gè)擁有數(shù)萬個(gè)元素的范圍之間。按照當(dāng)前的計(jì)算方式,其實(shí)已經(jīng)無法精確計(jì)算p20這樣的指標(biāo)了。之前也說了,rocksdb實(shí)現(xiàn)的是工業(yè)級(jí)的分位數(shù)計(jì)算,也就是我們通過p99即更高的分位數(shù)指標(biāo)作為系統(tǒng)可用性的評(píng)判標(biāo)準(zhǔn),那當(dāng)前的分桶算法實(shí)現(xiàn)的分位數(shù)計(jì)算也就能夠理解了。

正如我們打印的分桶過程:

[       0,       1 ]     9230   3.422%   3.422% #
(       1,       2 ]   174704  64.771%  68.193% #############
(       2,       3 ]    50114  18.580%  86.773% ####
(       3,       4 ]    13030   4.831%  91.604% #
(       4,       6 ]     8827   3.273%  94.877% #
(       6,      10 ]     9314   3.453%  98.330% #
(      10,      15 ]     1893   0.702%  99.032%
(      15,      22 ]      969   0.359%  99.391%
(      22,      34 ]      708   0.262%  99.653%
(      34,      51 ]      571   0.212%  99.865%
(      51,      76 ]      324   0.120%  99.985%
(      76,     110 ]       35   0.013%  99.998%
(     110,     170 ]        3   0.001%  99.999%

可以看到在更高層分桶區(qū)間內(nèi),命中的指標(biāo)越來越少,也就是像p999,p9999這樣的高分位數(shù)的統(tǒng)計(jì)將更加精確。

當(dāng)然,也可以有完全精確的計(jì)算統(tǒng)計(jì)方法,那就是需要通過空間以及計(jì)算資源來換取精確度了,這個(gè)代價(jià)并不是一個(gè)很好的trade off方式。rocksdb的分桶同樣可以控制精確度,我們可以手動(dòng)調(diào)整初始化桶的精度,來讓精度區(qū)間更小。

接下來我們看看后面兩步:

  • 確定p 分位數(shù)的位置
  • 計(jì)算p 分位數(shù)的值

這兩步的實(shí)現(xiàn)主要在如下函數(shù)中:
通過傳入分位數(shù)指標(biāo)50, 99,99.9,99.99等來進(jìn)行對(duì)應(yīng)的計(jì)算。

double HistogramStat::Percentile(double p) const {double threshold = num() * (p / 100.0); // 分位數(shù)所在總請(qǐng)求的位置uint64_t cumulative_sum = 0;for (unsigned int b = 0; b < num_buckets_; b++) {uint64_t bucket_value = bucket_at(b);cumulative_sum += bucket_value;if (cumulative_sum >= threshold) { // 持續(xù)累加bucket_請(qǐng)求數(shù),直到達(dá)到分位數(shù)所在總請(qǐng)求個(gè)數(shù)的位置// Scale linearly within this bucketuint64_t left_point = (b == 0) ? 0 : bucketMapper.BucketLimit(b-1);uint64_t right_point = bucketMapper.BucketLimit(b);uint64_t left_sum = cumulative_sum - bucket_value;uint64_t right_sum = cumulative_sum;double pos = 0;uint64_t right_left_diff = right_sum - left_sum;if (right_left_diff != 0) {pos = (threshold - left_sum) / right_left_diff; // 計(jì)算p 分位數(shù)的位置}double r = left_point + (right_point - left_point) * pos; // 計(jì)算分位數(shù)的值uint64_t cur_min = min();uint64_t cur_max = max();if (r < cur_min) r = static_cast<double>(cur_min);if (r > cur_max) r = static_cast<double>(cur_max);return r;}}return static_cast<double>(max());
}

以上函數(shù)關(guān)于分位數(shù)位置和值的計(jì)算基本和下面公式接近:
p分位數(shù)位置的值 = 位于p分位數(shù)取整后位置的值 + (位于p分位數(shù)取整下一位位置的值 - 位于p分位數(shù)取整后位置的值)*(p分位數(shù)位置 - p分位數(shù)位置取整)

rocksdb的計(jì)算方式是通過取分位數(shù)所處總請(qǐng)求的位置,該請(qǐng)求所在的hash桶范圍內(nèi)取第pos個(gè)位置的指標(biāo),作為當(dāng)前分位數(shù)的值。
通過這樣圖就比較清晰的展示計(jì)算threshold的精確percentile的值了。

  • left_point hash桶區(qū)間左端點(diǎn)
  • right_point 代表右端點(diǎn)
  • threshold 代表分位數(shù)所處該區(qū)間的位置的比例(并不是一個(gè)精確的位置)
  • left_sum 達(dá)到左端點(diǎn)的之前所有hash桶請(qǐng)求總個(gè)數(shù)
  • right_sum 達(dá)到右端點(diǎn)的之前所有hash桶請(qǐng)求總個(gè)數(shù)

threshold所在的pos 計(jì)算如下:pos = (right_sum - threshold) / (right_sum - left sum)
整個(gè)pos代表的值的計(jì)算如下:r = left_point + pos * (right_point - left_point) ,即可得到當(dāng)前hash桶所代表的耗時(shí)區(qū)間內(nèi)分位數(shù)所處的精確位置,當(dāng)前這里的精確肯定不是100%,也是一個(gè)概率性的數(shù)值。

一些總結(jié) 和 相關(guān)論文

工業(yè)界的實(shí)現(xiàn)總是在成本和收益之間進(jìn)行取舍,最低的成本換來最大的收益。分位數(shù)的價(jià)值是說能夠用最低的計(jì)算和存儲(chǔ)成本換來系統(tǒng)可用性的評(píng)判,從而更有針對(duì)性的進(jìn)行系統(tǒng)優(yōu)化,從而產(chǎn)生更大的價(jià)值。

分位數(shù)的工業(yè)界算法實(shí)現(xiàn)有很多篇可以參考的論文:
1. 隨機(jī)算法- Random Sampling with a Reservoir
2.靜態(tài)分桶算法 - HdrHistogram
3.二叉樹實(shí)現(xiàn)的靜態(tài)分桶算法 - Medians and Beyond: New Aggregation Techniques
for Sensor Networks
4. 動(dòng)態(tài)分桶 GK算法

歡迎感興趣的同學(xué)一起討論~

總結(jié)

以上是生活随笔為你收集整理的Rocksdb 的优秀代码(一) -- 工业级分桶算法实现分位数p50,p99,p9999的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。