欧阳亮的博客

编程不止是一份工作,还是一种乐趣!!!

HBase简介

HBase是一个建立在Hadoop之上的高可靠、高性能、面向列、可伸缩的分布式存储系统,可以存储海量数据并对海量数据进行检索。利用HBase技术可在廉价PC上搭建起大规模结构化存储集群。HBase使用HDFS作为底层文件存储系统,在其上可以运行MapReduce任务批量处理数据。


HBase的特点


  1. 大容量

    HBase单表可以有百亿行、百万列,数据矩阵横向和纵向两个维度所支持的数据量级都非常具有弹性。

  2. 列式存储

    HBase是面向列的存储和权限控制,并支持列独立检索。

  3. 高扩展性

    HBase底层文件存储依赖HDFS,从“基因”上决定了其具备可扩展性。HBase以Region的方式对数据进行分区,分区后数据可以位于不同的机器上,所以在HBase核心架构层面也具备可扩展性。此外,HBase的扩展性是热扩展,在不停止现有服务的前提下,可以随时添加或者减少节点。

  4. 高可靠性

    HBase提供WAL和Replication机制。前者保证了数据写入时不会因集群异常而导致写入数据的丢失;后者保证了在集群出现严重问题时,数据不会发生丢失或者损坏。而且HBase底层使用的HDFS本身很大程度上保证了HBase的可靠性。同时,协调服务的ZooKeeper组件具备高可用性和高可靠性。

  5. 高性能

    LSM数据结构和Rowkey有序排列等架构上的独特设计,使得HBase具备非常高的写入性能。Region切分、主键索引和缓存机制使得HBase在海量数据下具备一定的随机读取性能,该性能针对Rowkey的查询能够达到毫秒级别。同时,HBase对于高并发的场景也具备很好的适应能力。


HBase架构设计


在HBase集群中,主要有客户端Client、HMaster、HRegionServer和Zookeeper等几个主要角色。在底层,它将数据存储于HDFS中,其总体架构如下所示:

hbase arch

Client

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


主节点HMaster

  1. 管理HRegionServer,实现其负载均衡。
  2. 管理和分配HRegion,比如在HRegion拆分时分配新的HRegion;在HRegionServer退出时迁移其内的HRegion到其他HRegionServer上。
  3. 监控集群中所有HRegionServer的状态(通过Heartbeat和监听ZooKeeper中的状态)。
  4. 处理Schema更新请求 (创建、删除、修改Table的定义等)。
  5. HMaster没有单点问题,在Hbase中可以启动多个HMaster,通过Zookeeper的Master Election机制保证总有一个Master胜出。


HRegionServer

  1. HRegionServer维护HMaster分配给它的region,处理这些Region的IO请求。
  2. HRegionServer负责切分在运行过程中变得过大的Region。


ZooKeeper

  1. 通过选举,保证任何时候,集群中只有一个HMaster,HMaster与HRegionServer启动时会向ZooKeeper注册。
  2. 实时监控HRegionServer的上线和下线信息,并实时通知给HMaster。
  3. 存贮所有Region的寻址入口和HBase的Schema和Table元数据。
  4. Zookeeper的引入实现HMaster主从节点的failover。


小结

  1. Client访问HBase数据不需要HMaster参与(寻址访问ZooKeeper,数据读写访问HRegionServer),HMaster仅仅维护Table和Region的元数据信息,负载很低。
  2. HRegion所处理的数据尽量和数据所在的DataNode在一起,实现数据的本地化。


HBase数据模型


  • Table

    与传统关系型数据库类似,HBase以表(Table)的方式组织数据。

  • Row

    HBase表中的行通过RowKey(类似于DB表的主键)进行唯一标识,不论是数字还是字符串,最终都会转换成字段数据进行存储。HBase表中的行是按RowKey字典顺序排列的。

  • Column Family

    HBase表由行和列共同组织,同时引入列族的概念,它将一列或多列组织在一起。HBase的列必须属于某一个列族,在创建表时只需指定表名和至少一个列族。

  • Cell

    行和列的交叉点称为单元格,单元格的内容就是列的值,以二进制形式存储。

  • Version

    每个Cell的值可保存数据的多个版本(到底支持几个版本可在建表时指定),按时间顺序倒序排列,时间戳是64位的整数,可在写入数据时赋值,也可由RegionServer自动赋值。


  • 小结

    1. HBase没有数据类型,任何列值都被转换成字符串进行存储。
    2. 与关系型数据库在创建表时需明确包含的列及类型不同,HBase表的每一行可以有不同的列。
    3. 相同RowKey的插入操作被认为是同一行的操作。即相同RowKey的二次写入操作,第二次是对该行某些列的更新操作。
    4. 列由列族和列名连接而成,分隔符是冒号,如 d:Name(d: 列族名, Name: 列名)。
    5. HBase不支持条件查询和Order by等查询,读取记录只能按Row key(及其range)或全表扫描。
    6. 在表创建时只需声明表名和至少一个列族名,每个Column Family为一个存储单元。
    7. 列是可以增加和删除的,同一Column Family的Columns会群聚在一个存储单元上,并依Column key排序,因此设计时应将具有相同I/O特性的Column设计在一个Column Family上以提高性能。
    8. HBase通过row和column确定一份数据,这份数据的值可能有多个版本,不同版本的值按照时间倒序排序,即最新的数据排在最前面,查询时默认返回最新版本。
    9. 每个单元格值通过4个键唯一索引,TableName+RowKey+ColumnKey+Timestamp -> Value。
    10. 存储类型
      • TableName是字符串
      • RowKey和ColumnName是二进制值(Java 类型 byte[])
      • Timestamp是一个64位整数(Java 类型 long)
      • Value是一个字节数组(Java类型 byte[])


HRegionServer


HRegionServer一般和DataNode在同一台机器上运行,实现数据的本地性。HRegionServer包含多个HRegion,由WAL(HLog)、BlockCache、MemStore、HFile组成,内部结构如下:

HRegionServer

  • WAL

    即Write Ahead Log,在早期版本中称为HLog,它是HDFS上的一个文件,如其名字所表示的,所有写操作都会先保证将数据写入这个Log文件后,才会真正更新MemStore,最后写入HFile中。

    采用这种模式,可以保证HRegionServer宕机后,我们依然可以从该Log文件中读取数据,Replay所有的操作,而不至于数据丢失。这个Log文件会定期Roll出新的文件而删除旧的文件(那些已持久化到HFile中的Log可以删除)。WAL文件存储在/hbase/WALs/${HRegionServer_Name}的目录中,一般一个HRegionServer只有一个WAL实例,也就是说一个HRegionServer的所有WAL写都是串行的(就像log4j的日志写也是串行的),这当然会引起性能问题,因而在HBase 1.0之后,通过HBASE-5699实现了多个WAL并行写(MultiWAL),该实现采用HDFS的多个管道写,以单个HRegion为单位。

  • BlockCache

    BlockCache是一个采用LRU算法的读缓存。HBase中提供两种BlockCache的实现:默认on-heap LruBlockCache和BucketCache(通常是off-heap)。通常BucketCache的性能要差于LruBlockCache,然而由于GC的影响,LruBlockCache的延迟会变的不稳定,而BucketCache由于是自己管理BlockCache,而不需要GC,因而它的延迟通常比较稳定,这也是有些时候需要选用BucketCache的原因。

  • HRegion

    HRegion是一个Table中的一个Region在一个HRegionServer中的表达。一个Table可以有一个或多个Region,他们可以在一个相同的HRegionServer上,也可以分布在不同的HRegionServer上,一个HRegionServer可以有多个HRegion,他们可以属于不同的Table。HRegion由多个Store(HStore)构成,每个HStore对应了一个Table在这个HRegion中的一个Column Family,即每个Column Family就是一个集中的存储单元,因而最好将具有相近IO特性的Column存储在一个Column Family,以实现高效读取(数据局部性原理,可以提高缓存的命中率)。HStore是HBase中存储的核心,它实现了读写HDFS功能,一个HStore由一个MemStore和0个或多个StoreFile组成。

  • MemStore

    MemStore是一个写缓存(In Memory Sorted Buffer),所有数据的写在完成WAL日志写后,会写入MemStore中,MemStore根据一定的算法将数据Flush到底层HDFS文件中(HFile),通常每个HRegion中的每个Column Family有一个自己的MemStore。

  • HFile

    HFile(StoreFile)用于存储HBase的数据(Cell/KeyValue)。在HFile中的数据是按RowKey、Column Family、Column排序,对相同的Cell(即这三个值都一样),则按timestamp倒序排列。


Region查找


为了让客户端找到包含特定RowKey的Region,HBase提供了两张特殊的表:-ROOT-.META.。其中-ROOT- Table的位置存储在ZooKeeper,它存储了.META. Table的RegionInfo信息,并且它只能存在一个HRegion,而.META. Table则存储了用户Table的RegionInfo信息,它可以被切分成多个HRegion,因而对第一次访问用户Table时,首先从ZooKeeper中读取-ROOT- Table所在HRegionServer;然后从该HRegionServer中根据请求的TableName,RowKey读取.META. Table所在HRegionServer;最后从该HRegionServer中读取.META. Table的内容而获取此次请求需要访问的HRegion所在的位置,然后访问该HRegionSever获取请求的数据,这需要三次请求才能找到用户Table所在的位置,然后第四次请求开始获取真正的数据。当然为了提升性能,客户端会缓存-ROOT-表及.META.表的内容。


可是即使客户端有缓存,在初始阶段需要三次请求才能直到用户Table真正所在的位置也是性能低下的,而且真的有必要支持那么多的HRegion吗?或许对Google这样的公司来说是需要的,但是对一般的集群来说好像并没有这个必要。在BigTable的论文中说,每行METADATA存储1KB左右数据,中等大小的Tablet(HRegion)在128MB左右,3层位置的Schema设计可以支持2^34个Tablet(HRegion)。即使去掉-ROOT- Table,也还可以支持2^17(131072)个HRegion, 如果每个HRegion还是128MB,那就是16TB,这个貌似不够大,但是现在的HRegion的最大大小都会设置的比较大,比如我们设置了2GB,此时支持的大小则变成了4PB,对一般的集群来说已经够了,因而在HBase 0.96以后去掉了-ROOT- Table,只剩下这个特殊的目录表叫做Meta Table(hbase:meta),它存储了集群中所有用户HRegion的位置信息,而ZooKeeper的节点中(/hbase/meta-region-server)存储的则直接是这个Meta Table的位置,并且这个Meta Table如以前的-ROOT- Table一样是不可split的。这样,客户端在第一次访问用户Table的流程就变成了:


  1. 从ZooKeeper(/hbase/meta-region-server)中获取hbase:meta的位置(HRegionServer的位置),缓存该位置信息。
  2. 从HRegionServer中查询用户Table对应请求的RowKey所在的HRegionServer,缓存该位置信息。
  3. 从查询到HRegionServer中读取Row。

Region Locate

从这个过程中,我们发现客户会缓存这些位置信息,然而第二步它只是缓存当前RowKey对应的HRegion的位置,因而如果下一个要查的RowKey不在同一个HRegion中,则需要继续查询hbase:meta所在的HRegion,然而随着时间的推移,客户端缓存的位置信息越来越多,以至于不需要再次查找hbase:meta Table的信息,除非某个HRegion因为宕机或Split被移动,此时需要重新查询并且更新缓存。


hbase:meta表

hbase:meta表存储了所有用户HRegion的位置信息,它的RowKey是:tableName,regionStartKey,regionId,replicaId等,它只有info列族,这个列族包含三个列:

  • info:regioninfo:regionId,tableName,startKey,endKey,offline,split,replicaId
  • info:server格式:HRegionServer对应的server:port。
  • info:serverstartcode: HRegionServer的启动时间戳。

meta table


HBase的写路径


当客户端发起一个Put请求时,首先它从hbase:meta表中查出数据最终需要去的HRegionServer,然后客户端将Put请求发送给相应的HRegionServer,在HRegionServer中它首先会将该Put操作写入WAL日志文件中(Flush到磁盘中)。

writes to WAL

写完WAL日志文件后,HRegionServer根据Put中的TableName和RowKey找到对应的HRegion,并根据Column Family找到对应的HStore,并将Put写入到该HStore的MemStore中。此时写成功,并返回通知客户端。

writes to Memstore


MemStore刷写


MemStore是一个In Memory Sorted Buffer,每个HStore都有一个MemStore,即它是一个HRegion的一个Column Family对应一个实例。它的排列顺序以RowKey、Column Family、Column的顺序以及Timestamp的倒序,如下所示:

Memstore

每一次Put/Delete请求都是先写入到MemStore中,当MemStore满后会Flush成一个新的StoreFile(HFile),即一个HStore(Column Family)可以有0个或多个StoreFile。需要注意的是MemStore的最小Flush单元是HRegion而不是单个MemStore,这也是Column Family有个数限制的其中一个原因,太多的Column Family一起Flush会引起性能问题

在MemStore Flush过程中,还会在尾部追加一些meta数据,其中就包括Flush时最大的WAL sequence值,以告诉HBase这个StoreFile写入的最新数据的序列。在HRegion启动时,这个sequence会被读取,并取最大的作为下一次更新时的起始sequence。

Memstore Flush


HFile文件


HBase的数据以KeyValue的形式顺序的存储在HFile中,HFile在MemStore的Flush过程中生成。由于MemStore中存储的Cell遵循相同的排列顺序,因而Flush过程是顺序写,磁盘的顺序写性能很高,因为不需要不停的移动磁盘指针。

从HBase开始到现在,HFile经历了三个版本,其中V2在0.92引入,V3在0.98引入。首先我们来看一下V1的格式:

HFile

V1的HFile由多个Data Block、Meta Block、FileInfo、Data Index、Meta Index、Trailer组成,其中Data Block是HBase的最小存储单元,在前文中提到的BlockCache就是基于Data Block的缓存的。一个Data Block由一个魔数和一系列的KeyValue组成,魔数是一个随机的数字,用于表示这是一个Data Block类型,以快速监测这个Data Block的格式,防止数据的破坏。

Data Block的大小可以在创建Column Family时设置(HColumnDescriptor.setBlockSize()),默认值是64KB,大号的Block有利于顺序Scan,小号Block利于随机查询,因而需要权衡。Meta块是可选的,FileInfo是固定长度的块,它纪录了文件的一些Meta信息。Data Index和Meta Index纪录了每个Data块和Meta块的起始位置。Trailer纪录了FileInfo、Data Index、Meta Index块的起始位置,Data Index和Meta Index索引的数量等。其中FileInfo和Trailer是固定长度的。

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

KeyValue

开始是两个固定长度的数值,分别表示Key的长度和Value的长度。紧接着是Key,开始是固定长度的数值,表示RowKey的长度,紧接着是RowKey,然后是固定长度的数值,表示Family的长度,然后是Family,接着是Qualifier,然后是两个固定长度的数值,表示Time Stamp和Key Type(Put/Delete)。Value部分没有这么复杂的结构,就是纯粹的二进制数据了。随着HFile版本迁移,KeyValue的格式并未发生太多变化,只是在V3版本,尾部添加了一个可选的Tag数组。

HFile V1版本在实际使用过程中占用内存过多,并且Bloom File和Block Index会变的很大,导致启动时间变长。每个HFile的Bloom Filter可以增长到100MB,这在查询时会引起性能问题,因为每次查询时需要加载并查询Bloom Filter,100MB的Bloom Filer会引起很大的延迟;另一方面,Block Index在一个HRegionServer可能会增长到总共6GB,HRegionServer在启动时需要先加载所有这些Block Index,因而增加了启动时间。为了解决这些问题,在0.92版本中引入HFileV2版本:

Hfile V2

在这个版本中,Block Index和Bloom Filter添加到了Data Block中间,而这种设计同时也减少了写的内存使用量;另外,为了提升启动速度,在这个版本中还引入了延迟读的功能,即在HFile真正被使用时才对其进行解析。

FHile V3版本和V2版本相比,并没有太大的改变,它在KeyValue层面上添加了Tag数组的支持;并在FileInfo结构中添加了和Tag相关的两个字段。关于具体HFile格式演化介绍,可以参考这里

对HFile V2格式具体分析,它是一个多层的类B+树索引,采用这种设计,可以实现查找不需要读取整个文件:

Hfile V2 structure

Data Block中的Cell都是升序排列,每个block都有它自己的Leaf-Index,每个Block的最后一个Key被放入Intermediate-Index中,Root-Index指向Intermediate-Index。在HFile的末尾还有Bloom Filter用于快速定位那么没有在某个Data Block中的Row;TimeRange信息用于给那些使用时间查询的参考。在HFile打开时,这些索引信息都被加载并保存在内存中,以增加以后的读取性能。


HBase读路径


看到这里,你应该明白在Hbase中一行数据可能出现在不同的地方:HFile文件中、MemStore中或者Block Cache中都有可能。当我们Get一行数据时,HBase使用一种Merge Read的方式来查找对应的数据:

  1. 首先,HBase会从Block Cache中查找数据。近期查询过的数据会在这里,前面我们说过,Block Cache是一个采用LRU算法的缓存。
  2. 其次,HBase会从MemStore中查找数据,近期更新的数据会在这里。
  3. 最后,如果在Block Cache和MemStore中没有获得全部的KeyValues,HBase会使用Block Cache Index及Bloom filters的信息来加载HFiles文件读取需要的信息。

前面说过,一个列族可能包括多个HFile,所有一次读请求可能会导致加载多个文件,我们称之为Read Amplification,Read Amplification对性能是有影响的。

Read Path

HBase Minor Compaction


随着MemStore的刷写,会产生越来越多的HFile文件,前面我们讲过,过多的HFile文件对HBase的读性能会产生很大的影响。所以,HBase会简单的将HFiles文件合并成更大一文件,这个过程叫做Minor Compaction。

Minor Compaction

HBase Major compaction


相比于Minor Compaction,Major Compaction做得更加彻底。它会将一个列族下所有的HFile文件压缩成一个HFile文件。这里我们说压缩,是因为Major Compaction不是简单的将多个HFile合并成一个HFile,实际上在合并过程,它还会清理掉有删除标方或已经过期的Cells。也正因为此,Major Compaction过程会占用很大的I/O和网络资源,称之为Write amplification。

Major Compaction

Region拆分


HBase中初始情况下每个表只有一个Region,随着数据的增加,表的大小变得越来越大。当表大小超过hbase.hregion.max.filesize时,HBase会将该表一分为二,从正中间拆分成两个大小相等的Region,并将拆分的情况告诉HMaster。新的Region有可能被HMaster移动到其它的HRegionServer,这也是HBase实现负载均衡的方式。

Region Split

HBase集群部署


我们之前已经部署了一个hadoop环境《HDFS简介》《MapReduce简介》,现在我们接着在上面部署HBase。其中192.168.0.161为HMaster,192.168.0.162与192.168.0.163为Region Server。


一、将HBase安装文件分别解压到各个服务器的/opt/local目录下面

sudo tar -zvxf hbase-1.2.6-bin.tar.gz -C /opt/local


二、修改HBase安装目录所有者权限(三台服务器一样)

cd /opt/local
sudo chown -R john:john hbase-1.2.6


三、修改hadoop-env.sh文件

export JAVA_HOME=/opt/local/jdk1.8.0_151 #设置JAVA_HOME
export HBASE_MANAGES_ZK=false #依赖外部独立zookeeper集群


四、修改hbase-site.xml文件(三台服务器一样)

<property>
  <name>hbase.rootdir</name>
  <value>hdfs://192.168.0.161:9000/hbase</value>
</property>
<property>
  <name>hbase.cluster.distributed</name>
  <value>true</value>
</property>
<property>
  <name>hbase.zookeeper.property.clientPort</name>
  <value>2181</value>
</property>
<property>
  <name>hbase.zookeeper.quorum</name>
  <value>192.168.0.161,192.168.0.162,192.168.0.163</value>
</property>
<property>
  <name>hbase.zookeeper.property.dataDir</name>
  <value>/hbase</value>
</property>


四、修改regionservers文件(192.168.0.161)

192.168.0.162
192.168.0.163


五、启动HDFS集群,参照《HDFS简介》《MapReduce简介》


六、启动Zookeeper集群,参照《zookeeper集群部署》


七、启动HBase集群(192.168.0.161)

/opt/local/hbase-1.2.6/bin/start-hbase.sh


现在我们可以通过http://192.168.0.161:16010访问HBase控制平台了,可以看到192.168.0.162和192.168.0.163两台Region Server。

hbase web

参考文献《An In-Depth Look at the HBase Architecture》