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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Rocksdb 的 rate_limiter实现 -- compaction限速

發布時間:2023/11/27 生活经验 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Rocksdb 的 rate_limiter实现 -- compaction限速 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • 前言
    • 1. Compaction為什么會影響Client qps
      • 1.1 基本LSM介紹
      • 1.2 LSM internal ops
      • 1.3 長尾延時的原因
    • 2. Rate limiter 基本限速接口
    • 3. Rate Limiter 限速原理實現
      • 3.1 Rate Limiter的傳入
      • 3.2 Rate Limiter 控制 sync datablock的速率
      • 3.3 Rate Limiter控制寫入速率
    • 4. rocksdb Rate Limiter的限制

前言

LSM 引擎針 的業界相關優化方案已經有很多了,優化的方向也是在不同workload緯度上進行取舍。
比如頭條的Amap ,中科的dCompaction 是為了降低Compaction的寫放大(寫放大除了消耗ss),卻在一定程度上降低了讀性能;

奧斯汀大學 17年發表在頂會SOSP上的pebblesdb 較為有效得降低了寫放大,寫吞吐相比于原LSM實現提升2-2.7x,同時在range query有一定的優化,性能甚至超過了原有LSM的range query。

悉尼大學 19年發表在頂會UTC上的SILK 通過rocksdb的rate limiter來 通過客戶端壓力的監控來對 LSM內部整個IO進行限速控制,隨未減少寫放大,但卻有效得降低了compaction對上層吞吐的qps和抖動的影響,尤其是長尾延時的優化上有效降低且極為穩定。

還有很多其他的優化方案,甚至專門有人整理了一篇LSM的優化論文,對業界數十篇LSM相關優化論文做了一個總結:
LSM-based Storage Techniques: A Survey
當然,rocksdb社區也在努力研究有效的compaction優化方案,本節想要介紹一下rocksdb 原生實現的compaction IO層的限速方案,通過compaction限速來降低compaction的IO對Client的 qps 和 波動的影響。

通過本文,你能夠了解到如下關于rocksdb/LSM的知識點:

  1. compaction為什么會影響上層qps (純寫場景)?
  2. rocksdb rate limiter 基本限速接口
  3. rocksdb rate limiter 原理實現
  4. rocksdb rate limter 的限制

ps: 文中涉及到的源代碼 都是基于rocksdb 6.4.6版本,不同的版本之間可能有實現細節上的差異,不過主體邏輯都是差不多的。

1. Compaction為什么會影響Client qps

1.1 基本LSM介紹

如下圖 是一個基本的LSM 樹的分層結構(以下的操作描述是針對傳統LSM 的實現進行描述的,沒有完整描述整個rocksdb的基本實現):

內存中有一個 write-buffer, 在rocksdb 上 也叫做 memtable
磁盤上從 L0-Ln也是一個分層結構: 每一層可以有很多有序字節表(sstables),其中L0層的sst之間可以有重疊,大于L0層的sst文件嚴格有序 且層內sst之間不能有重疊keys.

如下圖 兩種請求落到LSM 中:

  • Client 的update請求 會寫入到內存中的write buffer之中即可返回,后續的寫入由internal operation – flush and compaction來負責。

  • Client 的read 請求 會先讀 write buffer,如果讀不到則按照磁盤上的L0 - Ln逐層讀取。

1.2 LSM internal ops

LSM三種internal操作:

  • Flush 由write-buffer 寫入 L0
  • L0-L1 Compaction 因為L0 層文件之間允許有key的重疊(LSM 為了追求寫性能,使用append only方式寫入key,write-buffer一般是skiplist的結構),所以只允許單線程將L0的文件通過compaction寫入L1
  • Higher Level Compactions 大于L0層 的文件嚴格有序,所以可以通過多線程進行compaction.

Flush 操作如下:

  • 請求寫入到(write-buffer)memtable之中,當達到write_buffer_size大小 進行memtable switch
  • 舊的memtable變成只讀的用來 Flush,同時生成一個新的memtable用來接收客戶端請求
  • Flush的過程就是在L0 生成一個sst文件描述符,將immutable 中的數據通過系統調用寫入該文件描述符代表的文件中。

L0–> L1 Compaction 操作如下:

  • 將一個L0的sst文件和多個L1 的文件進行合并
  • 目的是節省足夠的空間來讓write-buffer持續向L0 Flush

Higher Level Compactions操作如下:
對整個LSM進行GC,主要丟棄一些多key的副本 和 刪除對應的key的values

這個過程并不如L0–>L1的compaction 緊急,但是會產生巨量的IO操作,這個過程可以后臺并發進行。

1.3 長尾延時的原因

  • L0滿, 無法接收 write-buff不能及時Flush,阻塞客戶端

    因果鏈如下:
    沒有協調好internal ops – 》 Higher Level Compactions 占用了過多的IO --》L0–>L1 compaction 過慢 --》L0沒有足夠的空間 --》Write-buffer無法繼續刷新。
  • Flush 太慢,客戶端阻塞

    因果鏈如下:
    沒有協調好internal ops --》 Higher Level Comapction占用了過多的I/O --》 Flushing 過程中沒有足夠的IO資源 --》Flushing 過慢 --》Write-buffer提早寫滿而無法切換成immutable memtable,阻塞客戶端請求。

綜上我們知道了在LSM 下客戶端長尾延時主要是由于三種 內部操作的IO資源未合理得協調好導致 最終的客戶端操作發生了阻塞。

針對長尾延時的優化我們需要通過協調內部的internal 操作之間的關聯,保證Flushing 優先級最高,能夠占用最多的IO資源;同時也需要在合理的時機完成L0–L1的Compaction 以及 優先級最低但是又十分必要的Higher Level Compaction。

以下簡單介紹一下Rocksdb 內部原生的Rate Limiter對這個過程的優化。

2. Rate limiter 基本限速接口

社區Rate Limiter 介紹
核心接口:

RateLimiter* rate_limiter = NewGenericRateLimiter(rate_bytes_per_sec /* int64_t */, refill_period_us /* int64_t */,fairness /* int32_t */);

將該接口加入到應用中的option的方式就是:

options.rate_limiter.reset(rocksdb::NewGenericRateLimiter(rate_bytes_per_sec /* int64_t */,refill_period_us /* int64_t */,fairness /* int32_t */));

核心參數如下三個:

  • rate_bytes_per_sec 這個是最常用也是大家使用起來最有效的一個參數,用來控制compaction或者flush過程中每秒寫入的量。比如,設置了200M, 表示當compaction 累積的總寫入token達到 200M /s 時才會觸發系統調用的write.
  • refill_period_us 用來控制 token 更新的頻率;比如設置的rate_bytes_per_sec是10M/s, 且refill_period_us 設置的是100ms,那么表示 每100ms即可重新調用一次compaction的寫入。針對1M的大value 可以立即寫入,而小于1M的數據則需要消耗CPU, 累積到1M 觸發一次寫入。
  • fairness 表示低優先級請求獲得處理的概率。
    RateLimiter 支持接受高優先級線程 和 低優先級線程的請求,一半flush操作是最高的優先級,其次是 L0 --> L1 compaction優先級較高,最后則是Higher Level compactions 優先級最低。那么這個參數 fairness表示 即使現在有較多的高優先級任務在調度,低優先級的任務也有 1 / fairness 的機會能夠被調度,從而防止被餓死。

3. Rate Limiter 限速原理實現

3.1 Rate Limiter的傳入

先從我們的客戶端入口來看 創建了RateLimiter 都做了一些什么?
rocksdb::NewGenericRateLimiter 接口主要是做一些初始化變量的工作:

以上類是繼承自RateLimiter 類,能夠提供更加精確的限速控制,比如初始化變量中的RateLimiter::Mode mode來制定限速是針對只讀 或者 只寫 或者 所有的讀寫。

這個時候我們初始化好了rate_limter的對象,并將其傳給options中,應用拿著options打開了rocksdb,具體的DB::Open過程中會初始化DBImpl對象

初始化該對象的過程中會拿著我們之前創建好的rate_limiter 進行全局option的初始化工作:

DBOptions SanitizeOptions函數中 能夠看到通過rate_limiter 結合其他配置 或者交給指定的配置來達到后續限速的目的。

同時還會有在其他地方直接使用的rate_limiter對象 達到限速的目的:
在從sst file中調用read接口讀取數據時能夠通過rate_limiter 限制讀請求的速率

同理當需要通過flush或者compaction 過程向sst文件中寫入數據的時候可以通過rate limiter限制寫入的速率

3.2 Rate Limiter 控制 sync datablock的速率

接著上文 RateLimiter 不為空時會將rate_bytes_per_sec 數值作為delayed_write_rate的速率。
這個delayed_write_rate參數會在rocksdb的Write Stall 的限速中進行描述,這里簡單說一下 rocksdb 觸發Write Stall 的幾種原因:

  • 過多memtables. 當此時內存中有 max_write_buffer_number 個memtables等待被flush,寫會被完全Stall
    在rocksdb的日志中會有如下記錄:
    Stopping writes because we have 5 immutable memtables (waiting for flush), max_write_buffer_number is set to 5
    
  • 過多的L0 SST files. 當L0的sst 文件個數超過level0_slowdown_writes_trigger個之后,會觸發write stall;當L0文件個數達到level0_stop_writes_trigger 之后寫入會完全阻塞。
    在rocksdb的日志中會有類似如下記錄:
    Stalling writes because we have 4 level-0 files
    Stopping writes because we have 20 level-0 files
    
  • 過多的待處理 Compaction-bytes. 當預估的待處理Compaction 總大小達到了soft_pending_compaction_bytes會觸發Stall,達到了hard_pending_compaction_bytes 會觸發stop write。
    在rocksdb的日志中會有類似記錄如下:
    Stalling writes because of estimated pending compaction bytes 500000000
    Stopping writes because of estimated pending compaction bytes 1000000000
    

回到上文中提到的bytes_per_sync參數:
在rocksdb 進行Compaction或Flush過程中 ,寫入數據之前 會通過OpenCompactionOutputFile函數 創建WritableFileWriter 對象,來負責將即將寫入的數據通過posix接口進行數據處理寫入到文件系統的page cache或者 direct寫入到磁盤之中。
創建WritableFileWriter 對象的過程中會將 通過option 傳入的rate_limiter 傳入到該對象之中。

到實際寫入時,會通過BlockBasedTableBuilder::Add --> BlockBasedTableBuilder::Flush() --> BlockBasedTableBuilder::WriteBlock() --> BlockBasedTableBuilder::WriteRawBlock() --> WritableFileWriter::Append() --> WritableFileWriter::Flush() 以及WritableFileWriter::WriteBuffered()這兩個函數

其中WritableFileWriter::Flush()這個函數中會調用如下邏輯:
即當前寫入到內存中緩存的數據偏移地址 相比于上一次的偏移地址 大小超過了1M,則觸發一次RangeSync

RangeSync中也需要strict_bytes_per_sync_ 參數為1 才會是一次真正的sync系統調用。

這里為什么會有這樣的配置呢,簡單描述一下rocksdb寫sst文件的邏輯:
原生配置是將所有的sst文件中的datablock,filterblock, index block,等所有的數據block寫到內存(page cache)之后統一調用一次對當前文件句柄的sync操作,從而有效減少IO次數。

但是問題是類似這樣大批量的累積sync 可能會導致compaction/flush 在每隔一段時間占用巨量的IO帶寬,從而造成client的latency spike或者qps下降。

所以通過 rate_limiter配置 + bytes_per_sync + strict_bytes_per_sync_ 能夠減少大批量的累積sync,而讓整個IO均勻分不到整個compaction/flush的寫入鏈路,可能client 的qps還是會有下降,但是不會出現過高的latency spike.

3.3 Rate Limiter控制寫入速率

回到上文 Compaction / Flush的寫入鏈條:
BlockBasedTableBuilder::Add --> BlockBasedTableBuilder::Flush() --> BlockBasedTableBuilder::WriteBlock() --> BlockBasedTableBuilder::WriteRawBlock() --> WritableFileWriter::Append() --> WritableFileWriter::Flush() 以及WritableFileWriter::WriteBuffered()這兩個函數
看看WritableFileWriter::WriteBuffered() 函數,它是負責向緩沖區中添加數據:

通過 調用rate_limiter的RequestToken函數 --》 調用GenericRateLimiter::Request函數,來檢測添加的數據大小left 是否滿足開始配置的rate_limiter的rate_bytes_per_sec限速大小,如果未達到,則會讓當前線程休眠,并按照refill_period_us頻率來定時更新待寫入的bytes是否滿足寫入的速率要求,并及時填充寫入緩沖區。

此時當前的寫入過程會阻塞,直到left累積到rate_limiter的寫入限速閾值,才會繼續向當前的文件句柄中正常寫入。

4. rocksdb Rate Limiter的限制

靜態的限速控制:
無法靈活變通, 后臺internal ops的寫入速率,比如偶爾Client 壓力較大,需要降低internal ops寫入速率對client的影響;偶爾Client 壓力較小, 又可以增加internal ops,將之前累積的待寫入的數據寫入。

實際的測試,剛開始能夠提供較為穩定的qps latency。但是在寫入一段時間之后,隨著數據量的增加,靜態的Rate limter無法保證 internal ops(flush , L0–>L1 compaction 以及 higher level compactions) 在不影響client latency的情況下及時有效的處理,從而出現了較高的latency spike.

當然rocksdb后續推出的 auto tune 以及 業界的 TRIAD 和 SILK設計 看他們的描述 都能夠有效的降低latency spike,后續會嘗試有效測試 并發掘背后實現機理 之后分享出來。

總結

以上是生活随笔為你收集整理的Rocksdb 的 rate_limiter实现 -- compaction限速的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。