Rocksdb Ribbon Filter : 结合 XOR-filter 以及 高斯消元算法 实现的 高效filter
文章目錄
- 前言
- XOR-filter 實現原理
- xor filter 的構造原理
- xor filter 構造總結
- XOR-filter 和 ADD-filter對比
- XOR-filter 在計算上的優化
- Ribbon filter
- 高斯消元法
- 總結
- 參考
前言
還是起源于前幾天的Rocksdb meetup,其中Peter C. Dillinger 這位大佬分享了自己為rocksdb實現的Ribbon filter。這個Filter從2020年5月份左右提的第一個PR 到現在基本已經成熟了,也已經合入到了rocksdb-master中。同時,作者因為 ribbon-filter的設計,寫了一篇論文放在了arxiv上保持自己的原創性。
Ribbon filter的全稱是Rapid Incremental Boolean Banding ON the fly ,能夠基于真值矩陣 高效得用增量方式構建過濾器。
相比于傳統的Bloom Filter 來說,它的核心差異以及 優勢主要是如下幾點:
- 采用XOR 方式進行按位匹配。而大多數的bloom filter都是ADD 方式進行按位匹配。
- ribbon filter的構成是 在原始key經過多個hash函數之后 由一個二維bit數組組成,并不是像傳統的filter 由一個bit數組組成。
- 對CPU cache 更友好,檢查一個key 是否在filter內部,ribbon filter 可以直接加載二位數組中的一個bit數組進行順序匹配,而傳統ADD filter則需要在一個bit數組上根據hash函數的結果隨機匹配。
- 對空間更友好,ribbon filter存儲二位數組時可以通過高斯消元法轉化成上三角矩陣,更利于壓縮;且加載到內存中也更利于計算。
- 消耗更多的CPU,因為ribbon filter 優化的主體是節約內存 以及 磁盤存儲,而因為需要針對二維矩陣的后臺計算(高斯消元),相比于傳統一維數組來說會消耗更多的CPU。
本文將主要介紹Ribbon filter的設計 以及 基于行列式的高斯消元法 在ribbon filter中的應用。
XOR-filter 實現原理
介紹Ribbon-filter之前,xor-filter 是一定先要介紹的,整個ribbon filter的設計思想都是在xor-filter的基礎上演化出來的。
我們傳統的 ADD - filter大體形態是這樣的:
一個輸入的字符串或者說序列,經過多個 hash函數生成對應的hash值,ADD-filter會將這個hash值會與對應的bit 位按位與,從而將ADD-filter的 對應bit位置0,或者置1。
最后查找的時候,將輸入的key經過同樣多個hash函數生成的hash值 映射到ADD-filter上,與對應bit位進行按位與,如果結果位0,則key一定不存在于ADD-Filter中,如果位1,則可能存在。
而XOR-filter 的設計形態就有很大的差異了:
首先一個輸入的字符串或者數據序列經過多個hash函數生成的hash值 不會映射到一個bit位,而是映射為一個固定長度的slot,這個slot中會有多個bit位,映射的過程是 這個hash值和一個slot上的多個元素按位XOR。 然后多個hash函數產生的多個hash值會映射到多個slot上。XOR-filter 會維護著一些slots,通過將這一些slots中的bits數據做一個整體的計算生成一個唯一的hash函數保存下來。
最后用戶查找輸入的字符串的時候 會將多個hash函數映射的slots一起進行XOR運算,假設運算的結果是R1;同時將輸入的字符串 輸入到上面構造時唯一的一個hash函數中,運算的結果是R2。通過判斷R1==R2來確認這個key是否存在于XOR-filter中,不想等,則一定不存在。
即判斷指紋函數 中 key的值 和 幾個hash(key) 的 對應k-bits 值 按位異或之后的值是否相等,相等則在表示x可能在xor-filter中,否則不在。
xor filter 的構造原理
關于xor-filter 和 fingerprint的構造,大體邏輯如下,定義以下幾個關鍵變量:
- BBB 存放最終的k-bit 的 xor filter
- nnn 是參與構造當前 xor filter的所有key/items
- ddd 是 hash 函數的個數
- mmm 是slot 的個數
1 初始的BBB 數組的大小是m=7m=7m=7,每一個item/key 經過d=3d=3d=3個 hash函數 h1,h2,h3h1,h2,h3h1,h2,h3 的映射 分布到三個BBB的slot之中。每一個item的 k-bits值的計算是通過 h1(x)?h2(x)?h3(x)h1(x) \bigoplus h2(x) \bigoplus h3(x)h1(x)?h2(x)?h3(x) 得到的,也可以看作fingerprint(x)fingerprint(x)fingerprint(x),即 item的指紋標識。
首先我們看一下 @ 會通過三個hash函數 映射到 slot7, slot6, slot4中,我們可以看到slot7是被@ 獨享的,沒有被其他的 item映射到,所以slot7 能夠唯一標識 @,對slot7做一個邏輯標記。對于ADD filter來說,這里的slot就是一個bit位。
2 接下來我們將@ 的映射結果從BBB 的 7 個slot中剝離下來,即對應的slot 計數-- 即可,能夠看到slot6 是可以唯一標識+ item,同樣,我們也對slot6 做一個+ 的標記。
3 依次 處理完所有的 input items/keys,直到最后一個 ≈\approx≈,這個時候,將最后一個符號的 fingerprint(x) 的結果,也就是h1(x)?h2(x)?h3(x)h1(x) \bigoplus h2(x) \bigoplus h3(x)h1(x)?h2(x)?h3(x) 的結果放在 slot1, slot2 ,slot4 的任意一個slot中,比如slot1,其他的slot都設置為k-bits 0即可。 然后將≈\approx≈ 的fingerprint(x)=10010fingerprint(x) = 10010fingerprint(x)=10010 的結果 與slot2,slot4 的結果按位異或 得到的結果 然后放到slot1中。 slot1′sk?bits=10010?00000?00000=10010slot1's k-bits = 10010 \bigoplus 00000 \bigoplus 00000 = 10010slot1′sk?bits=10010?00000?00000=10010
4 回填其他的item,比如??
和前面的操作一樣,將標識?? 的fingerprint(x)=11011fingerprint(x) = 11011fingerprint(x)=11011 與其映射的三個slot: slot1,slot2,slot3 進行按位異或,將結果放在能唯一標識其結果的slot3中。slot3′sk?bits=11011?10010?0000=01001slot3's k-bits = 11011\bigoplus10010\bigoplus0000=01001slot3′sk?bits=11011?10010?0000=01001
5 同理回填其他的item 即可得到最終的xorfilter BBB 的結果
可以看到slot0和slot5 還都沒有被映射過,但所有的item都已經處理完了。 這個時候,我們就將所有沒有被映射過的slot設置為k-bit0,最終得到的 nnn 個items 的 BBB數組,即xorfilter 如下:
當我們查找的時候需要 通過選擇的 ddd個hash函數 得到一個指紋標識,fingerprint(x)=h1(x)?h2(x)?h3(x)fingerprint(x) = h1(x)\bigoplus h2(x) \bigoplus h3(x)fingerprint(x)=h1(x)?h2(x)?h3(x),拿著這個結果和B(h1(x))?B(h2(x))?B(h3(x))B(h1(x)) \bigoplus B(h2(x)) \bigoplus B(h3(x))B(h1(x))?B(h2(x))?B(h3(x)) 進行比對,相等,則說明這個item是在這個xorfilter之中的。因為從上面的圖中我們看到,對于@來說 ,他的指紋函數 很明顯是由 B中的三個slot 按位異或得到的。
xor filter 構造總結
xorfilter BBB 的構造過程主要是分為兩步:
- 構造唯一slot和item的映射: 將所有的item 按照ddd 個hash 函數映射到 mmm 個slot中,并且通過 peel 的過程 標記哪一個slot能唯一標識當前item
- 回填其他的 item,回填的過程就是完整構造 BBB的過程,將每一個item 的finger-print 與 對應slot 的k-bits 按位異或,得到的結果放在能唯一標識它的slot中即可。
第一步的大體的算法如下:
最終返回的結果δ\deltaδ 是一個棧,每一個元素就是我們的item 和 唯一標識它的 slot的下標的映射。
第二步的算法如下,主要是構造我們的xor-filter BBB
XOR-filter 和 ADD-filter對比
總的來說XOR-filter相比于ADD-filter有如下特點:
- 性能:XOR 查找性能相對更高一些。因為xor 過濾器采用的是bits 分組,所以每一次查找時的 hash函數之間的XOR操作都是按組進行,實現起來對cache更加友好,并不像 bit過濾器 的按位ADD操作都是隨機的,很有可能造成cache-miss。
- 空間消耗:實際測試過程中XOR 過濾器和 ADD過濾器在提供相同的FP時,XOR 每一個item 平均消耗1.23n * item的空間,而ADD 則需要消耗1.44n * item。
- CPU消耗:XOR消耗更多的cpu,因為構建的時候 除了基本的bits 分組之外,需要構建一個核心的fingerprint 函數。
XOR-filter 在計算上的優化
說了這么多xor-filter的實現原理,現在我們大體知道了xor-filter的構造過程,接下來我們來看看其中可以優化的部分(也就是ribbon-filter的主要目的)。
以下幾個關鍵變量:
- BBB 存放最終的k-bit 的 xor filter
- nnn 是參與構造當前 xor filter的所有key/items
- ddd 是 hash 函數的個數
- mmm 是slot 的個數
- bbb 是每一個slot的bit位數
從宏觀來看,對于每一個輸入的item,我們的結果是一個數組的數組S=m?bS=m*bS=m?b,而輸入是 B 已經存在的 m?bm*bm?b的系數,輸出是R=d?bR=d*bR=d?b 每一個key會有ddd個hash 函數進行映射 。
所以,我們可以抽象成 矩陣運算(每一個slot都是bit數組)在,這樣我們就得到了一個矩陣行列式:
B?S=RB * S = R B?S=R
當然,構造這個行列式是ribbon-filter的過程。
為什么要將原來的 ?\bigoplus? 轉化為矩陣運算呢?
主要還是效率問題,我們通過算法可以看到 對于BBB的操作 最開始需要先對所有元素進行映射,后面找到了唯一的元素在BBB的 slot標識之后 要進行解映射;更耗CPU的是回帶操作,因為不論這個slot是否全是0, 都需要進行回帶。
而如果我們將這么多的 bit位的?\bigoplus? 操作都轉化為矩陣運算,高效的消元一波即可得到最終每一個元素在當前slot應該存放的結果。
Ribbon filter
因為ribbon filter的主體是xor-filter 的實現,在其上構造可以高效運算的矩陣,從而利用矩陣本身的一些高效運算來解決構造xor-filter 過程中的重復操作。
所以,我們下面我們主要介紹的是 ribbon-filter 的高斯消元的主體過程,其本身的輸入和輸出都是和xor-filter一樣,并且判斷key是否存在的邏輯也一樣。當然,相比于xor-filter存在的問題顯而易見,因需要為高斯消元服務而消耗過多的計算-- CPU,其查找效率以及存儲成本都和xor-filter大同小異(存儲成本主要是受mmm slot的個數 以及 kkk 每一個slot中bit位的個數 影響 ,容錯率主要受ddd hash函數影響)。
高斯消元法
在描述高斯消元法在ribbon filter中如何使用之前,我們先了解一下什么是高斯消元法。
我們在學習線性代數時 會有提到通過行列式運算 解線性方程組的過程,其實高斯消元法就是用在求解線性方程組中,當然本身是將一個行列式轉換為上三角或者下三角矩陣的過程。
我們直接來看一個高斯消元法解線性方程組的過程,如下方程組
{2x+y?z=8(L1)?3x?y+2z=?11(L2)?2z+y+2z=?3(L3)\begin{cases} 2x+y-z=8 & (L1)\\ -3x-y+2z=-11&(L2)\\ -2z+y+2z=-3&(L3) \\\end{cases} ??????2x+y?z=8?3x?y+2z=?11?2z+y+2z=?3?(L1)(L2)(L3)?
對于這個方程組來說,我們原本的求解過程也是消元,通過L2-L3 消除zzz,得到一個等式;再將L1*2 + L2上得到一個等式;這樣我們就有一個二元一次方程組,僅有xxx和yyy兩個變量;但是,假如我們不是三元一次方程組,我們10元一次方程組,那這種方式的消元消耗的代價就非常之大。
而行列式 以及 矩陣運算 也就是高斯消元法 則能夠極大得簡化這個過程,大體原理是上面說到的,將線性方程組每一個未知數的系數 以及 等式的結果一起構造成一個增廣矩陣;將這個矩陣通過行列式的性質轉換成一個下三角矩陣就可以通過回代法進行方程組求解了,具體步驟如下:
-
構造增廣矩陣,即線性方程組的系數矩陣 一起 其結果常量 如下:
[21?18?3?12?11?212?3]\left[ \begin{array}{ccc|c} 2 & 1 & -1 & 8\\ -3 & -1 & 2 & -11 \\ -2 & 1 & 2 & -3 \end{array} \right] ???2?3?2?1?11??122?8?11?3???? -
由行列式的性質:對行列式的某一行乘以一個數,加到另一行上去,整個行列式的值保持不變。
這個性質證明也是很好證明的,因為上面有一個行所有的元素是兩個元素相加得到的,那我們可以將這一個行列式進行拆分。
比如對于如下矩陣:
[a11a12?a1na21a22?a2n????an1an2?ann]\left[ \begin{matrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \\ \end{matrix} \right] ??????a11?a21??an1??a12?a22??an2???????a1n?a2n??ann????????
將r1?3r1*3r1?3加到r2r2r2之上:[a11a12?a1na21+3?a11a22+3?a12?a2n+3?a1n????an1an2?ann]?[a11a12?a1na21a22?a2n????an1an2?ann]+[a11a12?a1n3?a113?a12?3?a1n????an1an2?ann]\left[ \begin{matrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} +3*a_{11} & a_{22} + 3*a_{12} & \cdots & a_{2n} + 3*a_{1n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \\ \end{matrix} \right] \\ \Rightarrow \left[ \begin{matrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{21} & a_{22} & \cdots & a_{2n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \\ \end{matrix} \right] + \left[ \begin{matrix} a_{11} & a_{12} & \cdots & a_{1n} \\ 3*a_{11} & 3*a_{12} & \cdots & 3*a_{1n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \\ \end{matrix} \right] ??????a11?a21?+3?a11??an1??a12?a22?+3?a12??an2???????a1n?a2n?+3?a1n??ann???????????????a11?a21??an1??a12?a22??an2???????a1n?a2n??ann????????+??????a11?3?a11??an1??a12?3?a12??an2???????a1n?3?a1n??ann????????
可以看到拆和之后的兩個矩陣中的一個和原本的矩陣是一樣的,另一個矩陣則有對應行成比例(r1r1r1和r2r2r2)的矩陣,我們可以將這個行的公因數提取出來作為矩陣的系數,也就是這個矩陣會有兩個一樣的行,當這個矩陣按行展開之后因為存在兩個一樣的行,他們的正值和負值之和的絕對值是恰好想等的,所以這個矩陣的展開值就是0。
3?[a11a12?a1na11a12?a1n????an1an2?ann]3*\left[ \begin{matrix} a_{11} & a_{12} & \cdots & a_{1n} \\ a_{11} & a_{12} & \cdots & a_{1n} \\ \vdots & \vdots & \ddots & \vdots \\ a_{n1} & a_{n2} & \cdots & a_{nn} \\ \end{matrix} \right] 3???????a11?a11??an1??a12?a12??an2???????a1n?a1n??ann????????
當然,還可以這樣更直觀的理解,n?nn*nn?n行列式是求n維空間的體積,也就是3*3的行列式是求一個三維空間的體積,每一個行代表一個三維空間的點,而三個點則可以表示一個立體空間,那么當兩個點是成比例的時候整個三維空間就只能變成二維投影了,二維空間的體積肯定是0了。 也就是按和展開后的行列式就只剩下第一個矩陣了,而這個矩陣就是原本的矩陣。
也就是這個性質是沒有任何問題的,那我們拿著這個性質: 對行列式的某一行乘以一個數,加到另一行上去,整個行列式的值保持不變;就可以開始愉快的開始消元增廣矩陣了:
[21?18?3?12?11?212?3]\left[ \begin{array}{ccc|c} 2 & 1 & -1 & 8\\ -3 & -1 & 2 & -11 \\ -2 & 1 & 2 & -3 \end{array} \right] ???2?3?2?1?11??122?8?11?3????
-
矩陣消元過程,目的是轉化系數矩陣位下三角:
a. 執行 r1?32+r2r1*\frac{3}{2} + r2r1?23?+r2 以及 r1+r2r1+r2r1+r2 消元得到如下矩陣
[21?1801/21/210215]\left[ \begin{array}{ccc|c} 2 & 1 & -1 & 8\\ 0 & 1/2 & 1/2 & 1 \\ 0 & 2 & 1 & 5 \end{array} \right] ???200?11/22??11/21?815????
b. 執行 ?r2?4+r3-r2 *4 + r3?r2?4+r3,消元得到了系數矩陣的下三角矩陣
[21?1801/21/2100?11]\left[ \begin{array}{ccc|c} 2 & 1 & -1 & 8\\ 0 & 1/2 & 1/2 & 1 \\ 0 & 0 & -1 & 1 \end{array} \right] ???200?11/20??11/2?1?811???? -
回帶,整個系數矩陣已經變成了下三角矩陣,我們可以將每一行看作原本的方程組。
{2x+y?z=8(L1)12y+12z=1(L2)?z=1(L3)\begin{cases} 2x+y-z=8 & (L1)\\ \frac{1}{2}y+\frac{1}{2}z=1&(L2)\\ -z=1&(L3) \\\end{cases} ??????2x+y?z=821?y+21?z=1?z=1?(L1)(L2)(L3)?
其中L3L3L3可以看到已經能夠得到?z=1-z = 1?z=1,則z=?1z = -1z=?1;將這個結果回帶到前面的兩個方程中可以得到y=1,x=3y=1, x = 3y=1,x=3。
總結
filter 存在的目的是為了加速我們的單機讀性能,有效減少持久化存儲中的I/O次數。
因為 單機存儲系統是一個融合CPU/內存/磁盤 的系統,我們想要提升上層應用的QOS,那我們就得付出對應的CPU或者內存或者磁盤某一方面的代價。
- 對于xor-filter以及ribbon-filter來說,能夠節約存儲成本(1.23n*item, addfilter 是 1.42n *item),同時能夠擁有相比于add-filter更高效的判斷性能;而且ribbon 為了加速xor-filter的效率,通過高斯消元算法會消耗更多的CPU。
- 當然性能最好的肯定是blocked-bloomfilter,它可以按照cpu的cache-lin進行構造,但是因為實際場景它會由多個filter組成,會有存儲上的冗余。同樣的fp下會消耗 30%額外的內存。
更多實現和測試可以參考下面的引文。
參考
- Ribbon filter: practically smaller than Bloom and Xor
- Xor filter: Faster and Smaller Than Bloom and Cuckoo Filters
- Rocksdb - master
- xor-filter implemetation
- All filter code
總結
以上是生活随笔為你收集整理的Rocksdb Ribbon Filter : 结合 XOR-filter 以及 高斯消元算法 实现的 高效filter的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么合理安排时间
- 下一篇: 每个版本倚天屠龙记里的明教山洞拍摄地,是