布隆过滤器(Bloom Filter): 由一个很长的二进制向量和一系列随机的哈希函数组成. 它的作用不是过滤数据, 而是判断当前这条数据是否已经存在 在了当前的数据集合中.
hash是内存中使用的经典数据结构.
当我们需要判断一个元素是否在一个集合当中时, 我们可以先对集合生成一个hash表, 将当前元素转为对应的hash值(用的是同一个hash函数), 然后判断当前hash值是否在hash表中, 即可知元素是否在集合中.
然而在大数据场景下(PB), 很多时候, hash就显得力不从心. 因为在海量数据下hash要占用大量的内存空间, 且远远超过了我们能提供的内存大小
例如在黑名单过滤当中, 我们有100亿的网站黑名单url需要过滤, 家事儿一个url是64bytes. 如果我们用hash 表, 那么至少需要6400亿字节 即640GB的内存空间(实际所需空间还远大于此), 空间消耗巨大, 必须要多个服务器来同时分摊内存.
在上述背景下, 我们就需要使用更加精简的结构来替代hash表, 布隆过滤器就是这样一个高度节省空间的结构, 并且其时间也元超一般算法.
布隆过滤器实际就是一种集合. 假设我们有一个数组, 它的长度为L, 从0 ~ (L-1)位置上, 存储的不是一个字符串或整数, 而是一个bit, 这意味着它的取值只能为0或1.
例如我们实现如下的一个数组:
int[] array = new int[1000];
1 int = 4byte = 32bit, 所以我们申请的array数组中包含有32000个bit位. 此时我们想将第20003个bit位描黑, 将其改为1, 这样我们需要怎么做呢?
首先我们需要定位, 这第20003个bit位于array的索引值, 接着我们需要定位该bit位于该索引值的第几个bit位.
int index = 20003
int intIndex = index / 32 向下取整
int bitIndex = index % 32 取余
描黑:
array[intIndex] = (array[intIndex] | (1 << (bitIndex - 1)))
上述代码块的解释:
intIndex = 625, bitIndex = 3, 所以第20003个bit位于array的第625索引位置(加1, 减1)的第3个bit位.
value << num: 表示的是将value左移num位. 所以上述位运算符的右侧就是将数值1 左移2位, 得到的结果为:
0000000 00000000 00000000 00000100
java中int类型的初始值为0, 所以array[intIndex] 与上述结果做或的位运算的结果就是将array[intIndex]的第3bit位赋值为1(描黑).
有了这些基础后, 我们如何设计黑名单问题呢?
我们设置一个长度为m个bit的数组,将100亿个黑名单网站url, 通过同一个hash函数计算出hashcode, 然后hashcode % m 将对应数组的bit位置描黑. 此时新来一个网站url, 计算其hashcode, 然后hashcore % m 后看对应的bit位上是否已经描黑, 描黑说明新的url是黑名单网站, 没有描黑说明新的url是正常网站
但是布隆过滤器存在一定的失误率,正常url的 hashcode % m
与黑名单中某个url的hashcode % m 的结果相同时就会出现失误. 但好处就是: 布隆过滤器绝不会将黑名单中网页查错, 但是有可能将正常的网页URL判定为黑名单当中的, 它的失误可以说是宁可错杀,不可放过. 不过布隆过滤器的失误率是可调节的
很明显通过增长array数组的长度可以减少失误率, 但是也会带来布隆过滤器占用的内存增加的问题, 所以另一种方式是使用多个hash函数, 这样一个url(一条数据)就会有多个hashcode, 会在array中对应多个描黑bit, 一个url只有多个被描黑的bit位与某个黑名单url的描黑的bit位匹配时才说明当前url是黑名单网站.
所以,正如本文开头所说的: 布隆过滤器有一个二进制向量和一系列哈希函数组成. 二进制向量的长度和哈希函数的个数就影响着布隆过滤器的失误率.
首先, 我们要使用多少个哈希函数呢?
这一点只与我们黑名单中URL的数量及二进制向量长度有关, 而与单个URL的大小无关(哈希函数的输入域是无穷的). 它只要求满足我们的哈希函数能够接受URL这一参数类型即可.
而我们的数组的大小长度与什么有关呢?
它同样与我们黑名单中URL的数量有关, 除此之外它还与我们能够接受的失误率有关.
下面我们给出有关公式:
m = -n * lnp / (ln2) ^ 2 # n为样本数量, p为预计的失误率
当我们的样本量为100亿, 而我们预计的失误率为万分之一, 根据这个公式我们便可以得到m为: 131571428572bit ~ 23GB(向上取整).对比hash表, 原来我们需要640GB, 而现在只需要23GB, 大大节省了内存空间的消耗. 而我们所需要的哈希函数的个数k的数学公式为:
k = ln2 * m / k # m为数组长度, n为样本数量, k向上取整
这里经过计算n为13
因为我们的m和n都是经过向上取整, 所以我们的实际失误率会变得更低, 失误率的计算公式为:
p = (1 - e(-k*n/m))^k #n为样本数量, m为数组长度, k为哈希函数个数
经过我们向上取整后, 计算出来的实际失误率为十万分之六.
所以布隆过滤器相当于将数据集中的每条数据做了一下转换存储, 存储在了一个Array[Bit]中, 但其目的不是为了存储, 而是为了判断新的一条数据是否也在这个Array[Bit]中
布隆过滤器是hbase中的高级功能,是列族下的一个属性, 它能够减少特定访问模式(get/scan)下的查询时间. 不过由于这种模式增加了内存和存储的负担,所以被默认认为关闭状态.
hbase支持如下类型的布隆过滤器:
1、NONE 不使用布隆过滤器
2、ROW 行键使用布隆过滤器
3、ROWCOL 行列键一起使用布隆过滤器
其中ROWCOL是粒度更细的模式.
ROW: 将一个列族中的所有rowkey取hashcore, 然后去对应的二进制向量中描黑.
ROWCOL:将一个列族下的所有rowkey+ 列值取hashcode, 然后去对应的二进制向量中描黑
我们知道HBase的实际存储结构是HFile, 它是位于HDFS系统中的, 也就是在磁盘中, 而加载到内存中的数据存储在memstore, 当memstore中的数据达到一定阈值shi,他会将数据存入HFile中.
一个HFile是由多个数据块(64KB)和一个索引块组成的, HBase是通过块索引来访问这些数据块的. 而索引是由每个数据块的第一行数据的rowkey组成的.当HBase打开一个HFile时, 块索引信息会优先加载到内存当中. 然后HBase会通过这些块索引来查询数据
但是块索引是相当粗粒度的, 我们可以简单计算一下. 假设一行占用100bytes空间, 所以一个数据块64KB, 所包含的行大概: (64 * 1024) / 100 ~ 700行.而我们只能从索引给出的一个数据块的起始行开始查询
如果用户随机查找一个行键, 则这个行键很可能位于两个开始键(即索引)之间的位置. 对应HBase来说, 它判断你这个行键是否真实存在的唯一方法就是加载这个数据块, 并且扫描它是否包含这个键.
先思考两个问题,:
数据块中的rowkey是否是连续的?
数据块之间的起始rowkey是否是连续的?
都不是连续的, 因为HBase是支持随机读写的, 且HBase支持多版本数据, 且对于删除,修改操作的数据并不是直接删除, 修改HBase中的数据, 而是新增这条数据, 并给这条数据打上删除, 修改的标识, 且在minor合并后这些打上标识的数据还是存储在HFile中, 只有major合并时, HBase才会根据标识做真正的删除, 修改数据.
所以由上述两个问题得出:
1. 一条数据(一个rowkey)可能会在一个HFile中找到对应的多个数据块(根据起始rowkey).
2.找到的对应的数据块中也不一定含有rowkey对应的数据
同时, 还存在很多情况使得这种情况更加复杂
对于一个应用来说, 用户通常会以一定的速率进行更新数据, 这就将导致内存中的数据被刷新到磁盘中, 并且之后系统会将他们合并成更大的存储文件(minor). 在HBase的合并存储文件的时候, 它仅仅会合并最近几个存储文件, 直到合并的存储文件达到配置的最大大小. 最终系统会有很多的存储文件(一个列族有很多的HFile), 所有的存储文件都是候选文件,其可能包含用户请求行键的单元格.如下图所示:
我们可以看到, 这些不同的文件(HFile)都来自同一列族, 所以他们的行键分布类似.
在介绍为什么HBase要引入布隆过滤器之前, 我们先来了解一下HBase存储文件的块索引机制
由上图, 我们假设当前列族对应的store下有6个HFile文件, 针对某个特定rowkey的块索引机制为:
1、首先根据rowkey到每个HFile中索引块中查找包含当前rowkey的数据块, 并返回. 假设每个HFile中刚好都有2个数据块的起始rowkey满足条件, 这样就返回了12个数据块.
2、regionserver需要加载每一个数据块来检查该块中是否真的包含有该rowkey对应的数据.
所以HBase本身的块索引机制的问题就是,返回的数据块过多, 每个都需要加载, 确认其中是否有对应的数据.
针对块索引机制存在的问题, 我们对store中的每个HFile设置布隆过滤器(为每个HFile文件建立一个二进制向量), 此时针对特定rowkey的查询就变为:
1、首先rowkey对每个HFile的布隆过滤器使用, 如果当前rowkey对应的hashcode取余操作, 刚好对应上HFile文件中某条数据的二进制向量描黑, 说明当前HFile中一定还有rowkey对应的数据(不考虑布隆过滤器本身的误差)
2、由上图知: 返回的数据块由原来的12个, 减少为4个, 大大降低了regionsever加载数据块的时间, 从而增加了HBase集群的吞吐量
对于HBase而言, 当我们选择采用布隆过滤器后, HBase会在生成storeFile(HFile)时包含一份布隆过滤器结构的数据, 称其为MetaBlock; MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护. 所以, 开启BloomFilter会有一定的存储及内存cache开销. 但是在大多数情况下, 这些负担相对于布隆过滤器带来的好处是可以接受的.
create ‘table’, {BLOOMFILTER => ‘ROW’}
HColumnDescriptor.setBloomFilterType(NONE | ROW | ROWCOL)
在读取数据时, HBase会首先在布隆过滤器中查询, 根据布隆过滤器的结果, 再在memstore中查询, 最后再去对应的HFile中查询.
这取决于用户的使用模式, 如果用户只做行扫描, 使用更加细粒度的行加列布隆过滤器不会有任何的帮助, 这种场景就应该使用行级布隆过滤器. 当用户不能批量更新特定的一行, 并且最后的使用存储文件都含有该行的一部分时, 行加列级的布隆过滤器更加有用
例如: ROW 使用场景假设有2个Hfile文件hf1和hf2, hf1包含kv1(r1 cf:q1 v)、kv2(r2 cf:q1 v) hf2包含kv3(r3 cf:q1 v)、kv4(r4 cf:q1 v) 如果设置了CF属性中的bloomfilter(布隆过滤器)为ROW,那么get(r1)时就会过滤hf2,get(r3)就会过滤hf1 。
ROWCOL使用场景假设有2个Hfile文件hf1和hf2, hf1包含kv1(r1 cf:q1 v)、kv2(r2 cf:q1 v) hf2包含kv3(r1 cf:q2 v)、kv4(r2 cf:q2 v) 如果设置了CF属性中的bloomfilter为ROW,无论get(r1,q1)还是get(r1,q2),都会读取hf1+hf2;而如果设置了CF属性中的bloomfilter为ROWCOL,那么get(r1,q1)就会过滤hf2,get(r1,q2)就会过滤hf1。
https://blog.csdn.net/qq_38180223/article/details/80922114 好吧, 你要说我是在抄袭人家的, 我也认.