Rocksdb 的优秀代码(一) -- 工业级分桶算法实现分位数p50,p99,p9999
文章目錄
- 基本概念
- 普通的分位數(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ò)的grafana和prometheus一定需要工業(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);
}
以上添加的過程整體的邏輯如下幾步
- 計(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% - 拿到索引號(hào),更新該hash桶的元素個(gè)數(shù)。 即bucket_自增
- 更新當(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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如果科比和乔丹巅峰时PK谁等分能力更猛?
- 下一篇: 磁盘I:O 性能指标 以及 如何通过 f