hbase是大数据领域内提供数据的随机存储与随机访问的nomysql数据库。提供实时的数据访问(HBase 为了解决海量数据的扩展性,支持简单的增加节点来实现线性扩展,从而在集群上管理海量的非结构化或半结构化的稀疏数据。HBase 仅能通过主键(raw key)或主键的 range 检索数据,支持单行事务。)是一个分布式的,可扩展的数据库,是以key-value来进行列式存储的,通过rowkay,列族,列,时间戳,来定位和存储,访问数据的。提供数10亿的行乘与百万的
hbase是分布式非关系型数据库。hbase是基于hdfs的
hbase是根据gogle的bigtable设计出来的

HBase 位于结构化存储层,Hadoop HDFS 为 HBase 提供了高可靠性的底层存储支持,Hadoop MapReduce 为 HBase 提供了高性能的计算能力,Zookeeper 为 HBase 提供了稳定服务和 failover 机制。
Pig 和 Hive 还为 HBase 提供了高层语言支持,使得在 HBase 上进行数据统计处理变的非常简单。Sqoop 则为 HBase 提供了方便的 RDBMS 数据导入功能,使得传统数据库数据向 HBase 中迁移变的非常方便。

行健(Row Key):表的主键,表中的记录默认按照行健升序排序
时间戳(Timestamp):每次数据操作对应的时间戳,可以看作是数据的版本号
列族(Column Family):表在水平方向有一个或者多个列族组成,一个列族中可以由任意多个列组成,列族支持动态扩展,无需预先定义列的数量以及类型,所有列均以二进制格式存储,用户需要自行进行类型转换。所有的列族成员的前缀是相同的,例如 abc:a1 和 abc:a2 两个列都属于 abc 这个列族。
表和区域(Table&Region):当表随着记录数不断增加而变大后,会逐渐分裂成多份,成为区域一个区域是对表的水平划分,不同的区域会被 Master 分配给相应的 RegionServer 进行管理
单元格(Cell):表存储数据的单元。由{行健,列(列族:标签),时间戳}唯一确定,其中的数据是没有类型的,以二进制的形式存储。

Master 参与(寻址访问 Zookeeper 和 RegionServer,数据读写访问 RegionServer),Master 仅仅维护 Table 和 Region 的元数据信息,负载很低
HBase 访问接口
Native Java API,最常规和高效的访问方式,适合 Hadoop MapReduce Job 并行批处理 HBase 表数据。
HBase Shell,HBase 的命令行工具,最简单的接口,适合 HBase 管理使用。
Thrift Gateway,利用 Thrift 序列化技术,支持 C++,PHP,Python等多种语言,适合其他异构系统在线访问 HBase 表数据。
REST Gateway,支持 REST 风格的 Http API 访问 HBase, 解除了语言限制。
Pig可以使用 Pig Latin 流式编程语言来操作 HBase 中的数据,和 Hive 类似,本质最终也是编译成 MapReduce Job 来处理 HBase 表数据,适合做数据统计。
HBase 存储格式
HBase 中的所有数据文件都存储在 Hadoop HDFS 文件系统上,主要包括上述提出的两种文件类型:
HFile, HBase 中 KeyValue 数据的存储格式,HFile 是 Hadoop 的二进制格式文件,实际上 StoreFile 就是对 HFile 做了轻量级包装,即 StoreFile 底层就是 HFile。
HLogFile,HBase 中 WAL(Write Ahead Log) 的存储格式,物理上是 Hadoop 的 Sequence File。
HBase 的优势主要在以下几方面:
海量数据存储
快速随机访问
大量写操作的应用

常见的应用场景:
互联网搜索引擎数据存储(BigTable 要解决的问题)
审计日志系统
实时系统
消息中心
内容服务系统
HBase 的最底层结构是基于 hdfs 的,它将自己的日志文件 hlog,以及数据表 Region 存储在 hdfs 的 datanode 当中
管理HBase 的主要是 zookeeper 和 master。其中,HBase 要依靠 zookeeper 管理master 仅仅负责当启动 HBase 时,分配区域到指定区域服务器。当 HBase 启动以后 master 几乎不起作用,我们可以关掉 Hmaster 进程,HBase 仍然可以运行
regionServer 即是一台 HBase 服务器,一个进程。我们配置的伪分布式 HBase 就只有一台 regionServer,而真正的分布式集群每一台部署了 HBase 的节点都是一个 regionServer
我们使用的 HBase 是一个列式数据库,一张表可以达到十亿行,这就需要将表拆分成多个部分储备起来。即是分别存入 region 中,由 regionserver 管理。
RegionServer 主要管理两种文件,一是 Hlog(预写日志 write-ahead log WAL)二是 region(实际的数据文件)。当用户向 HBase 请求写入 put 数据时,首先就要写入 Hlog 实现的预写日志当中,当 HBase 服务器崩溃时,hlog 可以回滚还没有持久化的数据,以便后续从 WAL 中恢复数据。 当数据写入预写日志后,数据便会存放当 region 中的 memStore 中,同时还会检查 memStore 是否写满,如果写满,就会被请求刷写到磁盘中
当我们用客户端与 HBase 交互时,一般要经历如下步骤:
联系zookeeper,找出 meta 表(元数据表)所在 rs(regionserver) /hbase/meta-region-server;
定位 row key, 找到对应 region server;
缓存信息在本地(再次查找数据时,不需要经过 1,2 步骤);
联系 RegionServer;
HRegionServer 负责 open HRegion 对象,为每个列族创建 Store 对象,Store 包含多个 StoreFile 实例,他们是对 HFile 的轻量级封装。每个 Store 还对应了一个 MemStore,用于内存存储数据。

热点问题
原因
在设计 rowkey 时,有大量连续编号的 rowkey。这就会导致大量 rowkey 相近的记录集中在个别 region 里,也就是集中在一台或几台 regionServer 当中。当 client 检索记录时,对个别 region 访问过多,这时此 region 所在的主机过载,就导致热点问题的出现。
影响
对于 HBase 来说,一旦存在热点问题,便会导致 HBase 的读写大量集中与一台或少数的几台 regionServer 当中。这时,会造成 HBase 集群的负载不均衡,大幅度降低 HBase 的响应速度

rowkey 设计原则和方法
rowkey 设计首先应当遵循三大原则。
rowkey 长度原则
rowkey 是一个二进制码流,可以为任意字符串,最大长度为 64kb,实际应用中一般为10-100bytes,它以 byte[] 形式保存,一般设定成定长
一般越短越好,不要超过 16 个字节,注意原因如下:
1、目前操作系统都是 64 位系统,内存 8 字节对齐,控制在 16 字节,8 字节的整数倍利用了操作系统的最佳特性
2、HBase 将部分数据加载到内存当中,如果 rowkey 过长,内存的有效利用率就会下降
rowkey 散列原则
如果 rowkey 按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey 的高位字节采用散列字段处理,由程序随即生成低位放时间字段,这样将提高数据均衡分布,各个 regionServer 负载均衡的几率
如果不进行散列处理,首字段直接使用时间信息,所有该时段的数据都将集中到一个 regionServer 当中,这样当检索数据时,负载会集中到个别 regionServer 上,造成热点问题,会降低查询效率。
rowkey 唯一原则
必须在设计上保证其唯一性rowkey 是按照字典顺序排序存储的,因此,设计 rowkey 的时候,要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到一块。但是这里的量不能太大,如果太大需要拆分到多个节点上去。
所以良好的 rowkey 设计,应当遵循三大原则,并且能让数据分散,从而避免热点问题本节介绍几种常用的 rowkey 设计方法,以供同学们学习

不过都是大数据岗位面试中常见问题,希望同学们认真研读。

加盐
这里所说的加盐并非密码学中的加盐,而是在== rowkey 的前面分配随机数==,当给 rowkey 随机前缀后,它就能分布到不同的 region 中,这里的前缀应该和你想要数据分散的不同的 region 的数量有关。
为了让同学们更好的理解加盐(salting)这个 rowkey 设计方法。我们以电信公司为例。
当我们去电信公司打印电话详单也就是通话记录
。对于通话记录来说,每个人每月可能都有很多通话记录,而使用电信的用户也是亿计。这种信息,我们就能存入 HBase 当中。
对于通话记录,我们有什么信息需要保存呢?首先,肯定应该有主叫被叫,然后有主叫被叫之间的通话时长,以及通话时间。除此之外,还应该有主叫的位置信息,和被叫的位置信息
由此,我们的通话记录表需要记录的信息就出来了:主叫、被叫、时长、时间、主叫位置、被叫位置
我们该如何来设计一张 HBase 表呢?
首先,HBase 表是依靠 rowkey 来定位的,我们应该将尽可能多的将查询的信息编入 rowkey 当中。HBase 的元数据表 mate 表就给我们了一个很好的示例。它包括了namespace,表名,startKey,时间戳,计算出来的码(用于分散数据)。
所以,当我们设计通话记录的 rowkey 时,需要将能唯一确定该条记录的数据编入 rowkey 当中。即是需要将主叫、被叫、时间编入。
如下所示:
17765657979 18688887777 201806121502 #主叫,被叫,时间
但是我们能否将我们设计的 rowkey 真正应用呢?
当然是可以的,但是热点问题便会随之而来。
例如你的电话是以 177 开头,电信的 HBase 集群有 500 台,你的数据就只可能被存入一台或者两台机器的 region 当中,当你需要打印自己的通话记录时,就只有一台机器为你服务。而若是你的数据均匀分散到 500 机器中,就是整个集群为你服务。两者之间效率速度差了不止一个数量级。
因为我们设定整个 HBase 集群有 500 台,所以我们随机在 0-499 之间中随机数字,添加到 rowkey 首部
如下所示:
12 17765657979 18688887777 201806121502 #随机数,主叫,被叫,时间
在插入数据时,判断首部随机数字,选择对应的 region 存入,由于 rowkey 首部数字随机,所以数据也将随机分布到不同的 regionServer 中。这样就能很好的避免热点问题了
预分区
通常HBase 会自动处理 region 拆分,当 region 的大小到达一定阈值后,region 将被拆分成两个,之后在两个 region 都能继续增长数据
然而在这个过程当中,会出现两个问题:
第一点,就是我们所说的热点问题,数据会继续往一个 region 中写,出现写热点问题;
第二点,则是 拆分合并风暴,当用户的region 大小以恒定的速度增长,region 的拆分会在同一时间发生,因为同时需要压缩region 中的存储文件这个过程会重写拆分后的 region,这将会引起磁盘 I/O 上升
压缩:HBase 支持大量的压缩算法,而且通常开启压缩,因为 cpu 压缩和解压的时间比从磁盘读写数据的时间消耗的更短,所以压缩会带来性能的提升
对于拆分合并风暴,通常我们需要关闭 HBase 的自动管理拆分。然后手动调用 HBase 的 split(拆分)和 major_compact(压缩,对其进行时间控制,来分散 I/O 负载。但是其中的 split 操作同样是高 I/O 的操作
为了解决这些问题,预分区就是一种很好的方法,通常它和加盐结合起来使用
所谓预分区,就是预先创建 HBase 表分区。这需要我们明确 rowkey 的取值范围和构成逻辑
比如前面我们所列举的电信电话详单表。通过加盐我们得到的 rowkey 构成是:随机数+主叫+被叫+时间,如果我们现在并没有 500 台机器,只有 10 台,但是按照我们的计划,未来将扩展到 500 台的规模。所以我们仍然设计 0 到 499 的随机数,但是将以主叫 177 开头的通话记录分配到十个 region 当中,所以我们将随机数均分成十个区域,范围如下:
-50,50-100,100-150,150-200,200-250,250-300,300-350,350-400,400-450,450-
然后我们将我们的预分区存入数组当中,当插入数据时,先根据插入数据的首部随机数,判断分区位置,再进行插入数据。同样,这样也能使得各台节点负载均衡。
哈希
细心的同学可能会发现,在我们刚刚提出的加盐 与 预分区 rowkey 设计方法中,并没有完整运用到 rowkey 设计的散列原则
更一步思考下,我们会发现如果只运用 加盐 与 预分区 rowkey 设计方法,数据会真正无序
随即分布在 HBase 集群当中,这并没有让我们利用到 HBase 根据字典顺序排序的这一特点。
由此,哈希这一设计理念便顺理成章的出现在我们眼前。
同样以电信通话记录为例,我们想将某一天的通话记录存入同一 region 当中,所以我们利用哈希函数算出哈希值,再模以我们需要存入 region 数量,我们就能将相同输入的数据,存入同一 region 当中
在 主叫,被叫,时间 rowkey 当中,我们将 callerID(主叫)与 20180612(某一天的时间)作为参数,传入哈希函数当中,将得到的哈希值模以 500,余数添加到 rowkey 首部中,再结合 预分区 设计方法,就能将数据均匀分布到 regionServer 当中
同时,我们还能将相同 rowkey 的数据收集到一台节点上,在避免热点问题的情况下,充分利用 HBase 字典排序的优点
反转
对于以手机号码这样比较固定开头的 rowkey(例如开头 177,159,138),但是它的后几位都是随机的,没有规律的。我们可以将手机号反转之后作为 rowkey,这样就避免了热点问题
这就是 rowkey 设计的另一种方法 反转,通过反转固定长度或者数字格式的 rowkey。这样可以使得 rowkey 中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机 rowkey,但是牺牲了 rowkey 的有序性

哈希函数
哈希函数的性质
哈希函数又名散列函数,对于经典哈希函数来说,它具有以下 5 点性质:
输入域无穷大
输出域有穷尽
输入一样输出肯定一样
当输入不一样输出也可能一样(哈希碰撞)
不同输入会均匀分布在输出域上(哈希函数的散列性)
这里对第 5 点做一些解释,例如输入域是 0-99 这一百个数字,而我们使用的哈希函数的输出域为 0,1,2,当我们将 0-99 这一百个数字通过该哈希函数,得到的返回值,0,1,2 数量都会接近 33 个,不会出现某个返回值数量特别多,而某个返回值特别少
这里需要注意的是,对于哈希函数来说,有规律的输入并不能得到有规律的输出,例如十个 1Mb 的字符串,只有最后 1bytes 的内容不一样,在经过哈希函数后得到返回值千差万别,而不会有规律,所以它可以来打乱输入规律
通常哈希函数的输出域都很大(相对于输入域很小),例如常见的 MD5(Message Digest Algorithm (中文名为消息摘要算法第五版))算法,它的输出域是 0 到 2^64-1,但是往往我们都会将哈希函数的返回值模上一个较小的数 m,让哈希函数的输出域缩减为0 到 m-1.
如何生成多个哈希函数
这里我们介绍一种快速生成多个哈希函数的方法。
假如你急需要 1000 个哈希函数,并且这 1000 个哈希函数都要求相互独立,不能有相关性。这时,错误的方法是去在网上寻找 1000 个哈希函数。我们可以通过一个哈希函数来生成这样的 1000 个独立的哈希函数。
假如,你有一个哈希函数 f,它的输出域是 2^64,也就是 16 字节的字符串,每个位置上是 16 进制的数字 0-9,a-f。
我们将这 16 字节的输出域分为两半,高八位,和低八位是相互独立的(这 16 位都相互独立)。这样,我们将高八位作为新的哈希函数 f1 的输出域,低八位作为新的哈希函数 f2 的输出域,得到两个新的哈希函数,它们之间相互独立。
故此可以通过以下算式得到 1000 个哈希函数:
f1+2f2=f3
f1+3
f2=f4
f1+3*f2=f5
这里可以通过数学证明 f3 与 f4 及以后的哈希函数不相关,数学基础较好的同学可以查询相关资料,尝试证明,这里就不给出具体的证明了。
布隆过滤器
布隆过滤器(Bloom Filter)是 1970 由布隆提出的。通过一个很长的二进制向量于一系列随即哈希函数生成。

哈希表的经典结构
在数据结构中,哈希表最开始被描述成一个指针数组,数组中存入的每个元素是指向一个链表头部的指针。
我们知道,哈希表中存入的数据是 key,value 类型的,哈希表能够 put(key,value),同样也能 get(key,value) 或者 remove(key,value)。当我们需要向哈希表中 put(插入记录)时,我们将 key 拿出,通过哈希函数计算 hashcode。假设我们预先留下的空间大小为 16,我们就需要将通过 key 计算出的 hashcode 模以 16,得到 0-15 之间的任意整数,然后我们将记录挂在相应位置的下面(包括 key,value)。
注意:位于哪个位置下只与 key 有关,与 value 无关。
例如我们要将下面这样一条记录插入哈希表中:
“dds”,666 # key是dds,value是666
copy
首先我们通过哈希函数,计算 dds的 hashcode,然后模以 16。假如我们得到的值是 6,哈希表会先去检查 6 位置下是否存在数据。如果有,检查该节点中的 key 是否等于 dds,如果等于,则将该节点中的 value 替换为 666;如果不等于,则在链表的最后新添加一个节点,保存我们的记录。
由于哈希函数的性质,得到的 hashcode 会均匀分布在输出域上,所以模以 16,得到的 0-15 之间的数目也相近。这就意味着我们哈希表每个位置下面的链表长度相近。
对于常见的几种数据结构来说,数组的特点是:容易寻址,但是插入和删除困难。而链表的特点是:寻址困难,但是插入和删除容易。而对于哈希表来说,它既容易寻址,同样插入和删除容易,这一点我们从它的数据结构中是显而易见的
在实际哈希表应用中,它的查询速度近乎 O(1),这是因为通过 key 计算 hashcode 的时间是常数项时间,而数组寻址的时间也是常数时间。在实际应用中,每个位置的链表长度不会太长,当到达一定长度后,哈希表会经历一次扩容,这就意味着遍历链表的时间也是常数时间。
所以,我们增删改查哈希表中的一条记录的时间可以默认为 O(1)。

哈希函数在大数据中的应用
例如,我们有一个 10TB 的大文件存在分布式文件系统上,存的是 100 亿行字符串,并且字符串无序排列,现在我们要统计该文件重复的字符串
假设,我们可以调用 100 台机器来计算该文件
那么,现在我们需要怎样通过哈希函数来统计重复字符串呢
首先,我们需要将这一百台机器分别从0-99 标好号,然后我们在分布式文件系统中一行行读取文件(多台机器并行读取,通过哈希函数计算 hashcode,将计算出的 hashcode 模以 100,根据模出来的值,将该行存入对应的机器中
根据哈希函数的性质,我们很容易看出,相同的字符串会存入相同的机器中
然后我们就能并行 100 台机器,各自分别计算相应的数据,加快统计的速度
注意:这 10TB 文件并不是均分成 100GB,分给 100 台机器,而是这 10TB 文件中不同字符串的种类,均分到 100 台机器中。如果还嫌单个机器处理的数据过大,可以按照同样的方法,在一台机器中并行多个进程,处理数据。

布隆过滤器
布隆过滤器(Bloom Filter)是 1970 由布隆提出的。通过一个很长的二进制向量于一系列随即哈希函数生成

原因与结构解析
首先,我们应当知道,hash 是内存中使用的经典数据结构
当我们需要判读一个元素是否在一个集合当中时,我们可以用哈希表来判断。在集合较小的情况下,hash 是可行而且高效的。
然而数据量以 PT 计的大数据场景中,很多时候,hash 便力有未逮。这是因为在海量数据下hash 要占据巨额内存空间,这远远超过我们能够提供的内存大小。
例如在黑名单过滤当中,我们有100 亿的网站黑名单 url 需要过滤,假设一个 url 是 64bytes。如果我们用 hash 表来做,那么我们至少需要 6400 亿字节即 640G 的内存空间(实际所需空间还远大于此),空间消耗巨大,必须要多个服务器来同时分摊内存
然而我们是否能用更加精简的结构来做这件事呢?布隆过滤器就是这样一个高度节省空间的结构,并且其时间也远超一般算法,但是布隆过滤器存在一定的失误率,例如在网页 URL 黑名单过滤中,布隆过滤器绝不会将黑名单中网页查错,但是有可能将正常的网页 URL 判定为黑名单当中的,它的失误可以说是宁可错杀,不可放过。不过布隆过滤器的失误率可以调节,下面我们会详细介绍。
布隆过滤器实际就是一种集合。假设我们有一个数组,它的长度为 L,从 0-(L-1)位置上,存储的不是一个字符串或者整数,而是一个 bit,这意味它的取值只能为 0 或 1.
例如我们实现如下的一个数组:
int[] array = new int[1000];
该数组中有 1000 个 int 类型的元素,而每一个int 由有 4 个 byte 组成,一个 byte 又由 8 个 bit 组成,所以一个 int 就由 32 个 bit 所组成。
所以我们申请含 1000 整数类型的数组,它就包含 32000 个 bit 位。
但是我们如果想将第20001 个 bit 位描黑,将其改为 1,我们需要怎样做呢
首先我们的需要定位,这第 20001 个 bit 位于哪个整数,接着我们需要定位该 bit 位于该整数的第几个 bit 位=。
int index = 20001;
int intIndex = index/32;
int bitIndex = index%32;
然后我们就将其描黑:
array[intIndex] =(array[intIndex] | (1 << bitIndex));
这段代码可能有些同学不能理解,下面我们详细解释一下。
我们的数组是 0-999 的整型数组,但是每个位置上实际保存的是 32 个 bit,我们的 intIndex 就是代表第几个整数,定位到 625,而 bitIndex 定位到第 625 个整数中的第几个 bit,为 1。
所以,我们需要描黑的位置是第 625 号元素的第 1 个 bit
而 (1 << bitIndex) 代表将 1 左移到 1 位置,即是代表只有 1 位置为 1,而其他位置为零。就相当于获得了这样一串二进制数:
00000000 00000000 00000000 00000001
然后我们将这样的二进制数与 array[indIndex] 进行逻辑或运算,就能将第 625 号元素的第一个 bit 描黑变 1,最后我们将描黑后的元素赋值给 array[indIndex],完成运算。
这里我们采用的是 int 类型的数组,如果我们想更加节省空间,我们就能创建long类型的数组,这样申请 1000 个数组空间,我们就能得到 64000 个 bit。
然后我们还能进一步扩展,将数组做成矩阵:
long[][] map = new long[1000][1000];
有了这些基础以后,我们如何设计黑名单问题呢?
假设我们已经有了一个拥有 m 个 bit 的数组,然后我们将一个 URL 通过哈希函数计算出 hashcode,然后 hashcode%m 将对应位置描黑。
然而这还不是真正的布隆过滤器,真正的布隆过滤器是通过多个哈希函数对一个 URL 进行计算,得到 hashcode,然后在对不同位置的 bit 进行描黑
注意:布隆过滤器采用的多个哈希函数必须是相互独立的,前面我们已经介绍了如何通过一个哈希函数构造多个独立哈希函数的方法
当我们将一个 URL 通过 n 个哈希函数,得到 hashcode,模以 m,再将对应位置描黑之后。我们可以说该 URL 已经进入我们的布隆过滤器当中了
接下来,我们将黑名单中所有 URL 都通过哈希函数,进入布隆过滤器中(布隆过滤器并不真正存储实际的 URL)。
对于一个新的 URL,我们要查询其是否在黑名单中,我们便通过同样的 n 个哈希函数,计算出 n 个位置,然后我们查询这 n 个位置是否都被描黑,如果都被描黑,我们就说该 URL 在黑名单当中。如果 n 个位置但凡有一个不为黑,我们就说该 URL 不在该黑名单中
注意:我们的数组不应该过小,不然很可能数组中的大多数位置都被描黑,这很容易将正常的 URL 判定为不合法的,这也是布隆过滤器的失误率来源

HBase 中的布隆过滤器
布隆过滤器是 HBase 中的高级功能,它能够减少特定访问模式(get/scan)下的查询时间。不过由于这种模式增加了内存和存储的负担,所以被默认为关闭状态
HBase 支持如下类型的布隆过滤器:
1、NONE 不使用布隆过滤器
2、ROW 行键使用布隆过滤器
3、ROWCOL 列键使用布隆过滤器
其中 ROWCOL 是粒度更细的模式
在介绍为什么 HBase 要引入布隆过滤器之前,我们先来了解一下 HBase 存储文件 HFile 的块索引机制
我们知道 HBase 的实际存储结构是 HFile,它是位于 hdfs 系统中的,也就是在磁盘中。而加载到内存中的数据存储在 MemStore 中,当 MemStore 中的数据达到一定数量时,它会将数据存入 HFile 中。
HFIle 是由一个个数据块与索引块组成,他们通常默认为 64KB。HBase 是通过块索引来访问这些数据块的。而索引是由每个数据块的第一行数据的 rowkey 组成的。当 HBase 打开一个 HFile 时,块索引信息会优先加载到内存当中。
然后 HBase 会通过这些块索引来查询数据
但是块索引是相当粗粒度的,我们可以简单计算一下。假设一个行占 100bytes 的空间,所以一个数据块 64KB,所包含的行大概有:(64 * 1024)/100 = 655.53 ~= 700 行。而我们只能从索引给出的一个数据块的起始行开始查询。
如果用户随机查找一个行键,则这个行键很可能位于两个开始键(即索引)之间的位置。对于 HBase 来说,它判断这个行键是否真实存在的唯一方法就是加载这个数据块,并且扫描它是否包含这个键。
同时,还存在很多情况使得这种情况更加复杂。
对于一个应用来说,用户通常会以一定的速率进行更新数据,这就将导致内存中的数据被刷写到磁盘中,并且之后系统会将他们合并成更大的存储文件。在 HBase 的合并存储文件的时候,它仅仅会合并最近几个存储文件,直至合并的存储文件到达配置的最大大小。最终系统中会有很多的存储文件,所有的存储文件都是候选文件,其可能包含用户请求行键的单元格
我们可以看到,这些不同的文件都来着同一个列族,所以他们的行键分布类似。所以,虽然我们要查询更新的特定行只在某个或者某几个文件中,但是采用块索引方式,还是会覆盖整个行键范围。当块索引确定那些块可能含有某个行键后,regionServer 需要加载每一个块来检查该块中是否真的包含该行的单元格。
从前一小节当中我们可以知道,当我们随机读 get 数据时,如果采用 HBase 的块索引机制,HBase 会加载很多块文件。如果采用布隆过滤器后,它能够准确判断该 HFile 的所有数据块中,是否含有我们查询的数据,从而大大减少不必要的块加载,从而增加 HBase 集群的吞吐率
这里有几点细节:
布隆过滤器的存储在哪?
对于 HBase 而言,当我们选择采用布隆过滤器之后,HBase 会在生成 StoreFile(HFile)时包含一份布隆过滤器结构的数据,称其为 MetaBlock;MetaBlock 与 DataBlock(真实的 KeyValue 数据)一起由 LRUBlockCache 维护。所以,开启 bloomfilter 会有一定的存储及内存 cache 开销。但是在大多数情况下,这些负担相对于布隆过滤器带来的好处是可以接受的。
采用布隆过滤器后,HBase 如何 get 数据
在读取数据时,HBase 会首先在布隆过滤器中查询,根据布隆过滤器的结果,再在 MemStore 中查询,最后再在对应的 HFile 中查询
采用 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。
注意:ROW 和 ROWCOL 只是名字上有联系,但是 ROWCOL 并不是 ROW 的扩展,也不能取代 ROW