contributed by < grb72t3yde
>
sysprog2020
SF-Zhou
bloom filter
, 以對 query 進行優化
嘗試引入 Xor filter
取得 LevelDB 最新 release 1.22 版本。
如成功建構靜態函式庫, 應出現類似下圖之輸出 :
接著編譯以下程式碼,操作一個 DB
物件來檢驗安裝是否成功 :
LevelDB
使用 LSM-tree (Log-Structured Merged Tree)
作為其 On disk
組織檔案的資料結構, 它能保證資料與日誌的寫入是 sequential write
, 而這在 SSD 上面是格外重要的 (因為它們不善於 random write
)。Log-Strctured
在寫入資料時以 append
機制在後台清理過時的資料)的方式寫入。對於資料的更新不做 in-place
的修改, 而是直接寫入新的資料並等待 garbage collection (GC)
,因此 :random read
的表現, 這同時也是引入 Bloom Filter
的原因key value pair
的操作, 我們必須搜索 所有具有包含此 key
的 key range
的檔案, 原因是我們不確定哪個檔案中有我們的目標 key
。舉例來說, 假設我們的目標是
k = 4
, 對於擁有 key range1~5
的檔案 s, 與擁有 key range3~7
的檔案 s', (以及更多其他 key range 橫跨了4
的其他檔案), 我們不確定哪些檔案確實包含4
這個 key,因此必須全部找過一遍。
事實上, 也有可能全部都沒有; 也有可能全部都有, 而當然此情況下我們能夠知道哪一份是最新的版本。
所以引入 Bloom filter 的動機為藉由預測某個 key 是否存在某檔案中,以有機會減少要搜尋的檔案數量,減緩讀取放大。
value size
的增加, 使用 Bloom Filter
的收益也更顯著我們在 dict 中曾經提及 Bloom Filter,則 LevelDB 中的 Bloom Filter 實作與 dict 中的有何不同處 ?以下沿用 dict 中推導出的公式 之變數意義來說明 :
file
中,僅每 2KB
單位的 data block 資料中所有 keys 就建立一個 filter 並存放其於此 file
的 filter (meta) block 中,因此系統中其實存在著許多小 filter。(可以這麼做的原因以及其動機可以參考此篇文件。另外,文中也提及在 RocksDB 中移除了這個做法而改採用 full filter 的原因。)我們可觀察寫入 filter block 部分之程式碼 filter_block.cc, 看到 FilterBlockBuilder 類別下 StartBlock 的實作,其會用 2KB 去計算 filter_index。
line 40 的 CreateFilter 即為針對 tmp_keys_ 這組 keys 建立 filter。這個函式留待接下來講到 bloom filter 實作時說明。
對每一個 filter,LevelDB 是使用 double-hashing
來產生 個 hash values。這個部份的實作也留待接下來講到 bloom filter 實作時說明。
我們從 include/leveldb/filter_policy.h 來看起,這個檔案中定義了 FilterPolicy 這個 base class,提供了一個界面。
舉例來說,使用者可自訂一個 filter policy 實作如下 :
LevelDB 中的 bloom filter 實作則是 util/bloom.cc 中的 BloomFilterPolicy 類別,這裡 line 12 的 CreateFilter 就是前述建立 filter block 時所呼叫函數之定義 :
我們可以從上面這個程式碼片段看到幾件事情 :
bits_per_key
。從 line 5 可得知這個數字就是 ,這個數字決定了 。而且 levelDB 還會將算出來的 值控制在 1~30 之間,以控制每次建立 filter 花在運算 hash functions 的成本。一個 BloomFilterPoicy 建構的時候接入一個 bits_per_key 引數。若 在這個 policy 之下是固定的,則為什麼會需要存下 呢?
如前所述,levelDB 已經控制了花在計算 hash function 上的時間。我們接下來著重比較 levelDB 所使用的 bloom filter 和 xorfilter 之記憶體開銷以及 FPP(false positive possibility)。
實驗組
: Xor8 filter
對照組
: levelDB bloom filter with bits_per_key
= 10 (這個參數為 LevelDB 預設的推薦值, 當然它會影響記憶體開銷)
測試程式
: 使用 util/bloom_test 中的 VaryingLengths TEST。此測試先連續加入length
個 key ,再來透過預測一萬個未被加入的 key 是否在 filter 中來測量 false positive possibility。被判定為 good 的標準為得到 1.25% 以下的 false positive rate,否則將被判定為 mediocre。
可以看到當 length
逐漸上升,Xor filter 能維持在 的 FPP,最後成功通過 37 個測試。
另外,LevelDB 將一個 filter 最小大小規定為 8 byte,原因是過小的 filter 將造成高 FPP。所以可以看到前面幾個 iteration 都是 9 byte (8 byte 加上 1 byte 的 )。Xor8 則可以參考論文中的公式,其也提到可以用 Xor+ filter 來降低空間浪費。在 length
到達 2000 時,其 Xor filter 的記憶體開銷已經小於 bloom filter。
[TODO : 整理成圖表]
Skip List 用來實作 LevelDB 中的 memtable,也用來實作 Redis 中的 sorted sets 資料結構。
程式碼: leveldb/db/skiplist.h
可改進之處:
Next
操作Random 的修改
由於
kBranching
實在太小 (4
),甚至可避免用 PRNG
Skip Lists: Done Right 一文中點出 :
"In the past five years, people have become increasingly sceptical of skip lists’ performance, due to their poor cache behavior when compared to e.g. B-trees"
並且基於這個動機提出了一些得以改善快取機制效益的 skip list 設計方法。
pointer chasing 在系統執行期間造成諸多 TLB, cache miss,其原因來自於處理器必須處理連續的"不規則記憶體存取"之指令,進而導致快取效益大幅下降,甚至造成快取污染。
除了透過軟體設計減緩此問題,也有硬體層面的方法,例如 : Accelerating Pointer Chasing in 3D-Stacked Memory: Challenges, Mechanisms, Evaluation。
為了觀察 levelDB 原本 skip list 實作的 cache 表現,我嘗試用 performance event 來觀察 db_bench
。執行 --benchmarks="fillrandom,readseq"
。
首先用 perf stat 觀察執行此 benchmark 時的 cache miss, sTLB miss 狀況 :
接著使用 perf record 觀察原實作的程式熱點 :
得到以下執行結果 :
得到 perf cache-misses event 報告如下 :
得到 perf dTLB-load-misses event 報告如下 :
發現 leveldb::SkipList<char const*, leveldb::MemTable::KeyComparator>::Insert
這個函式在兩種 miss 的統計中都是熱點。
接著觀察對 symbol leveldb::SkipList<char const*, leveldb::MemTable::KeyComparator>::Insert
進行 annotation