

HBase 是一款面向列存储,用于存储处理海量数据的 NoSQL 数据库。它的理论原型是 Google 的 BigTable 论文。你可以认为 HBase 是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统。
HBase 的存储是基于HDFS的,HDFS 有着高容错性的特点,被设计用来部署在低廉的硬件上,基于 Hadoop 意味着 HBase 与生俱来的超强的扩展性和吞吐量。
HBase 采用的时key/value的存储方式,这意味着,即使随着数据量的增大,也几乎不会导致查询性能的下降。HBase 又是一个面向列存储的数据库,当表的字段很多时,可以把其中几个字段独立出来放在一部分机器上,而另外几个字段放到另一部分机器上,充分分散了负载的压力。如此复杂的存储结构和分布式的存储方式,带来的代价就是即便是存储很少的数据,也不会很快。
HBase 并不是足够快,只是数据量很大的时候慢的不明显。HBase主要用在以下两种情况:
我们知道 Mysql 是一个关系型数据库,学数据库的时第一个接触的就是 MySQL 了。但是 MySQL 的性能瓶颈是很大的,一般单个table行数不宜超过500万行,大小不宜超过2G。
我们以互联网公司最核心用户表为例,当数据量达到千万甚至亿级别时候,尽管你可以通过各种优化来提速查询,但是对单条数据的检索耗时还是会超出你的预期!看下这个User表:

假如查询 id=1 这条数据对应的用户name,系统会给我们返回aa。但由于MySQL是以行为位单位存储的,当查 name 时却需要查询一整行的数据,连 age 和 email 也会被查出来!如果列非常多,那么查询效率可想而知了。
我们称列过多的表为宽表,优化方法一般就是对列进行竖直拆分:

此时查找 name 时只需要查找 user_basic 表,没有多余的字段,查询效率就会很快。如果一张表的行过多,会影响查询效率,我们将这样的表称之为高表,可以采用水平拆表的方式提高效率:

这种水平拆分应用比较多的 场景就是日志表,日志信息每天产生很多,可以按月/按日进行水平拆分,这样就实现了高表变矮。
上述的拆分方式貌似可以解决宽表跟高表问题,但是如果有一天公司业务变更,比如原来没有微信,现在需加入用户的微信字段。这时候需要改变表的结构信息,该怎么办?最简单的想法是多加一列,像这样:

但是你要知道不是所有用户都要微信号的,微信号这一列是设置默认值还是采取其他的做法就得权衡一下了。如果需扩展很多列出来,但不是所有的用户都有这些属性,那么拓展起来就更加复杂了。这时可以用下JSON格式的字符串,将若干可选择填写信息汇总,而且属性字段可以动态拓展,于是有了下边做法:

至此你可能认为这样存储数据它不挺好的嘛,用 HBase 出来干嘛?Mysql 有个致命缺点,就是当数据达到一定的阈值,无论怎么优化,它都无法达到高性能的发挥。而大数据领域的数据,动辄 PB 级数据量,这种存储应用明显是不能很好的满足需求的!并且针对上边的问题,HBase 都有很好的解决方案~~。
接着上边说到的几个问题:高表、宽表、数据列动态扩展,把提到的几个解决办法:水平切分、垂直切分、列扩展方法 杂糅在一起。
有张表,你怕它又宽又高跟动态扩展列,那么在设计之初,就把这个表给拆开,为了列的动态拓展,直接存储JSON格式:

这样就解决了宽表跟列扩展问题,高表怎么办呢?一个表按行切分成partition,各存一部分行:

解决了高表、宽表、动态扩展列 的问题后你会发现数据量大了速度不够快咋办?用缓存呗,查询出的数据放缓存中,下次直接从缓存拿数据。插入数据怎么办呢?也可以这样理解,我把要插入的数据放进缓存中,再也不用管了,直接由数据库从缓存拿数据插入到数据库。此时程序不需要等待数据插入成功,提高了并行工作的效率。
你用缓存的考虑服务器宕机后缓存中数据没来得及插入到数据库中造成丢数据咋办?参考 Redis 的持久化策略,可以插入数据这个操作添加一个操作日志,用于持久化插入操作,宕机重启后从日志恢复。这样设计架构就变成了这个样子:

这就是 HBase 实现的大致思路。接下来正式进入 HBase 设计解析。
Hbase 官网:http://hbase.apache.org
HBase适合存储 PB 级别的海量数据,能在几十到百毫秒内返回数据。
HBase是根据
列族来存储数据的。列族下面可以有非常多的列,在创建表的时候列族就必须指定。
在并发的情况下,HBase的单个IO延迟下降并不多,能获得高并发、低延迟的服务。
HBase的列具有灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。
逻辑思维层面 HBase的存储模型如下:

表由一个或者多个
列族构成。数据的属性如name、age、TTL(超时时间)等都在列族里边定义。定义完列族的表是个空表,只有添加了数据行以后,表才有数据。
HBase 中的每个列都由 Column Family(列族) 和 Column Qualifier(列限定符)进行限定,例如 info:name、info:age。建表时只需指明列族,而列限定符无需预先定义。
组合成一个列族。建表时不用创建列,在 HBase 中列是可增减变化的!唯一要确定的是列族,表有几个列族在开始创建时就定好的。表的很多属性,比如数据过期时间、数据块缓存以及是否使用压缩等都是定义在列族上的。一行包含多个列,这些列通过列族来分类。行中的数据所属的列族从该表所定义的列族中选取。由于HBase是一个面向列存储的数据库,所以一个
行中的数据可以分布在不同的服务器上。
RowKey 类似 MySQL 中的主键,在 HBase 中 RowKey 必须有且 RowKey 是按照字典排序的,如果用户不指定 RowKey 系统会自动生成不重复字符串。查询数据时只能根据 RowKey 进行检索,所以 Table 的 RowKey 设计十分重要。
RegionServer 就是存放Region的容器,直观上说就是服务器上的一个服务。负责管理维护 Region。
以上只是一个基本的逻辑结构,底层的物理存储结构才是重中之重的内容,看下图

命名空间,类似关系型数据库 DatabBase 概念,每个命名空间下有多个表。HBase有两个自带的命名空间,分别是
hbase和default,hbase 中存放的是 HBase 内置的表,default 表是用户默认使用的命名空间。
时间戳,用于标识数据的不同版本(version),每条数据写入时如果不指定时间戳,系统会自动添加为其写入 HBase 的时间。并且读取数据的时候一般只拿出数据的Type符合,时间戳最新的数据。之所以按照Type取数据是因为HBase的底层
HDFS支持增删查,但不支持改。
单元格,由 {rowkey, column Family:column Qualifier, time Stamp} 唯一确定的单元。cell 中的数据是
没有类型的,全部是字节码形式存储。

Client 包含了访问 Hbase 的接口,另外 Client 还维护了对应的 cache 来加速 Hbase 的访问,比如缓存元数据的信息。
HBase 通过 Zookeeper 来做 Master 的高可用、RegionServer 的监控、元数据的入口以及集群配置的维护等工作。Zookeeper 职责如下:
Master 在 HBase 中的地位比其他类型的集群弱很多!数据的读写操作与他没有关系,它挂了之后,集群照样运行。但是Master 也不能宕机太久,有很多必要的操作,比如创建表、修改列族配置等DDL跟Region的分割与合并都需要它的操作。
HBase 中可以启动多个Master,通过 Zookeeper 的 Master Election 机制保证总有一个 Master 运行。
HregionServer 直接对接用户的读写请求,是真正的干活的节点。它的功能概括如下:
ZooKeeper 会监控 RegionServer 的上下线情况,当 ZK 发现某个 HRegionServer 宕机之后会通知 Master 进行失效备援。下线的 RegionServer 所负责的 Region 暂时停止对外提供服务,Master 会将该 RegionServer 所负责的 Region 转移到其他 RegionServer 上,并且会对 下线RegionServer 上存在 MemStore 中还未持久化到磁盘中的数据由 WAL重播进行恢复。
WAL (Write-Ahead-Log) 预写日志是 HBase 的 RegionServer 在处理数据插入和删除的过程中用来记录操作内容的一种日志。每次Put、Delete等一条记录时,首先将其数据写入到 RegionServer 对应的HLog文件中去。只有当WAL日志写入成功的时候,客户端才会被告诉提交数据成功。如果写WAL失败会告知客户端提交失败,这其实就是数据落地的过程。
WAL是保存在HDFS上的持久化文件。数据到达 Region 时先写入WAL,然后被加载到MemStore中。这样就算Region宕机了,操作没来得及执行持久化,也可以再重启的时候从WAL加载操作并执行。跟Redis的AOF类似。
每一个 Region 都有起始 RowKey 和结束 RowKey,代表了存储的Row的范围。从大图中可知一个Region有多个Store,一个Store就是对应一个列族的数据,Store 由 MemStore 和 HFile 组成的。
Store 由 MemStore 跟 HFile 两个重要的部分。
每个 Store 都有一个 MemStore 实例,数据写入到 WAL 之后就会被放入 MemStore 中。MemStore是内存的存储对象,当 MemStore 的大小达到一个阀值(默认64MB)时,MemStore 会被 flush到文件,即生成一个快照。目前HBase 会有一个线程来负责MemStore 的flush操作。
MemStore 内存中的数据写到文件后就是StoreFile,StoreFile底层是以 HFile 的格式保存。HBase以Store的大小来判断是否需要切分Region。
在Store中有多个HFile,每次刷写都会形成一个HFile文件落盘在HDFS上。HFile文件也会动态合并,它是数据存储的实体。
这里提出一点疑问:操作到达Region时,数据进入HFile之前就已经被持久化到WAL了,而WAL就是在HDFS上的,为什么还要从WAL加载到MemStore中,再刷写成HFile呢?
HDFS 为 HBase 提供最终的底层数据存储服务,HBase 底层用HFile格式 (跟hadoop底层的数据存储格式类似) 将数据存储到HDFS中,同时为HBase提供高可用(Hlog存储在HDFS)的支持,具体功能概括如下:
在HBase集群中如果我们做 DML 操作是不需要关心 HMaster 的,只需要从 ZooKeeper 中获得hbase:meta 数据地址,然后从RegionServer中增删查数据即可。


HBase 在实现中提供了两种缓存结构 MemStore(写缓存) 和 BlockCache(读缓存)。写缓存前面说过不再重复。
重点:
读数据时
不要理解为先从 MemStore 中读取,读不到再读 BlockCache 中,还读不到再从HFile中读取,然后将数据写入到 BlockCache 中。因为如果人为设置导致磁盘数据new,内存数据old。你读取的时候会出错的!
结论:
HBase 把磁盘跟内存数据一起读,然后把磁盘数据放到 BlockCache中,BlockCache 是磁盘数据的缓存。HBase 是个读比写慢的工具。

对于用户来说数据写到 MemStore 中就算OK,但对于底层代码来说只有数据刷到硬盘中才算彻底搞定了!因为数据是要写入到WAL(Hlog)中再写入到MemStore中的,flush有如下几个时机。
由于 MemStore 每次刷写都会生成一个新的 HFile,且同一个字段的不同版本(timestamp) 和不同类型(Put/Delete)有可能会分布在不同的 HFile 中,因此查询时需要遍历所有的 HFile。为了减少 HFile 的个数跟清理掉过期和删除的数据,会进行 StoreFile Compaction。
Compaction 分为两种,分别是 Minor Compaction 和 Major Compaction。

每个 Table 起初只有一个 Region,随着不断写数据 Region 会自动进行拆分。刚拆分时,两个子 Region 都位于当前的 Region Server,但出于负载均衡的考虑, HMaster 有可能会将某个 Region 转移给其他的 Region Server。

Region Split 时机:
当 1 个 Region 中的某个 Store 下所有 StoreFile 的总大小超过 hbase.hregion.max.filesize(默认10G), 该 Region 就会进行拆分。
当 1 个 Region 中的某个 Store 下所有 StoreFile 的总大小超过 Min(R^2 * “hbase.hregion.memstore.flush.size=128M”,hbase.hregion.max.filesize"),该 Region 就会进行拆分,其 中 R 为当前 Region Server 中属于该 Table 的个数。
举例:
官方不建议用多个列族,比如有CF1,CF2,CF3,但是 CF1数据很多而CF2跟CF3数据很少,那么当触发了region切分的时候,会把CF2跟CF3分成若干小份,不利于系统维护。
二进制码流RowKey 最大长度 64Kb,实际应用中一般为 10-100bytes,以 byte[] 形式保存,一般设计定长。建议越短越好,因为HFile是按照KV存储的Key太大浪费空间。
RowKey 在设计时候要尽可能的实现可以将数据均衡的分布在每个 RegionServer 上。
RowKey 必须在设计上保证其唯一性,RowKey 是按照字典顺序排序存储的,因此设计 RowKey 时可以将将经常读取的数据存储到一块。
其实就简单的把HBase当成大数据体系下的DataBase来用就行,任何可以分析HBase的引擎比如MR、Hive、Spark等框架连接上HBase都可以实现控制。比如你可以把Hive跟HBase进行关联,Hive中数据不再由HDFS存储而是存储到HBase中,并且关联后Hive中添加数据在HBase中可看到,HBase中添加数据Hive也可看到。
HBase中有几个内容会动态调整,如Region(分区)、HFile。通过一些方法可以减少这些会带来I/O开销的调整。
没有预建分区的话,随着Region中条数的增加,Region会进行分裂,这将增加I/O开销,所以解决方法就是根据你的RowKey设计来进行预建分区,减少Region的动态分裂。
MemStore执行flush会生成HFile,同时HFilewe年过多时候也会进行Merge, 为了减少这样的无谓的I/O开销,建议估计项目数据量大小,给HFile设定一个合适的值。
数据库事务机制就是为了更好地实现批量写入,较少数据库的开启关闭带来的开销,那么HBase中也存在频繁开启关闭带来的问题。
HBase 中自动化的Minor Compaction和Major Compaction会带来极大的I/O开销,为了避免这种不受控制的意外发生,建议关闭自动Compaction,在闲时进行compaction。
开启BloomFilter,BloomFilter是列族级别的过滤,在生成一个StoreFile同时会生成一个MetaBlock,用于查询时过滤数据
一般推荐使用Snappy和LZO压缩
HBase 表格中 RowKey 和 ColumnFamily 的设计是非常重要,好的设计能够提高性能和保证数据的准确性。
指标 | 传统关系数据库 | HBase |
|---|---|---|
数据类型 | 有丰富的数据类型 | 字符串 |
数据操作 | 丰富操作,复杂联表查询 | 简单CRUD |
存储模式 | 基于行存储 | 基于列存储 |
数据索引 | 复杂的多个索引 | 只有RowKey索引 |
数据维护 | 新覆盖旧 | 多版本 |
可伸缩性 | 难实现横向扩展 | 性能动态伸缩 |
大数据导入用 HBase API 跟 MapReduce 写入效率会很低,因为请求RegionServer 将数据写入,这期间数据会先写入 WAL 跟 MemStore,MemStore 达到阈值后会刷写到磁盘生成 HFile文件,HFile文件过多时会发生Compaction,如果Region大小过大时也会发生Split。

BulkLoad 适合初次数据导入,以及HBase与Hadoop为同一集群。BulkLoad 是使用 MapReduce 直接生成 HFile 格式文件后,Region Servers 再将 HFile 文件移动到相应的Region目录下。
