Co2y's Blog

hbase介绍

hbase学习总结,主要列出重点,也算是一个学习目录。还没写完,这篇会不断补充:D

背景

当数据量越来越大,RDBMS数据库撑不住了,就出现了读写分离策略,通过一个Master专门负责写操作,多个Slave负责读操作,服务器成本倍增。随着压力增加,Master撑不住了,这时就要增加缓存,当缓存不能再增加的时候就得分库了,把关联不大的数据分开部署,一些join查询不能用了,需要借助中间层。随着数据量的进一步增加,一个表的记录越来越大,查询就变得很慢,于是又得搞分表,比如按ID取模分成多个表以减少单个表的记录数。经历过这些事的人都知道过程是多么的折腾。采用HBase就简单了,只需要加机器即可,HBase会自动水平切分扩展,跟Hadoop的无缝集成保障了其数据可靠性(HDFS)和海量数据分析的高性能(MapReduce)。

HBase是一种列式存储数据库,列式的好处有两个:按列存储减少I/O,利于压缩。

组成部分

物理上

  • hdfs(NameNode+DataNode)+zookeeper(HQuorumPeer)+hbase(HMaster+HRegionServer)

Zookeeper

  1. 保证任何时候,集群中只有一个master
  2. 存贮所有Region的寻址入口。
  3. 实时监控Region Server的状态,将Region server的上线和下线信息实时通知给Master
  4. 存储Hbase的schema,包括有哪些table,每个table有哪些column family
  5. hmaster启动时候会将hbase系统表-ROOT- 加载到 zookeeper cluster,通过zookeeper cluster可以获取当前系统表.META.的存储所对应的regionserver信息。

Master

  1. 为Region server分配region
  2. 负责region server的负载均衡
  3. 发现失效的region server并重新分配其上的region
  4. GFS上的垃圾文件回收
  5. 处理schema更新请求
  6. 维护系统表-ROOT-,.META.,记录regionserver所对应region变化信息。

关于-ROOT-和.META.,他们是两张普通的表且结构一样,用来记录region的分布和region的详细信息。它的rowkey即regionname,包括tablename,startkey,timestamp,它的CF包括regioninfo:startkey/endkey/familylist,server:address。client寻址过程:先找.META.,然后从.META.中找到对应regionserver,但是.META.放在哪个regionserver上呢,于是把管理.META.和对应regionserver的表放在zookeeper上就可以了,但是这会让client自己去遍历哪个regionserver存储对应的.META.,于是有了一张-ROOT-表,-ROOT-的rowkey为.META.,tablename,startkey,timestamp,-ROOT-的路径默认为/hbase/root-region-server。另外Client端并不会每次数据操作都做这整个路由过程,很多数据都会被Cache起来。

Region Server

  1. Region server维护Master分配给它的region,处理对这些region的IO请求
  2. Region server负责切分在运行过程中变得过大的region
    任何时刻,一个region只能分配给一个region server。master记录了当前有哪些可用的region server。以及当前哪些region分配给了哪些region server,哪些region还没有分配。当存在未分配的region,并且有一个region server上有可用空间时,master就给这个region server发送一个装载请求,把region分配给这个region server。region server得到请求后,就开始对此region提供服务。

逻辑上

  • table+rowkey+columnfamily+columnqualifier+value+timestamp

多行组成一个region,对应一个regionserver,每个region对应多个store,每个store对应一个CF,每个store又由一个memstore和0至多个storefile组成,storefilehfile的形式存储在HDFS上,一个CF的所有列在每次memstore达到阈值flush时存在同一个hfile中,hfile有合并和分裂操作。

Rowkey决定Region,而ColumnFamliy决定HFile,并且由于Hbase的多版本性,不同的HFile也有不同的timestamp区间,所以在查询时,当然rowkey是必须的,决定所读取的region,和关系型数据库不同的是,查询时指定ColumnFamliy将大大提高查询效率,因为这决定了读取的HFile个数,如果可以指定timestamp,也可以进一步对HFile经行过滤,以提高查询效率。对于column qualifier,value就需要把数据读出来进行逐个过滤,相对比较低效。

Bloom Filters 简单的说, HBase文件HFile是按columnfamily组织的, 而且一共columnfamily可能需要存储成很多个HFile, 如果我要取某row的这个columnfamily的数据, 怎么办, 简单的办法, 就是把所有HFile都打开查一遍, 这个效率就很低。 BloomFilter, 可以用于对每个HFile建一个rowkey的filter, 在打开HFile前我们可以先filter一下, 该文件是否可能包含该row。 这样大大减少打开文件的个数, 效率会大大的提高。文中还提到一种row+column Bloom filter, 这种只有在row的columnfamily很大, 经常要多个HFile来存储一个row的columnfamily数据时才有用。

DDI设计技巧

关于DDI定义见上篇

其它一些设计技巧如下

  • 高表优于宽表

    • 应该尽量将需要查询的维度或信息存储在行键中,另外hbase是按行划分,当行太大时超过一个hfile,那么就很难在合适的位置划分,这种行为会导致compaction,而这样做是要把row读内存的。
  • 部分键扫描

    • 对于key1_key2_key3,可以对key1到key1+1来扫描全部key1开头的rowkey
  • 分页

    • 在起始键的位置处打开一个扫描器

    • 跳过offset数目的行

    • 读取limit数目的行

  • 时间序列

    • 热点问题:只有一个region频繁的写、分裂,解决方法通常有:字段交换、随机化(md5,salt)((((((。))))))这里的一个结论是顺序键顺序读的性能好但是写的性能差,随机键则相反。

API操作

除了常见的getputscan,操作之外下面介绍几个高级特性

filter

filter可以对行键、列名或列值进行过滤,filter是在服务器端进行的,可以减少网络开销。但值得说明的是filter效率往往并不高

counter

将列当做计数器

coprocessor

这是hbase的一个特性,目的是把部分计算放到数据存放端来做,减少服务器通过网络返回到客户端的数据量。也就是将数据的处理流程直接放到服务器端执行,然后仅返回一个小的处理结果集。类似于一个小型的mapreduce框架,该框架将工作分发到整个集群。

协处理器允许用户在region服务器上运行自己的代码,更准确地说是允许用户执行region级的操作。它有两个东西observer和endpoint,observer类似于RDBMS中的trigger。最常见的是使用钩子关联行修改操作来维护一个辅助索引。endpoint类似于存储过程。它们的计算都是在服务器端进行,可以通过zookeeper进行通信,客户端可以通过RPC的形式获取返回结果(利用protobuf)。

HTablePool

这是很好的一个东西,避免了多次创建HTable实例带来的开销

存储

B+ tree

Oracle的普通索引就是采用B+树的方式,与B树不同的是,每个叶子节点都有前指针和后指针,这是为了做范围查询时,叶子节点间可以直接跳转,从而避免再去回溯至枝和跟节点。

B+树最大的性能问题是会产生大量的随机IO,随着新数据的插入,叶子节点会慢慢分裂,逻辑上连续的叶子节点在物理上往往不连续,甚至分离的很远,但做范围查询时,会产生大量读随机IO。再因为延迟主要来源于磁盘寻道时间,所以效率底下。

LSM tree

LSM树和B+树相比,它牺牲了部分读性能,用来大幅提高写性能。
它的原理是把一颗大树拆分成N棵小树, 它首先写入到内存中(内存没有寻道速度的问题,随机写的性能得到大幅提升),在内存中构建一颗有序小树,随着小树越来越大,内存的小树会flush到磁盘上。当读时,由于不知道数据在哪棵小树上,因此必须遍历所有的小树,但在每颗小树内部数据是有序的。

  • WAL(Write Ahead Log)就是因为数据是先写到内存中,如果断电,内存中的数据会丢失,因此为了保护内存中的数据,需要在磁盘上先记录logfile,当内存中的数据flush到磁盘上时,就可以抛弃相应的Logfile。

  • LSM树就是一堆小树,在内存中的小树即memstore,每次flush,内存中的memstore变成磁盘上一个新的storefile。

  • 随着小树越来越多,读的性能会越来越差,因此需要在适当的时候,对磁盘中的小树进行merge,多棵小树变成一颗大树。

bloom filter

使用bloom filter的主要原因是默认索引只存储了文件包含块的开始键,Hbase利用bloomfilter来提高随机读(get)的性能 ,bloom filter是一个列族级的属性,如果在表中设置了bloomfilter,那么hbase会在生成storefile时包含一份bloomfilter结构的数据,称其为MetaBlock与DataBlock(真实的KeyValue数据)一起由LRUBlockCache维护。所以开启bloomfilter会有一定的存储及内存cache开销。它原理包括两步:映射+判断

映射就是m位的数组,且这m个数组的每一位都是零。k个相互独立的哈希函数,将其中每个元素进行k次哈希,他们分别将这个元素映射到m位的数组中,而其映射的位置就置为1,如果有重复的元素映射到这个数组的同一个元素,那么这个元素只会记录一次1,后续的映射将不会改变的这个元素的值。判断就是把元素做K次hash如果都为1就在集合中。显然判断属于的情况下可能会发生错误。

sequence file

我们知道HBase通常是构建在HDFS之上的,而HDFS的block很大,64/128M,存在大量小于64M的文件时,会形成多个block,即每个block中存储的实际文件内容很少,大量I/O,而HBase的一个块很小,因此就产生了许多小文件,通过将大量小文件输入到一个sequence file中减少了namenode中的元数据量 提高检索和查询效率。降低文件I/O和传输时间

索引

看了华为的索引设计,大概意思是在Client Ext中设定索引细节,在Balancer中收集信息,在Coprocessor中管理二级索引数据。

性能优化

垃圾回收

HBase默认是依赖于JRE进行垃圾回收,但是JRE不能很好地处理region服务器,是因为当region服务器处理写入量过大的负载时会迫使内存分配策略无法安全地只依赖JRE对程序行为的各种假设。因此要修改默认值,默认值普遍偏小。
数据会根据自身在内存中停留的时间被保存在java堆中分代结构的不同位置。被快速插入且被刷写到磁盘的数据被分配到新生代。停留时间过长就提升为老生代。新生代占用的空间在128M到512M之间,而老生代几乎占用了所有可以占用的堆空间,通常是几G。

memstore分配缓冲区

生存期长的keyvalue实例一旦刷写到磁盘就会在老生代的堆上产生孔洞。申请新空间时,由于碎片过多导致没有足够大的连续空间分配。本地memstore分配缓冲区只允许从堆中分配相同大小的对象,一旦这些对象分配并最终被回收,它们将在堆中留下固定大小的孔洞,可以重新使用。

压缩

CPU压缩和解压通常比从磁盘中读取和写入更多数据要快。

手动split和合并region

默认情况下region达到阈值就会自动拆分,但是假如region的增长速率相同,那么它们会在同一时间进行拆分,带来集中I/O消耗上升。因此可以开启手动拆分。手动拆分也可以缓解之前提到的region热点问题。

有时候合并也很有用,当删除大量数据时,通过合并region可以减少region数目,减轻regionserver负担。

负载均衡

内置的均衡器5分钟运行一次,这个也可以手动修改。

集群监控

JMX和Ganglia,关于测试可以用YCSB框架。YCSB可以测量不同的nosql数据库,非常简单易用。