HBase的布隆过滤器详解

1.布隆过滤器的简单介绍

 布隆过滤器(Bloom Filter): 由一个很长的二进制向量和一系列随机的哈希函数组成. 它的作用不是过滤数据, 而是判断当前这条数据是否已经存在 在了当前的数据集合中.

2.布隆过滤器的原理分析

2.1 哈希表存在的问题

 hash是内存中使用的经典数据结构.
 当我们需要判断一个元素是否在一个集合当中时, 我们可以先对集合生成一个hash表, 将当前元素转为对应的hash值(用的是同一个hash函数), 然后判断当前hash值是否在hash表中, 即可知元素是否在集合中.
 然而在大数据场景下(PB), 很多时候, hash就显得力不从心. 因为在海量数据下hash要占用大量的内存空间, 且远远超过了我们能提供的内存大小
 例如在黑名单过滤当中, 我们有100亿的网站黑名单url需要过滤, 家事儿一个url是64bytes. 如果我们用hash 表, 那么至少需要6400亿字节 即640GB的内存空间(实际所需空间还远大于此), 空间消耗巨大, 必须要多个服务器来同时分摊内存.

2.2 布隆过滤器的原理

 在上述背景下, 我们就需要使用更加精简的结构来替代hash表, 布隆过滤器就是这样一个高度节省空间的结构, 并且其时间也元超一般算法.

2.2.1 原理详解

 布隆过滤器实际就是一种集合. 假设我们有一个数组, 它的长度为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判定为黑名单当中的, 它的失误可以说是宁可错杀,不可放过. 不过布隆过滤器的失误率是可调节

2.2.2 布隆过滤器失误率的调节

 很明显通过增长array数组的长度可以减少失误率, 但是也会带来布隆过滤器占用的内存增加的问题, 所以另一种方式是使用多个hash函数, 这样一个url(一条数据)就会有多个hashcode, 会在array中对应多个描黑bit, 一个url只有多个被描黑的bit位与某个黑名单url的描黑的bit位匹配时才说明当前url是黑名单网站.

2.2.3 布隆过滤器的完整体

 所以,正如本文开头所说的: 布隆过滤器有一个二进制向量和一系列哈希函数组成. 二进制向量的长度和哈希函数的个数就影响着布隆过滤器的失误率.

 首先, 我们要使用多少个哈希函数呢?
 这一点只与我们黑名单中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]中

3.HBase是如何使用布隆过滤器的

 布隆过滤器是hbase中的高级功能,是列族下的一个属性, 它能够减少特定访问模式(get/scan)下的查询时间. 不过由于这种模式增加了内存和存储的负担,所以被默认认为关闭状态.
 hbase支持如下类型的布隆过滤器:
  1、NONE  不使用布隆过滤器
  2、ROW  行键使用布隆过滤器
  3、ROWCOL  行列键一起使用布隆过滤器
 其中ROWCOL是粒度更细的模式.
 ROW: 将一个列族中的所有rowkey取hashcore, 然后去对应的二进制向量中描黑.
 ROWCOL:将一个列族下的所有rowkey+ 列值取hashcode, 然后去对应的二进制向量中描黑

3.1 HBase的基本回顾

 我们知道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)都来自同一列族, 所以他们的行键分布类似.

3.2 HBase的块索引机制及存在的问题

 在介绍为什么HBase要引入布隆过滤器之前, 我们先来了解一下HBase存储文件的块索引机制
 由上图, 我们假设当前列族对应的store下有6个HFile文件, 针对某个特定rowkey的块索引机制为:
  1、首先根据rowkey到每个HFile中索引块中查找包含当前rowkey的数据块, 并返回. 假设每个HFile中刚好都有2个数据块的起始rowkey满足条件, 这样就返回了12个数据块.
  2、regionserver需要加载每一个数据块来检查该块中是否真的包含有该rowkey对应的数据.

 所以HBase本身的块索引机制的问题就是,返回的数据块过多, 每个都需要加载, 确认其中是否有对应的数据.

3.3 HBase布隆过滤器的作用

 针对块索引机制存在的问题, 我们对store中的每个HFile设置布隆过滤器(为每个HFile文件建立一个二进制向量), 此时针对特定rowkey的查询就变为:
 1、首先rowkey对每个HFile的布隆过滤器使用, 如果当前rowkey对应的hashcode取余操作, 刚好对应上HFile文件中某条数据的二进制向量描黑, 说明当前HFile中一定还有rowkey对应的数据(不考虑布隆过滤器本身的误差)
 2、由上图知: 返回的数据块由原来的12个, 减少为4个, 大大降低了regionsever加载数据块的时间, 从而增加了HBase集群的吞吐量

4.大战后的悠闲时光

4.1 布隆过滤器的存储在哪?

 对于HBase而言, 当我们选择采用布隆过滤器后, HBase会在生成storeFile(HFile)时包含一份布隆过滤器结构的数据, 称其为MetaBlock; MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护. 所以, 开启BloomFilter会有一定的存储及内存cache开销. 但是在大多数情况下, 这些负担相对于布隆过滤器带来的好处是可以接受的.

4.2 如何开启布隆过滤器

 create ‘table’, {BLOOMFILTER => ‘ROW’}
 HColumnDescriptor.setBloomFilterType(NONE | ROW | ROWCOL)

4.3 采用布隆过滤器后, HBase如何get数据

 在读取数据时, HBase会首先在布隆过滤器中查询, 根据布隆过滤器的结果, 再在memstore中查询, 最后再去对应的HFile中查询.

4.4 采用ROW还是ROWCOl布隆过滤器

 这取决于用户的使用模式, 如果用户只做行扫描, 使用更加细粒度的行加列布隆过滤器不会有任何的帮助, 这种场景就应该使用行级布隆过滤器. 当用户不能批量更新特定的一行, 并且最后的使用存储文件都含有该行的一部分时, 行加列级的布隆过滤器更加有用

 例如: 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。

5.参考博客

https://blog.csdn.net/qq_38180223/article/details/80922114  好吧, 你要说我是在抄袭人家的, 我也认.