Rocksdb 的优秀代码(二)-- 工业级 打点系统 实现分享
文章目錄
- 前言
- 數據結構選型
- 打點代碼設計
- 耗時打點
- 請求計數打點
- 打點總結
前言
一個完善的分布式系統一定是需要完善的打點統計,不論是對系統內核 還是 對系統使用者都是十分必要的。系統的客戶需要直觀得看到這個系統的性能相關的指標來決定是否使用以及如何最大化使用系統,同樣 系統的開發者也需要直觀的看到系統各個組件的各項指標,來進行性能調優 以及 穩定性保障(運維監控,failover)。
而打點 主要 是耗時相關 以及 請求數相關的統計,這一些統計需要對系統代碼侵入即需要消耗系統的執行時間。而 分布式系統對性能的極致需求肯定是不允許額外增加核心代碼之外的執行耗時的(計算平均值,最大值,不同的分位數),所以打點代碼需要盡可能降低對系統本身性能的影響,這里面就需要有足夠精巧的代碼設計。
回到今天分享的Rocksdb 的打點設計,Rocksdb作為單機引擎,被廣泛應用在了各個分布式系統之中,而引擎的性能是上層分布系統性能的基礎。本身外部系統對引擎性能有極致需求 且 Rocksdb本身也在不斷追求卓越的性能,這個時候rocksdb自己的打點系統就非常有借鑒意義,其已經經過無數系統 和 卓越的開發者們反復驗證,是一個非常有參考價值的打點代碼設計。
本次分享會從兩方面開看Rocksdb 的打點設計,也是最傳統的打點類型:
- 耗時統計(各個維度的耗時信息)
- 請求統計(各個維度的請求個數相關的信息)
以下描述的打點主要是全局統計的,就是通過打開options.statistics 參數獲取的;對于perf_context和iostats_contxt這一些thread local指標并不提及;
數據結構選型
打點的目的希望對內(研發者)以及對外(用戶)提供系統性能的參考 以及 系統問題的報警,這一些信息會涉及到計算。舉一個例子,比如分位數,像p50,p99,p999 這樣的指標是監控系統必不可少的,用來展示系統的延時/長尾延時情況,這一些指標的計算是需要對抓取的請求保存并排序才能取到的。對于一個集群百萬/千萬級每秒的請求,每次取分位數,都需要對百萬級數據進行排序?顯然不可能。
所以這里 對打點數據的保存需要有合理的數據結構才行,rocksdb 提供了如下細節:
-
保存各個指標都需要的數據結構
struct HistogramData {// 從media -- perceile99的 都是分位數double median; // P50double percentile95; // P95double percentile99; // P99double average; // 平均值double standard_deviation; // 方差// zero-initialize new members since old Statistics::histogramData()// implementations won't write them.double max = 0.0;uint64_t count = 0;uint64_t sum = 0;double min = 0.0; } -
計算分位數的核心數據結構,比如P999 表示到目前為止所有請求中從小到大 第0.1% 個請求的耗時指標。
而這個從小到大 不一定需要真正通過排序算法排序,第0.1% 也不一定精確的第0.1%個,可以在其上下10個請求內浮動。
所以這里針對分位數 的計算是維護了一個有序hash表,將統計的請求添加到對應所處范圍的hash桶內,后續取P999這樣的指標時排序只需統計從小桶的請求數到滿足0.1%時請求數目 的桶 ,再進行更細粒度的取值。分位數算法細節比較多,有興趣的可以看Rocksdb 的優秀代碼(一) – 工業級分桶算法實現分位數p50,p99,p9999
總之,不需要排序,只需要添加到一個map里自動排序,維護每個map 元素bucket的請求數即可。
class HistogramBucketMapper {public:HistogramBucketMapper(); // 初始化耗時值和整個耗時區間// converts a value to the bucket index.size_t IndexForValue(uint64_t value) const;// number of buckets required.size_t BucketCount() const {return bucketValues_.size();}uint64_t LastValue() const {return maxBucketValue_;}uint64_t FirstValue() const {return minBucketValue_;}uint64_t BucketLimit(const size_t bucketNumber) const {assert(bucketNumber < BucketCount());return bucketValues_[bucketNumber];}private:std::vector<uint64_t> bucketValues_; // 初始化耗時值uint64_t maxBucketValue_; // 耗時最大uint64_t minBucketValue_; // 耗時最小// 初始化耗時區間,以[bucketValues_[i-1],bucketValues_[i]]std::map<uint64_t, uint64_t> valueIndexMap_; }; -
保存每個指標的各個維度的data數據,比如請求總數,最大,最小,上面數據結構中 valueIndexMap_各個區間的請求數(方便計算分位數)等
struct HistogramStat {......// 計算統計信息的代碼,最大,最小等void Clear();bool Empty() const;void Add(uint64_t value);void Merge(const HistogramStat& other);inline uint64_t min() const { return min_.load(std::memory_order_relaxed); }inline uint64_t max() const { return max_.load(std::memory_order_relaxed); }inline uint64_t num() const { return num_.load(std::memory_order_relaxed); }inline uint64_t sum() const { return sum_.load(std::memory_order_relaxed); }inline uint64_t sum_squares() const {return sum_squares_.load(std::memory_order_relaxed);}inline uint64_t bucket_at(size_t b) const {return buckets_[b].load(std::memory_order_relaxed);}// 計算分位數相關的代碼,p50,平均值,標準差double Median() const;double Percentile(double p) const;double Average() const;double StandardDeviation() const;// 通過這個函數,將獲取到的各個數據給到上文最開始的數據結構HistogramData// 暴露給用戶。void Data(HistogramData* const data) const;std::string ToString() const;// To be able to use HistogramStat as thread local variable, it// cannot have dynamic allocated member. That's why we're// using manually values from BucketMapperstd::atomic_uint_fast64_t min_;std::atomic_uint_fast64_t max_;std::atomic_uint_fast64_t num_;std::atomic_uint_fast64_t sum_;std::atomic_uint_fast64_t sum_squares_;std::atomic_uint_fast64_t buckets_[109]; // 109==BucketMapper::BucketCount()const uint64_t num_buckets_; };
到現在,基礎的數據結構就這么多,接下來通過兩種維度的數據來看一下rocksdb 如何利用以上數據結構,完成自己的打點代碼設計的。
打點代碼設計
以如下兩個指標為例:
-
讀請求耗時統計:rocksdb.db.get.micros
rocksdb.db.get.micros P50 : 0.000000 P95 : 0.000000 P99 : 0.000000 P99.9 : 0.000000 P99.99 : 0.000000 P100 : 0.000000 COUNT : 0 SUM : 0這是讀指標數據,其中p99.9 和 p99.99是自己加的,原來并沒有,可以看到總體的指標數據就是我們上文中數據結構中的指標。
-
Block_cache命中數統計:rocksdb.block.cache.hit
rocksdb.block.cache.hit COUNT : 0請求數相關的指標就是單純的個數統計,不會有分位數的統計,畢竟分位數只在有延時需求的場景才會有用。
耗時打點
針對rocksdb.db.get.micros 指標,維護了一個直方圖變量
enum Histograms : uint32_t {DB_GET = 0, // 讀耗時DB_WRITE, // 寫耗時COMPACTION_TIME, // compaction 耗時COMPACTION_CPU_TIME,SUBCOMPACTION_SETUP_TIME,...
}
按照我們的理解,一個函數的執行耗時 是 在函數開始時獲取一個時間,函數運行結束后再獲取一個時間,兩個時間的差值就是這個函數的執行耗時,很簡答。
而rocksdb 獲取讀耗時指標的代碼如下:
Status DBImpl::GetImpl(const ReadOptions& read_options,ColumnFamilyHandle* column_family, const Slice& key,PinnableSlice* pinnable_val, bool* value_found,ReadCallback* callback, bool* is_blob_index) {assert(pinnable_val != nullptr);PERF_CPU_TIMER_GUARD(get_cpu_nanos, env_); // perf context的統計StopWatch sw(env_, stats_, DB_GET); // 統計讀耗時......
}
后面再沒有其他的耗時計算了。是不是有點詫異,也就是rocksdb 通過讀請求函數開始StopWatch對象的初始化,完成了整個讀函數的耗時統計。
StopWatch的代碼如下,大家就能夠看到Rocksdb 代碼的設計精妙了。
構造StopWatch對象需要傳入 基本的三個參數:
- env_ , rocksdb維護的全局共享的環境變量
- Stats_ ,statistics ,將獲取到的時間添加到上文的三種數據結構中,參與運算
- DB_GET, 直方圖變量,表示讀請求的指標;類似的還有DB_WRITE等
// 初始化代碼如下
StopWatch(Env* const env, Statistics* statistics, const uint32_t hist_type,uint64_t* elapsed = nullptr, bool overwrite = true,bool delay_enabled = false): env_(env), // 全局環境變量,用來提供一個便捷的函數,后續主要用來調用時間函數 NowMicros() statistics_(statistics), // 需要通過options.statistics初始化,否則默認為空,就不打開rocksdb的打點系統了hist_type_(hist_type), // 直方圖變量,DB_GET, DB_WRITE...elapsed_(elapsed),overwrite_(overwrite),stats_enabled_(statistics &&statistics->get_stats_level() >=StatsLevel::kExceptTimers &&statistics->HistEnabledForType(hist_type)),delay_enabled_(delay_enabled),total_delay_(0),delay_start_time_(0),start_time_((stats_enabled_ || elapsed != nullptr) ? env->NowMicros()// 起始時間: 0) {}
也就是在StopWatch對象初始化完成時記錄下了起始時間:
start_time_((stats_enabled_ || elapsed != nullptr) ? env->NowMicros()// 起始時間
因為StopWatch 是在GetImpl函數中創建的,屬于函數局部變量,那想要獲取到結束時間,只要這個函數退出,StopWatch的析構函數會被自動調用,也就是只需要在析構函數中調用獲取結束時間即可。
~StopWatch() {......if (stats_enabled_) {//計算結束時間,并將DB_GET和結束時間添加到直方圖中statistics_->reportTimeToHistogram(hist_type_, (elapsed_ != nullptr)? *elapsed_: (env_->NowMicros() - start_time_));}}
到此已經拿到了Get請求的準確耗時了,簡潔且優雅!!!
接下來就是拿著耗時,和請求類型添加到直方圖中即可。
先看一下直方圖中的原始數據形態:
** Level 0 read latency histogram (micros):
Count: 1805800 Average: 1.4780 StdDev: 8.70
Min: 0 Median: 0.8399 Max: 4026
Percentiles: P50: 0.84 P75: 1.40 P99: 2.32 P99.9: 5.10 P99.99: 9.75
------------------------------------------------------
[ 0, 1 ] 1075022 59.532% 59.532% ############
( 1, 2 ] 706790 39.140% 98.672% ########
( 2, 3 ] 18280 1.012% 99.684%
( 3, 4 ] 2601 0.144% 99.828%
( 4, 6 ] 2357 0.131% 99.958%
......
橫線之上的指標很明顯,之下的指標簡單說一下,它就是我們數據結構選型中的HistogramBucketMapper數據結構。
在Percentiles之下 總共有四列(這里將做括號和右方括號算作一列,是一個hash桶)
- 第一列 : 看作一個hash桶,這個hash桶表示一個耗時區間,單位是us
- 第二列:一秒內產生的請求耗時命中當前耗時區間的有多少個
- 第三列:一秒內產生的請求耗時命中當前耗時區間的個數占總請求個數的百分比
- 第四列:累加之前所有請求的百分比
耗時打點會簡化輸出如下:
回到我們拿著DB_GET和time 匯報到直方圖中,通過如下函數
virtual void reportTimeToHistogram(uint32_t histogramType, uint64_t time) {//表示禁止打開直方圖,也就是上文中說的options.statistics參數未初始化,使用默認的。if (get_stats_level() <= StatsLevel::kExceptTimers) { return;}// 添加直方圖recordInHistogram(histogramType, time);
}
最終調用到StatisticsImpl::recordInHistogram函數, 更新直方圖中HistogramStat數據結構中的各個指標。
void StatisticsImpl::recordInHistogram(uint32_t histogramType, uint64_t value) {assert(histogramType < HISTOGRAM_ENUM_MAX);if (get_stats_level() <= StatsLevel::kExceptHistogramOrTimers) {return;}// 將指標添加到histogram_中,并計算該時間在直方圖所屬bucket,將bucket計數自增。// 除了添加到直方圖bucket,還會更新總時間,總請求數等指標。per_core_stats_.Access()->histograms_[histogramType].Add(value); if (stats_ && histogramType < HISTOGRAM_ENUM_MAX) {// 留給用戶態的接口,如果用戶不繼承實現針對該指標的處理,則不會做任何事情。stats_->recordInHistogram(histogramType, value);}
}
如果用戶想要自己做一些請求統計,比如統計總共的打點次數。可以通過如下方式,用戶態繼承statistics類即可:
class DummyOldStats : public Statistics {public:......void measureTime(uint32_t /*histogram_type*/, uint64_t /*count*/) override {num_mt++;}... }
再看一下per_core_stats_.Access()->histograms_[histogramType].Add(value); 中的Add函數,很簡單的指標更新。
這個函數處于所有指標調用的必經路徑,可以看到這里設計的時無鎖方式執行邏輯。也就是認為 并發Get場景下的直方圖更新,其實順序性并沒有那么重要,因為耗時會置放到它所屬的時間bucket中,請求數自增,并不是嚴格排序方式獲取分位數指標的。
void HistogramStat::Add(uint64_t value) {// 獲取value-time 以及 耗時時間所處的直方圖索引// 拿著index,更新對應的buckets的個數const size_t index = bucketMapper.IndexForValue(value);assert(index < num_buckets_);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);
}
到此,一次Get請求的耗時信息已經添加到了直方圖中。需要注意的是,此時并沒有計算對應的分位數指標,僅僅更新了buckets_。
當調用 輸出直方圖的函數時,會進行分位數的計算,DBImpl::PrintStaistics()函數中
void DBImpl::PrintStatistics() {auto dbstats = immutable_db_options_.statistics.get();if (dbstats) {// 打印直方圖ROCKS_LOG_INFO(immutable_db_options_.info_log, "STATISTICS:\n %s",dbstats->ToString().c_str());}
}
主要是就是直方圖的ToString函數中,計算分位數,并將計算的結果填充到HistogramData數據結構中,后續直接打印。
std::string StatisticsImpl::ToString() const {MutexLock lock(&aggregate_lock_);......// 打印所有指標的耗時數據for (const auto& h : HistogramsNameMap) {assert(h.first < HISTOGRAM_ENUM_MAX);char buffer[kTmpStrBufferSize];HistogramData hData;// 計算分位數getHistogramImplLocked(h.first)->Data(&hData);// don't handle failures - buffer should always be big enough and arguments// should be provided correctlyint ret =snprintf(buffer, kTmpStrBufferSize,"%s P50 : %f P95 : %f P99 : %f P100 : %f COUNT : %" PRIu64" SUM : %" PRIu64 "\n",h.second.c_str(), hData.median, hData.percentile95,hData.percentile99, hData.max, hData.count, hData.sum);if (ret < 0 || ret >= kTmpStrBufferSize) {assert(false);continue;}res.append(buffer);}if (event_tracer_) res.append(event_tracer_->ToString());res.shrink_to_fit();return res;
}
其中getHistogramImplLocked(h.first)->Data(&hData);計算分位數,并將結果添加到HistogramData數據結構。
void HistogramStat::Data(HistogramData * const data) const {assert(data);data->median = Median();data->percentile95 = Percentile(95);data->percentile99 = Percentile(99);data->max = static_cast<double>(max());data->average = Average();data->standard_deviation = StandardDeviation();data->count = num();data->sum = sum();data->min = static_cast<double>(min());
}
關于分位數計算 細節可以看 上文中提到的Rocksdb分位數實現鏈接 。Rocksdb 的優秀代碼(一) – 工業級分桶算法實現分位數p50,p99,p9999
請求計數打點
這里就比較簡單了,針對計數打點,同樣維護了一個直方圖枚舉類型:
enum Tickers : uint32_t {// total block cache misses// REQUIRES: BLOCK_CACHE_MISS == BLOCK_CACHE_INDEX_MISS +// BLOCK_CACHE_FILTER_MISS +// BLOCK_CACHE_DATA_MISS;BLOCK_CACHE_MISS = 0,// total block cache hit// REQUIRES: BLOCK_CACHE_HIT == BLOCK_CACHE_INDEX_HIT +// BLOCK_CACHE_FILTER_HIT +// BLOCK_CACHE_DATA_HIT;BLOCK_CACHE_HIT,// # of blocks added to block cache.BLOCK_CACHE_ADD,......
}
其中BLOCK_CACHE_HIT屬于其中計數類型的一種,主要是在更新Cache的代碼中使用RecordTick函數來更新請求計數。
void BlockBasedTable::UpdateCacheHitMetrics(BlockType block_type,GetContext* get_context,size_t usage) const {......// 命中BlockCache,這里進行BLOCK_CACHE_HIT 更新指標// 默認使用RecordTick,即使用戶配置了get_context,也會用RecordTick來更新指標if (get_context) {++get_context->get_context_stats_.num_cache_hit;get_context->get_context_stats_.num_cache_bytes_read += usage;} else {RecordTick(statistics, BLOCK_CACHE_HIT);RecordTick(statistics, BLOCK_CACHE_BYTES_READ, usage);}
void StatisticsImpl::recordTick(uint32_t tickerType, uint64_t count) {assert(tickerType < TICKER_ENUM_MAX);// 對直方圖變量中的類型使用 松散內存序 進行自增per_core_stats_.Access()->tickers_[tickerType].fetch_add(count, std::memory_order_relaxed);if (stats_ && tickerType < TICKER_ENUM_MAX) {stats_->recordTick(tickerType, count);}
}
后續打印的話也是使用類似耗時打印的StatisticsImpl::ToString()函數進行打印。
以上除了關鍵路徑的耗時以及請求統計,后續的直方圖相關的指標的計算都是通過后臺thread_dump_stats_ 進行異步更新。
打點總結
Rocksdb 的打點系統 中核心是耗時打點,使用了巧妙的類的構造和析構 完成輕量耗時統計,并通過異步線程完成直方圖的計算和更新。
尤其是分位數的計算,使用hash桶方式僅僅統計 耗時時間段的請求計數 來完成分位數的預估。整個打點系統經歷過大量工業級應用的錘煉,可以說是非常優雅的系統代碼設計,值得學習借鑒。
總結
以上是生活随笔為你收集整理的Rocksdb 的优秀代码(二)-- 工业级 打点系统 实现分享的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 紫色梅花是谁画的呢?
- 下一篇: Intel Optane PMEM 概览