原文地址

简介

HBase ——Hadoop Database,是一个高可靠、高性能、面向列、可伸缩的分布式存储系统,利用HBase技术可在廉价PC Server上搭建起大规模结构化存储集群。

HBase是Google Bigtable的开源实现:

  • 类似Google Bigtable利用GFS作为其文件存储系统,HBase 利用Hadoop HDFS作为其文件存储系统,HDFS为HBase提供了高可靠性的底层存储支持。
  • Google运行MapReduce来处理Bigtable中的海量数据,HBase同样利用Hadoop MapReduce来处理HBase中的海量数据,MapReduce为HBase提供了高性能的计算能力。
  • Google BigTable利用Chubby作为协同服务,HBase使用的是Zookeeper作为对应,Zookeeper为HBase提供了稳定服务和failover机制。
  • 此外,Pig和Hive为Hbase提供了高层语言支持,使得在HBase上进行数据统计处理变得简单。
  • Sqoop则为HBase提供了方便的RDBMS数据导入功能。

在这里插入图片描述

特点

  • 强一致性读写:HBase 不是“eventually consistent(最终一致性)”数据存储,这让它很适合高速计数聚合类任务;
  • 自动分片(Automatic sharding): HBase 表通过 region 分布在集群中,数据增长时,region 会自动分割并重新分布;
  • RegionServer 自动故障转移;
  • Hadoop/HDFS 集成:HBase 支持开箱即用地支持 HDFS 作为它的分布式文件系统;
  • MapReduce: HBase 通过 MapReduce 支持大并发处理;
  • Java 客户端 API:HBase 支持易于使用的 Java API 进行编程访问;
  • Thrift/REST API:HBase 也支持 Thrift 和 REST 作为非 Java 前端的访问;
  • Block Cache 和 Bloom Filter:对于大容量查询优化, HBase 支持 Block Cache 和 Bloom Filter;
  • 运维管理:HBase 支持 JMX 提供内置网页用于运维。

优缺点

优点

  • 列的可以动态增加,并且列为空就不存储数据,节省存储空间
  • Hbase 自动切分数据,使得数据存储自动水平扩展
  • Hbase 可以提供高并发读写操作的支持
  • 与 Hadoop MapReduce 相结合有利于数据分析
  • 容错性
  • 版权免费
  • 非常灵活的模式设计(或者说没有固定模式的限制)
  • 可以跟 Hive 集成,使用类 SQL 查询
  • 自动故障转移
  • 客户端接口易于使用
  • 行级别原子性,即:PUT 操作一定是完全成功或者完全失败
  • 多语言支持:
    • Native Java API
    • HBase Shell
    • Thrift Gateway
    • REST Gateway:支持REST网格的HTTP API访问HBase
    • Pig:Pig Latin流式编程语言来操作HBase中的数据。
    • Hive:0.7.0版本的Hive加入HBase

缺点

  • 不能支持条件查询,只支持按照 row key 来查询
  • 容易产生单点故障(在只使用一个 HMaster 的时候)
  • 不支持事务
  • JOIN 不是数据库层支持的,而需要用 MapReduce
  • 只能在主键上索引和排序
  • 没有内置的身份和权限认证

数据架构

从一个示例说起。

传统的关系型数据库想必大家都不陌生,我们将以一个简单的例子来说明使用RDBMS和HBase各自的解决方式及优缺点。

以博文为例,RDBMS的表设计如下:

在这里插入图片描述

为了方便理解,我们以一些数据示例下:
在这里插入图片描述

上面的例子,我们用HBase可以按以下方式设计:

在这里插入图片描述

同样为了方便理解,我们以一些数据示例下,同时用红色标出了一些关键概念,后面会解释:

在这里插入图片描述

HTable一些基本概念:

Row Key:

行主键, HBase不支持条件查询和Order by等查询,读取记录只能按Row key(及其range)或全表扫描,因此Row key需要根据业务来设计以利用其存储排序特性提高性能。

Column Family(列族):

在表创建时声明,每个Column Family为一个存储单元。在上例中设计了一个HBase表blog,该表有两个列族:article和author。

Column(列):

HBase的每个列都属于一个列族,以列族名为前缀,如列article:title和article:content属于article列族,author:name和author:nickname属于author列族。

Column不用创建表时定义即可以动态新增,同一Column Family的Columns会群聚在一个存储单元上,并依Column key排序,因此设计时应将具有相同I/O特性的Column设计在一个Column Family上以提高性能。

同时这里需要注意的是:这个列是可以增加和删除的,这和我们的传统数据库很大的区别。所以它适合非结构化数据。

Timestamp

HBase通过row和column确定一份数据,这份数据的值可能有多个版本,不同版本的值按照时间倒序排序,即最新的数据排在最前面,查询时默认返回最新版本。

如上例中row key=1的author:nickname值有两个版本,分别为1317180070811对应的“一叶渡江”和1317180718830对应的“yedu”(对应到实际业务可以理解为在某时刻修改了nickname为yedu,但旧值仍然存在)。

Timestamp默认为系统当前时间(精确到毫秒),也可以在写入数据时指定该值。

Value

每个值通过4个键唯一索引,tableName+RowKey+ColumnKey+Timestamp=>value。

例如上例中{tableName=’blog’,RowKey=’1’,ColumnName=’author:nickname’,Timestamp=’ 1317180718830’}索引到的唯一值是“yedu”。

存储类型

  • TableName 是字符串
  • RowKey 和 ColumnName 是二进制值(Java 类型 byte[])
  • Timestamp 是一个 64 位整数(Java 类型 long)
  • value 是一个字节数组(Java类型 byte[])。

存储结构

可以简单的将HTable的存储结构理解为:

在这里插入图片描述

即HTable按Row key自动排序,每个Row包含任意数量个Columns,Columns之间按Column key自动排序,每个Column包含任意数量个Values。理解该存储结构将有助于查询结果的迭代。

系统架构

在这里插入图片描述

Client

HBase Client使用HBase的RPC机制与HMaster和HRegionServer进行通信,对于管理类操作,Client与HMaster进行RPC;对于数据读写类操作,Client与HRegionServer进行RPC。

Zookeeper

Zookeeper Quorum中除了存储了-ROOT-表的地址和HMaster的地址,HRegionServer也会把自己以Ephemeral方式注册到 Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的健康状态。此外,Zookeeper也避免了HMaster的 单点问题。

HRegionServer

用户I/O请求,向HDFS文件系统中读写数据,是HBase中最核心的模块。

在这里插入图片描述

HRegionServer内部管理了一系列HRegion对象,每个HRegion对应了Table中的一个 Region,HRegion中由多个HStore组成。

每个HStore对应Table中的一个Column Family存储,

每个Column Family其实就是一个集中的存储单元,因此最好将具备共同IO特性的column放在一个Column Family中,这样最高效。

HStore存储是HBase存储的核心了,其中由两部分组成,一部分是MemStore,一部分是StoreFiles。

MemStore是Sorted Memory Buffer,用户写入的数据首先会放入MemStore,当MemStore满了以后会Flush成一个StoreFile(底层实现是HFile), 当StoreFile文件数量增长到一定阈值,会触发Compact合并操作,将多个StoreFiles合并成一个StoreFile,合并过程中会进 行版本合并和数据删除,因此可以看出HBase其实只有增加数据,所有的更新和删除操作都是在后续的compact过程中进行的,这使得用户的写操作只要 进入内存中就可以立即返回,保证了HBase I/O的高性能。

当StoreFiles Compact后,会逐步形成越来越大的StoreFile,当单个StoreFile大小超过一定阈值后,会触发Split操作,同时把当前 Region Split成2个Region,父Region会下线,新Split出的2个孩子Region会被HMaster分配到相应的HRegionServer 上,使得原先1个Region的压力得以分流到2个Region上。下图描述了Compaction和Split的过程:

在这里插入图片描述

在理解了上述HStore的基本原理后,还必须了解一下HLog的功能,因为上述的HStore在系统正常工作的前提下是没有问 题的,但是在分布式系统环境中,无法避免系统出错或者宕机,

因此一旦HRegionServer意外退出,MemStore中的内存数据将会丢失,这就需 要引入HLog了。

每个HRegionServer中都有一个HLog对象,HLog是一个实现Write Ahead Log的类,在每次用户操作写入MemStore的同时,也会写一份数据到HLog文件中(HLog文件格式见后续),HLog文件定期会滚动出新的,并 删除旧的文件(已持久化到StoreFile中的数据)。

当HRegionServer意外终止后,HMaster会通过Zookeeper感知 到,HMaster首先会处理遗留的 HLog文件,将其中不同Region的Log数据进行拆分,分别放到相应region的目录下,然后再将失效的region重新分配,领取 到这些region的HRegionServer在Load Region的过程中,会发现有历史HLog需要处理,因此会Replay HLog中的数据到MemStore中,然后flush到StoreFiles,完成数据恢复。

HBase 存储格式

HBase中的所有数据文件都存储在Hadoop HDFS文件系统上,主要包括上述提出的两种文件类型:

  • HFile, HBase中KeyValue数据的存储格式,HFile是Hadoop的二进制格式文件,实际上StoreFile就是对HFile做了轻量级包装,即StoreFile底层就是HFile
  • HLog File,HBase中WAL(Write Ahead Log) 的存储格式,物理上是Hadoop的Sequence File

HFile

在这里插入图片描述

首先HFile文件是不定长的,长度固定的只有其中的两块:Trailer和FileInfo。

正如图中所示的,Trailer 中有指针指向其他数据块的起始点。

File Info中记录了文件的一些Meta信息,例如:AVG_KEY_LEN, AVG_VALUE_LEN, LAST_KEY, COMPARATOR, MAX_SEQ_ID_KEY等。

Data Index和Meta Index块记录了每个Data块和Meta块的起始点。

Data Block是HBase I/O的基本单元,为了提高效率,HRegionServer中有基于LRU的Block Cache机制。

每个Data块的大小可以在创建一个Table的时候通过参数指定,大号的Block有利于顺序Scan,小号Block利于随机查询。

每个Data块除了开头的Magic以外就是一个个KeyValue对拼接而成, Magic内容就是一些随机数字,目的是防止数据损坏。后面会详细介绍每个KeyValue对的内部构造。

HFile里面的每个KeyValue对就是一个简单的byte数组。但是这个byte数组里面包含了很多项,并且有固定的结构。我们来看看里面的具体结构:

在这里插入图片描述

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey 的长度。紧接着是RowKey,然后是固定长度的数值,表示Family的长度。然后是Family,接着是Qualifier,然后是两个固定长度的数 值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。

HLogFile

在这里插入图片描述

上图中示意了HLog文件的结构,其实HLog文件就是一个普通的Hadoop Sequence File。Sequence File 的Key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括 sequence number和timestamp,timestamp是“写入时间”,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number。

HLog Sequece File的Value是HBase的KeyValue对象,即对应HFile中的KeyValue,可参见上文描述。

入门操作

学生成绩表:

nameGradecourse:mathcourse:math
Tom18797
Jerry210087

这里grade对于表来说是一列,course对于表来说是一个列族,这个列族由两个列组成:math和art。

# create {Table Name},{Column Family},{Column Family}...
hbase> create 'scores','grade','course'
hbase> describe 'scores'

新建Tom行

# put {Table Name},{Row Key},{Column Family:Column},{Value}
hbase> put 'scores','Tome','grade:','1'
hbase> put 'scores','Tome','course:math','87'
hbase> put 'scores','Tome','course:art','97'

新建 Jerry行

# put {Table Name},{Row Key},{Column Family:Column},{Value}
hbase> put 'scores','Jerry','grade:','2'
hbase> put 'scores','Jerry','course:math','100'
hbase> put 'scores','Jerry','course:art','80'

查看表

# get {Table Name},{Row Key}
hbase> get 'scores','Tom'

查看scores表所有数据

# scan {Table Name}
hbase> scan 'scores'

适用场景

应用前提

  • 足够多数据,上亿或上千亿行数据
  • 不依赖RDBMS的特性,如列类型、第二索引、事务、高级查询等
  • 有足够的硬件,少于5节点Hadoop时,基本体现不出优势

使用场景

半结构化或非结构化数据

对于数据结构字段不够确定或杂乱无章很难按一个概念去进行抽取的数据适合用HBase。

以上面的例子为例,当业务发展需要存储author的email,phone,address信息时RDBMS需要停机维护,而HBase支持动态增加.

记录非常稀疏

RDBMS的行有多少列是固定的,为null的列浪费了存储空间。

而如上文提到的,HBase为null的Column不会被存储,这样既节省了空间又提高了读性能。

多版本数据

如上文提到的根据Row key和Column key定位到的Value可以有任意数量的版本值,因此对于需要存储变动历史记录的数据,用HBase就非常方便了。

比如上例中的author的Address是会变动的,业务上一般只需要最新的值,但有时可能需要查询到历史值。

超大数据量

当数据量越来越大,RDBMS数据库撑不住了,就出现了读写分离策略,通过一个Master专门负责写操作,多个Slave负责读操作,服务器成本倍增。

随着压力增加,Master撑不住了,这时就要分库了,把关联不大的数据分开部署,一些join查询不能用了,需要借助中间层。

随着数据量的进一步增加,一个表的记录越来越大,查询就变得很慢,于是又得搞分表,比如按ID取模分成多个表以减少单个表的记录数。

经历过这些事的人都知道过程是多么的折腾。

采用HBase就简单了,只需要加机器即可,HBase会自动水平切分扩展,跟Hadoop的无缝集成保障了其数据可靠性(HDFS)和海量数据分析的高性能(MapReduce)。

遇到的坑

坑:Rowkey设计遇到的坑

Rowkey是一段二进制码流,最大长度为64KB,内容可以由使用的用户自定义。数据加载时,一般也是根据Rowkey的二进制序由小到大进行的。

之前根据广告主id加上广告主的商品id作为rowkey,因为有的广告主sku非常多,有些广告主的sku比较少,这样导致导致数据hash不均匀,有的hbase节点上存储的数据量很大,有的数据量很少,这样的数据倾斜,导致整个hbase的集群性能下降。

注意事项

Rowkey的散列原则

必须要保证所有的rowkey那个均匀的分布在各个hbase节点上,后来我们把rowkey加上时间戳然后做了md5的加密来解决此问题。

如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。

如果没有散列字段,首字段直接是时间信息将产生所有新数据都在一个 RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。

Hbase检索的三种方式

  1. 基于单个rowkey查询,即按照具体的rowkey键值进行get操作;
  2. 通过Rowkey的range进行scan,即通过设置startRowKey和endRowKey,在这个范围内进行扫描。这样可以按指定的条件获取一批记录;
  3. 全表扫描所有数据(不建议这干,因为效率特别低).

Rowkey设计原则

Rowkey长度设计原则:Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。

因为:

  1. 数据持久化文件HFile中是按照KeyValue存储,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,近1G数据,这会极大影响HFile的存储效率;
  2. MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。
  3. 目前操作系统是都是64位系统,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性。

Rowkey的唯一原则

必须在设计上保证其唯一性。

其他注意事项

  1. 在准备使用hbase之前设计好rowkey是关键,一般rowkey上都会存一些比较关键的检索信息;
  2. 必须提前想好数据应该如何查询,以及数据具体需要如何使用,根据查询方式进行数据存储格式的设计;
  3. 列名,表名等尽量使用短名字,这样可以节省空间,提高性能;
  4. 使用预分区;
    • 默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。
    • 一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡。
  5. 创建表的时候,可以通过HColumnDescriptor.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,这样可以保证不活跃的数据或者过期的数据占用磁盘和内存空间;具体有效期设置多长时间需要根据具体的业务场景进行判断后设置。
  6. Compact & Split
    • 在HBase中,数据在更新时首先写入WAL 日志(HLog)和内存(MemStore)中,MemStore中的数据是排序的。
    • 当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。
    • 于此同时, 系统会在zookeeper中记录一个redo point,表示这个时刻之前的变更已经持久化了(minor compact)。
    • StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。
    • 当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile。
    • 当StoreFile的大小达到一定阈值后,又会对 StoreFile进行分割(split),等分为两个StoreFile。
    • 由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,通常合并过程还是比较快的。
    • 实际应用中,可以考虑必要时手动进行major compact,将同一个row key的修改进行合并形成一个大的StoreFile。同时,可以将StoreFile设置大些,减少split的发生。
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐