文章目录
前言
crm开发定制还是起源于前几天的Rocksdb meetup,其中Peter C. Dillinger
crm开发定制这位大佬分享了自己为rocksdb实现的 filter。这个Filter从2020年5crm开发定制月份左右提的第一个PR crm开发定制到现在基本已经成熟了,crm开发定制也已经合入到了rocksdb-master中。同时,作者因为 ribbon-filter的设计,crm开发定制写了放在了arxiv
crm开发定制上保持自己的原创性。
Ribbon
filter的全称是Rapid Incremental Boolean Banding ON the fly
,crm开发定制能够基于真值矩阵 crm开发定制高效得用增量方式构建过滤器。
crm开发定制相比于传统的Bloom Filter 来说,crm开发定制它的核心差异以及 crm开发定制优势主要是如下几点:
- 采用XOR crm开发定制crm开发定制方式进行按位匹配。crm开发定制而大多数的bloom filter都是ADD 方式进行按位匹配。
- ribbon filter的构成是 在原始key经过多个hash函数之后 crm开发定制由一个二维bit数组组成,crm开发定制并不是像传统的filter 由一个bit数组组成。
- 对CPU cache 更友好,检查一个key 是否在filter内部,ribbon filter crm开发定制可以直接加载二位数组中的一个bitcrm开发定制数组进行顺序匹配,而传统ADD filtercrm开发定制则需要在一个bitcrm开发定制数组上根据hashcrm开发定制函数的结果随机匹配。
- crm开发定制对空间更友好,ribbon filtercrm开发定制存储二位数组时可以通crm开发定制过高斯消元法转化成上三角矩阵,更利于压缩;且加载到内存中也更利于计算。
- 消耗更多的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的构造,大体逻辑如下,定义以下几个关键变量:
- B B B 存放最终的k-bit 的 xor filter
- n n n 是参与构造当前 xor filter的所有key/items
- d d d 是 hash 函数的个数
- m m m 是slot 的个数
1 初始的 B B B 数组的大小是 m = 7 m=7 m=7,每一个item/key 经过 d = 3 d=3 d=3个 hash函数 h 1 , h 2 , h 3 h1,h2,h3 h1,h2,h3 的映射 分布到三个 B B B的slot之中。每一个item的 k-bits值的计算是通过 h 1 ( x ) ⨁ h 2 ( x ) ⨁ h 3 ( x ) h1(x) \bigoplus h2(x) \bigoplus h3(x) h1(x)⨁h2(x)⨁h3(x) 得到的,也可以看作 f i n g e r p r i n t ( x ) fingerprint(x) fingerprint(x),即 item的指纹标识。
首先我们看一下 @
会通过三个hash函数 映射到 slot7, slot6, slot4中,我们可以看到slot7是被@
独享的,没有被其他的 item映射到,所以slot7 能够唯一标识 @
,对slot7做一个逻辑标记。对于ADD filter来说,这里的slot就是一个bit位。
2 接下来我们将@
的映射结果从 B B B 的 7 个slot中剥离下来,即对应的slot 计数-- 即可,能够看到slot6 是可以唯一标识+
item,同样,我们也对slot6 做一个+
的标记。
3 依次 处理完所有的 input items/keys,直到最后一个 ≈ \approx ≈,这个时候,将最后一个符号的 fingerprint(x)
的结果,也就是 h 1 ( x ) ⨁ h 2 ( x ) ⨁ h 3 ( 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 ≈ 的 f i n g e r p r i n t ( x ) = 10010 fingerprint(x) = 10010 fingerprint(x)=10010 的结果 与slot2,slot4 的结果按位异或 得到的结果 然后放到slot1中。 s l o t 1 ′ s k − b i t s = 10010 ⨁ 00000 ⨁ 00000 = 10010 slot1's k-bits = 10010 \bigoplus 00000 \bigoplus 00000 = 10010 slot1′sk−bits=10010⨁00000⨁00000=10010
4 回填其他的item,比如⭐️
和前面的操作一样,将标识⭐️ 的 f i n g e r p r i n t ( x ) = 11011 fingerprint(x) = 11011 fingerprint(x)=11011 与其映射的三个slot: slot1,slot2,slot3 进行按位异或,将结果放在能唯一标识其结果的slot3中。 s l o t 3 ′ s k − b i t s = 11011 ⨁ 10010 ⨁ 0000 = 01001 slot3's k-bits = 11011\bigoplus10010\bigoplus0000=01001 slot3′sk−bits=11011⨁10010⨁0000=01001
5 同理回填其他的item 即可得到最终的xorfilter B B B 的结果
可以看到slot0和slot5 还都没有被映射过,但所有的item都已经处理完了。 这个时候,我们就将所有没有被映射过的slot设置为k-bit0,最终得到的 n n n 个items 的 B B B数组,即xorfilter 如下:
当我们查找的时候需要 通过选择的 d d d个hash函数 得到一个指纹标识, f i n g e r p r i n t ( x ) = h 1 ( x ) ⨁ h 2 ( x ) ⨁ h 3 ( x ) fingerprint(x) = h1(x)\bigoplus h2(x) \bigoplus h3(x) fingerprint(x)=h1(x)⨁h2(x)⨁h3(x),拿着这个结果和 B ( h 1 ( x ) ) ⨁ B ( h 2 ( x ) ) ⨁ B ( h 3 ( 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 B B B 的构造过程主要是分为两步:
- 构造唯一slot和item的映射: 将所有的item 按照 d d d 个hash 函数映射到 m m m 个slot中,并且通过 peel 的过程 标记哪一个slot能唯一标识当前item
- 回填其他的 item,回填的过程就是完整构造 B B B的过程,将每一个item 的finger-print 与 对应slot 的k-bits 按位异或,得到的结果放在能唯一标识它的slot中即可。
第一步的大体的算法如下:
最终返回的结果 δ \delta δ 是一个栈,每一个元素就是我们的item 和 唯一标识它的 slot的下标的映射。
第二步的算法如下,主要是构造我们的xor-filter B B B
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的主要目的)。
以下几个关键变量:
- B B B 存放最终的k-bit 的 xor filter
- n n n 是参与构造当前 xor filter的所有key/items
- d d d 是 hash 函数的个数
- m m m 是slot 的个数
- b b b 是每一个slot的bit位数
从宏观来看,对于每一个输入的item,我们的结果是一个数组的数组 S = m ∗ b S=m*b S=m∗b,而输入是 B 已经存在的 m ∗ b m*b m∗b的系数,输出是 R = d ∗ b R=d*b R=d∗b 每一个key会有 d d d个hash 函数进行映射 。
所以,我们可以抽象成 矩阵运算(每一个slot都是bit数组)在,这样我们就得到了一个矩阵行列式:
B ∗ S = R B * S = R B∗S=R
当然,构造这个行列式是ribbon-filter的过程。
为什么要将原来的 ⨁ \bigoplus ⨁ 转化为矩阵运算呢?
主要还是效率问题,我们通过算法可以看到 对于 B B B的操作 最开始需要先对所有元素进行映射,后面找到了唯一的元素在 B B B的 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大同小异(存储成本主要是受 m m m slot的个数 以及 k k k 每一个slot中bit位的个数 影响 ,容错率主要受 d d d hash函数影响)。
高斯消元法
在描述高斯消元法在ribbon filter中如何使用之前,我们先了解一下什么是高斯消元法。
我们在学习线性代数时 会有提到通过行列式运算 解线性方程组的过程,其实高斯消元法就是用在求解线性方程组中,当然本身是将一个行列式转换为上三角或者下三角矩阵的过程。
我们直接来看一个高斯消元法解线性方程组的过程,如下方程组
{ 2 x + y − z = 8 ( L 1 ) − 3 x − y + 2 z = − 11 ( L 2 ) − 2 z + y + 2 z = − 3 ( L 3 )
对于这个方程组来说,我们原本的求解过程也是消元,通过L2-L3 消除 z z z,得到一个等式;再将L1*2 + L2上得到一个等式;这样我们就有一个二元一次方程组,仅有 x x x和 y y y两个变量;但是,假如我们不是三元一次方程组,我们10元一次方程组,那这种方式的消元消耗的代价就非常之大。
而行列式 以及 矩阵运算 也就是高斯消元法 则能够极大得简化这个过程,大体原理是上面说到的,将线性方程组每一个未知数的系数 以及 等式的结果一起构造成一个增广矩阵;将这个矩阵通过行列式的性质转换成一个下三角矩阵就可以通过回代法进行方程组求解了,具体步骤如下:
-
构造增广矩阵,即线性方程组的系数矩阵 一起 其结果常量 如下:
[ 2 1 − 1 8 − 3 − 1 2 − 11 − 2 1 2 − 3 ] \left[\right] ⎣⎡2−3−21−11−1228−11−3⎦⎤ -
由行列式的性质:对行列式的某一行乘以一个数,加到另一行上去,整个行列式的值保持不变。
这个性质证明也是很好证明的,因为上面有一个行所有的元素是两个元素相加得到的,那我们可以将这一个行列式进行拆分。
比如对于如下矩阵:
[ a 11 a 12 ⋯ a 1 n a 21 a 22 ⋯ a 2 n ⋮ ⋮ ⋱ ⋮ a n 1 a n 2 ⋯ a n n ] \left[\right] ⎣⎢⎢⎢⎡a11a21⋮an1a12a22⋮an2⋯⋯⋱⋯a1na2n⋮ann⎦⎥⎥⎥⎤
将 r 1 ∗ 3 r1*3 r1∗3加到 r 2 r2 r2之上:[ a 11 a 12 ⋯ a 1 n a 21 + 3 ∗ a 11 a 22 + 3 ∗ a 12 ⋯ a 2 n + 3 ∗ a 1 n ⋮ ⋮ ⋱ ⋮ a n 1 a n 2 ⋯ a n n ] ⇒ [ a 11 a 12 ⋯ a 1 n a 21 a 22 ⋯ a 2 n ⋮ ⋮ ⋱ ⋮ a n 1 a n 2 ⋯ a n n ] + [ a 11 a 12 ⋯ a 1 n 3 ∗ a 11 3 ∗ a 12 ⋯ 3 ∗ a 1 n ⋮ ⋮ ⋱ ⋮ a n 1 a n 2 ⋯ a n n ] \left[
\right] \\ \Rightarrow \left[\right] + \left[\right] ⎣⎢⎢⎢⎡a11a21+3∗a11⋮an1a12a22+3∗a12⋮an2⋯⋯⋱⋯a1na2n+3∗a1n⋮ann⎦⎥⎥⎥⎤⇒⎣⎢⎢⎢⎡a11a21⋮an1a12a22⋮an2⋯⋯⋱⋯a1na2n⋮ann⎦⎥⎥⎥⎤+⎣⎢⎢⎢⎡a113∗a11⋮an1a123∗a12⋮an2⋯⋯⋱⋯a1n3∗a1n⋮ann⎦⎥⎥⎥⎤
可以看到拆和之后的两个矩阵中的一个和原本的矩阵是一样的,另一个矩阵则有对应行成比例( r 1 r1 r1和 r 2 r2 r2)的矩阵,我们可以将这个行的公因数提取出来作为矩阵的系数,也就是这个矩阵会有两个一样的行,当这个矩阵按行展开之后因为存在两个一样的行,他们的正值和负值之和的绝对值是恰好想等的,所以这个矩阵的展开值就是0。
3 ∗ [ a 11 a 12 ⋯ a 1 n a 11 a 12 ⋯ a 1 n ⋮ ⋮ ⋱ ⋮ a n 1 a n 2 ⋯ a n n ] 3*\left[\right] 3∗⎣⎢⎢⎢⎡a11a11⋮an1a12a12⋮an2⋯⋯⋱⋯a1na1n⋮ann⎦⎥⎥⎥⎤
当然,还可以这样更直观的理解, n ∗ n n*n n∗n行列式是求n维空间的体积,也就是3*3的行列式是求一个三维空间的体积,每一个行代表一个三维空间的点,而三个点则可以表示一个立体空间,那么当两个点是成比例的时候整个三维空间就只能变成二维投影了,二维空间的体积肯定是0了。 也就是按和展开后的行列式就只剩下第一个矩阵了,而这个矩阵就是原本的矩阵。
也就是这个性质是没有任何问题的,那我们拿着这个性质: 对行列式的某一行乘以一个数,加到另一行上去,整个行列式的值保持不变;就可以开始愉快的开始消元增广矩阵了:
[ 2 1 − 1 8 − 3 − 1 2 − 11 − 2 1 2 − 3 ] \left[
-
矩阵消元过程,目的是转化系数矩阵位下三角:
a. 执行 r 1 ∗ 3 2 + r 2 r1*\frac{3}{2} + r2 r1∗23+r2 以及 r 1 + r 2 r1+r2 r1+r2 消元得到如下矩阵
[ 2 1 − 1 8 0 1 / 2 1 / 2 1 0 2 1 5 ] \left[\right] ⎣⎡20011/22−11/21815⎦⎤
b. 执行 − r 2 ∗ 4 + r 3 -r2 *4 + r3 −r2∗4+r3,消元得到了系数矩阵的下三角矩阵
[ 2 1 − 1 8 0 1 / 2 1 / 2 1 0 0 − 1 1 ] \left[\right] ⎣⎡20011/20−11/2−1811⎦⎤ -
回带,整个系数矩阵已经变成了下三角矩阵,我们可以将每一行看作原本的方程组。
{ 2 x + y − z = 8 ( L 1 ) 1 2 y + 1 2 z = 1 ( L 2 ) − z = 1 ( L 3 )
⎩⎪⎨⎪⎧2x+y−z=821y+21z=1−z=1(L1)(L2)(L3)
其中 L 3 L3 L3可以看到已经能够得到 − z = 1 -z = 1 −z=1,则 z = − 1 z = -1 z=−1;将这个结果回带到前面的两个方程中可以得到 y = 1 , x = 3 y=1, x = 3 y=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%额外的内存。
更多实现和测试可以参考下面的引文。