LSM 優化系列(三)-- SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores ATC‘19

v-code發表於2020-11-14

1. Latency Spike in LSM

論文原地址 可以參考

在 USENIX Annual Technical Conference2019的定會上,該篇論文提出瞭解決LSM 架構在update 或者有部分update參與的場景下出現的長尾延時問題。

關於LSM架構下的長尾延時問題,之前在介紹rocksdb的Rate Limiter 實現原理 時提到過,這裡簡單描述一下。

1.1 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.2 長尾延時的原因

  • 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對這個過程的優化。

綜上可以看到傳統LSM的長尾延時問題 主要是由於LSM 三個internal ops(Flush, L0–>L1 compaction ,Higher Level Compactions)的排程策略導致的。

2. 長尾延時的業界解決方案

2.1 Rocksdb

首先對比了Rocksdb 實現的LSM,開啟和關閉Compaction時的長尾延時情況如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-vD0ujYIh-1605277760775)(/Users/zhanghuigui/Library/Application Support/typora-user-images/image-20201113144152817.png)]

可以看到開啟Internal ops(橘黃色部分) 之後 整個長尾延時相比於未開 高了接近4個量級。

2.2 Rate-Limited Rocksdb

通過限制 internal ops的I/O頻寬 是一個業界比較認可的限制長尾延時問題的方法,SILK開發人員也對Rocksdb自實現的Rate Limiter做了測試。關於Rate Limter的實現可以參考Rocklsdb Rate Limiter實現原理

通過設定不同的Limit Rate來對長尾延時的情況進行統計,得到如下測試結果:

在這裡插入圖片描述

可以看出隨著 限制internal ops頻寬越來越高,那麼長尾延時 維持在較低的時間 越來越長。

但是這並不能無限制得增加internal ops的限制頻寬,因為隨著compaction 累積的量越多,後期會 較高概率有一次累積 更多的compaction 與上層吞吐來爭搶IO頻寬。

2.3 TRIAD

ATC’17 TRIAD TRIAD: Creating Synergies Between Memory,
Disk and Log in Log Structured Key-Value Stores
是一個非常有代表性的先進系統,來降低internal ops對長尾延時的影響,SILK對該系統也做了對應的測試。

在這裡插入圖片描述

TRIAD 通過排程 減少 L0–》L1 的compaction 降低 compaction 對client 的吞吐影響,但是這個問題會導致Higher Level Compactions的增加。所以上圖中可以看出在1000s以後較長的一段時間內,整體的長尾延時還是會增加。

2.4 Pebblesdb

關於Pebblesdb 的整體介紹可以參考Pebblesdb: Building Key-Value Stores Using FLSM

SILK也對pebblesdb的長尾延時做了整體的測試,在讀敏感的workload下(95:5)workload下,長尾延時保持在一個非常好的效果,但是在執行了8個小時之後,compaction大量接入,整體的系統效率被嚴重阻塞。
在這裡插入圖片描述

因為Pebblesdb執行guards內部存在重疊key,所以到後期大量的Higher level compactions搶佔了系統的資源,導致整個client的IO被阻塞,整個stall一直持續直到compaction完成終止。

2.5 Lessons Learned

  1. 對於較高的長尾延時出現的主要原因是 寫被存在於記憶體的write buffer 填滿阻塞了。
    Write buffer滿主要有如下兩個原因
    a. L0滿,造成write-buffer向L0 flush被終止。從而導致客戶端無法持續向新的write-buffer寫入。這裡L0滿的原因是,L0–》L1 Compaction的過程中沒有IO被更高層compaction 產生的I/O搶佔,導致L0 提前滿。
    b. Flush slow,由於更高層的併發compaction導致Flush的IO被搶佔,從而產生Flush的速度沒有write-buffer的寫入速度快。
  2. 僅僅限制 internal ops的IO頻寬並無法解決Flush或者L0 ->L1 compaction等優先順序較高的任務被 Higher Level Compactions搶佔的問題。所以Rocksdb的 Rate Limiter 以及 Rocksdb的Auto tune等都會導致後續Higher Level compactions搶佔優先順序較高的任務的IO,從而間接導致write-buffer full.
  3. 近些年的一些LSM優化的方法 提出為了提升吞吐,將Compaction下沉到更高層來做,這在短期內能夠收穫較為穩定的延時以及較高的吞吐,但是在跑了很長一段時間之後就會出現大量的Compaction搶佔系統資源的問題,從而導致Client qps stall。

綜上 ,我們能夠得出如下結論,並不是所有的internal ops都需要平等共享系統資源,比如 Flush 以及 L0->L1 Compaction,就是優先順序比較高的internal ops,這一些操作不能及時完成,就會導致Client ops被stall。

3. SILK 實現

SILK 在總結了之前業界相關的優化方案,為了避免再次出現上文2.5中指出的Lesson learned,核心的設計思想將遵循如下三個原則。

3.1 SILK 設計原則

  1. 讓Internal ops都有能夠獲得IO頻寬的機會。SILK 在client 的流量洪峰時會減少Higher level Compaction的IO頻寬佔用,在client 流量低峰時增加Higher Level的Compactions的頻寬。
  2. 對LSM tree的internal ops進行優先順序排程。正如SILK將LSM的internal ops分為三種不同的操作,並排程優先順序來降低對Clinet的長尾延時的影響。SILK的排程策略如下:
    a. SILK確保Flush足夠快,即Flush的優先順序最高。從而為記憶體中的write-buffer接受客戶端的請求留下足夠的空間。Flush的速度是直接影響客戶端的長尾延時問題。
    b. SILK 將L0 -> L1 Compaction的速度放在第二優先順序,確保L0不會達到它的容量上限,從而保證Flush能夠有足夠的空間完成操作。
    c. SILK 將Higher Level Compaction的優先順序設定為最低,因為這些Compactions的目的是為了維持LSM的形狀,並不會短期內對Client的長尾延時造成影響。

3.2 SILK 詳細實現

3.2.1 按照概率分配I/O頻寬

SILK會持續監控 Client操作被分配的I/O頻寬,並且分配可用的頻寬給到internal ops。
根據客戶端 qps的波動情況,配置對應的監控粒度,用來決定為internal ops分配頻寬的比例。當前SILK的實現配置的監控粒度是10ms。

具體配置方式如下:

T B/s : 表示在LSM 架構的KV儲存中 ,總的I/O頻寬
C B/s: SILK 監控的Client操作佔用的I/O頻寬

則internal ops可用的頻寬是:I = T - C - µ B/s,其中 µ 可以理解為是一個小的buffer。

為了靈活得調整I/O頻寬,SILK使用了Rocksdb標準的Rate Limiter, 同時SILK 為Flushing和 L0->L1 Compaction,也維護了一個最小的可配置的I/O頻寬 時間間隔。

3.2.2 優先順序排程 和 可搶佔的 Internal ops設計

SILK 維護了internal work的執行緒池。當一個Flush任務或者一個Compaction任務處理完成之後,執行緒池會根據當前LSM 不同層待Compaction的量以及記憶體中write-buffer的狀態 來判斷是否需要排程更多的執行緒進行對應的internal (Flush 或者 L0 Compaction)任務的排程處理。
接下來主要介紹一下SILK 維護的兩個執行緒池:一個 為flushing的 high-priority 執行緒池,另一個低優先順序的Compactions執行緒池。

Flushing 在所有的internal ops中優先順序最高。所以它有自己專有的執行緒池,並且有許可權搶佔其他任何低優先順序的I/O頻寬。Flushing的最小頻寬需要能夠滿足從immutable memtable flush的速度快於active memtable的更新速度。當前 實現的SILK 允許記憶體中存在兩個memtable(一個是接受更新的active memtable,另一個只讀的immutable memtable) 和一個flush執行緒,可以通過設定memtable的個數來讓client ops低時延維持時間更久。

ps : 這裡提到的memtable 也是之前描述的write-buffer,都是LSM在記憶體中使用跳錶維護的有序元件。

L0–》L1 Compaction。L0->L1的compaction 主要是為了保證L0有足夠的空間來接受Flush的結果。SILK的實現中 這個internal ops並沒有專用的執行緒進行排程,而是在需要進行L0–>L1的Compaction時 從Compaction pool中隨機選擇一個執行緒搶佔 進行L0->L1的處理。

這裡有必要說明一下,L0–>L1 的compaction這麼重要,為什麼我們不實用多執行緒排程,從而加快這個過程?
因為L0的sst檔案之間存在key的overlap,為了保證大於L0的層內sst檔案之間不存在overlap,則需要保證L0的檔案處理的原子性,所以這裡只能使用一個執行緒進行排程。

而Rocksdb原生的實現中為了減少L0的檔案重疊度,也會排程L0–>L0的compaction,這裡SILK仍然會沿用rocksdb的邏輯(SILK是基於rocksdb 5.7版本進行的改造),只是會將L0–>L0的Compaction 看作L0–>L1 一樣的優先順序。

Higher Level Compactions。更高層的Compaction 擁有排程的優先順序最低。比如L1–>L2進行Compaction的過程中任務被L0–>L1的Compaction需求搶佔,這個時候SILK會丟棄掉當前正在做的Compaction任務,不過實際的測試並沒有發現這個操作對效能產生非常明顯的影響。

SILK控制了總的KV store的I/O頻寬,通過在Compaction執行緒池中排程更低優先順序的 Compactions來完成這一過程。比較有趣的是 SILK能夠根據 Compactions 任務的緊急程度進行對應I/O頻寬的分配,從而能夠降低整個LSM tree發生Latency Spike的概率 。

4. 效能資料

在這裡插入圖片描述
Nutanix 的workload是:write:read:scan的比例 – 57:41:2 ,客戶端穩定輸出的峰值20k/s
可以看到 SILK 能夠在很長的一段時間保證客戶端p99維持在1ms以下,且吞吐能夠和客戶端接近,相比於其他的系統則 比較有優勢。

在這裡插入圖片描述
在Synthetic的workload下,擁有較高的寫比例,同時 peek也由原來的20k/s 逐漸下降到 每隔10s 20k, 每隔50s 20k,每隔100s 20k, 更長時間之後來一次洪峰20k

可以看到silk 的延時會隨著洪峰持續的時間而逐漸增加,如果洪峰是間斷性來臨(比如 洪峰維持10s, 50s, 100s),則SILK能夠提供非常穩定且較低的延時。而因為洪峰的持續時間越長,待Compaction的量累積越多,Higher level compactions的影響越大(已經無法避免得排程更高層的Compaction)。
不過還是能夠看到silk是有能力支撐突然而來的流量洪峰,保證吞吐的前提下並維持在一個較低的延時上。

在這裡插入圖片描述
50% read 和 50% write 場景下,這裡SILK 將自己和前兩種不同的 internal限速策略進行對比,比如 :
第一行藍色的表示 動態分配internal IO的頻寬,關閉 支援優先順序搶佔的排程策略。這個前期能夠維持一個平穩的延時,但是隨著 Compaction 量的累積,後期仍會降低高優先順序的操作。
第二行只開啟 優先順序搶佔的策略,關閉動態分配IO頻寬的策略。因為 internal ops能夠進行不同優先順序之間的排程搶佔,但是沒有辦法統籌整體的IO頻寬,從而導致後期 Higher Level 的Compaction量增加,無法增大I/O頻寬,使得internal ops的排程和client的 排程出現衝突。

第三行 SILK 是融合了以上兩種的策略,可以看到整體能夠維持在一個非常平穩的延時和吞吐上。

5. 業界相關LSM優化的展望

在這裡 ,SILK介紹了三個方向的LSM相關的優化 以及 近些年業界已有的優化方案,非常有參考價值。

5.1 降低Compaction開銷方向

在這裡插入圖片描述

5.2 Compaction變種 或者 實現相關的代替方案

在這裡插入圖片描述
在這裡插入圖片描述

5.3 在LSM 的資料結構和相關演算法上的一些優化

在這裡插入圖片描述

LSM 的作為新生代儲存引擎的基礎架構,優異的寫吞吐,天然支援的冷熱分離架構下提供足量的讀的優化。
有得必有失,Compaction的 資料回收和 merge sort帶來的I/O 排程 挑戰讓後來者想要不斷去征服這座效能高峰,為引擎界引入足以和B樹相媲美的經典架構。

學無止境,不過還好有高手指路,能夠發現如此精彩的儲存世界,嘆之有幸!!!

相關文章