简易版架构图:
复杂版架构图:
架构讲解见《Hadoop系列四——HBase简介》。
在《Hadoop系列四——HBase简介》里面已经介绍过HBase的数据模型了,但这个的确非常重要,这里再做一些补充。HBase是根据Google的Bigtable论文实现的开源版"Bigtable",所以对Bigtable的描述同样适用于HBase:
A Bigtable is a sparse, distributed, persistent multi-dimensional sorted map. The map is indexed by a row key, column key, and a timestamp; each value in the map is an uninterpreted array of bytes.
这里有几个关键词我已经加粗了,从这几个关键词可以引出来多个问题。
第一个就是sparse关键字。首先NULL值是RDBMS里面的概念,HBase里面其实没有这个概念,这里只是为了作对比,介绍稀疏性这个特性。我们把HBase想成一个二维矩阵(这样不准确,但有助于理解),那么其实就是一个稀疏矩阵。在RDBMS里面,没有值的地方一般用NULL表示,虽然相比于其他类型,NULL占很小的空间(一般是1bit),但仍然是占空间的,在RDBMS里面这没有问题,主要有两个原因:(1)RDBMS里面数据量不会很大,目前RDBMS能承载的最大数据量一般也就是百万级别的。(2)如果把RDBMS也看成二维矩阵的话,一般RDBMS都属于稠密矩阵,所以NULL值不会占大多数。
但HBase不一样,它正好与RDBMS的特性相反:数据量很大,一般都大于百万级别(如果小于这个量级,那你该重新考虑是否有必要使用HBase了);NULL值可能占很大部分,这个和HBase的设计使用有关系。比如一个表里面有1百万个列,有1千万行数据,每一行里面可能只有一小部分列有值,其他列都是“NULL值”(实质是没有值)。所以,在HBase里面,"NULL值"是不存储的,即不占用任何存储。其实这个和稀疏矩阵的存储原理上是一样的,只存储有数据的cell的“坐标”及值,其它的默认就是NULL。当我们在HBase里面获取一行数据的时候,只会返回有数据的列,不像RDBMS会返回所有列,没有值的用NULL占位。
更多信息见本文“HBase的逻辑存储和物理存储”部分。
想想如果因为性能或存储等不够了,需要将RDBMS扩展成分布式的,能否做到?也许可以做到,但一般是受限的,而且难度比较高,比如像业内的GreenPlum,如果还想要保证RDBMS的所有特性,那就只能用更好的硬件做垂直扩展了。但HBase本身就是分布式的,扩展一个HBase节点基本上没有什么难度。所以这也是很多企业选择HBase的一个非常重要的考量。
多维的概念在我的《Hadoop系列四——HBase简介》里面已经讲过了。
有序之前也提过,在HBase里面,不论是内部存储,还是查询返回的数据,都是有序的:依次按照row key、Column Family、Column qualifier、Timestamp四个维度排序,时间戳是按照从新到旧排的,其它都是字典序。
上面对于value的描述是“uninterpreted array of bytes”,也就是没有类型,默认按字节流解析(我们知道任何类型其实都可以按字节流其解析)。其实在HBase里面除了会被作为路径的名称之外,其它都是作为字节流的,也就是没有类型。具体就是:表名、Column Family会作为路径名,所以名称必须是合法的路径名,也就是必须是可打印字符;row key,Column qualifier、Cell都是没有类型的,也就是可以是任何数据。实际中,我们经常会在row key、Column qualifier中存储数据,而不光是cell里面。
引用HBase:The Definitive Guide上的一幅图:
第一幅图(左上角)是HBase里面一个典型的表的逻辑图,里面有cf1和cf2两个Column Family(以下简称CF),每个CF里面有两列。里面的红色和黄色小块表示有数据,其他地方都没有数据(如前面所说,是一个稀疏矩阵),多个层叠的部分表示有多个版本的数据。
第二幅图(右上角)也是一个逻辑图,主要为了说明以下几个点:
第三幅图(右下角)是上面的逻辑存储在物理文件上的存储形式,需要注意以下几个点:
第四幅图(左下角)是为了说明查询时指定各个Key对性能的影响:
STARTROW
和STOPROW
指定row key范围。Tall-Narrow也就是我们所说的“窄表”,Flat-Wide是“宽表”。举个例子,比如我们要存储一个用户的邮件信息:用户ID、邮件ID、邮件内容。如果按照Tall-Narrow的思想去设计,表结构可能是下面这样:
# 将用户id和邮件id拼接成row key,邮件内容作为一列数据
userid-emailid, cf:emailbaody
如果按照Tall-Narrow思路去设计,表结构可能是下面这样:
# userid为row key,emailid和emailbody作为两个列
userid, cf:emailid, cf:emailbody
两种设计各有利弊,使用宽表的好处主要在于HBase的ACID特性仅限于行内,所以如果把所有数据都放在一行,那可以很好的利用其ACID特性。而窄表在实际使用中更加常见一些,因为HBase里面的的row key类似于RDBMS里面的主键,所以我们尽可能将要经常查询的维度放在row key里面,可以提高查询性能;另外一个表里面不推荐有太多的Column Family,一般1个最好,最多也不要超过3个,具体见本文“Column Family的数量”部分。
先说结论:可以修改。使用alter
命令可以修改表和Column Family,具体语法可以help "alter"
查看。这里需要注意两点:
我们知道当MemStore里面的数据量达到一定值的时候,就会落盘形成StoreFile(HFile),这样就会形成很多文件,而Compaction就是将这些文件合并成大文件。HBase里面有两种Compaction:Minor compactions和Major compactions,主要有如下区别:
虽然Compaction合并文件是为了提高性能,但合并这个操作却是消耗资源的,就跟Jvm的GC一样。默认Major compactions一周一次。
先说结论:一个表内的Column Family最好1个,最多不要超过3个。原因主要有两点:
版本数指我们之前说的Timestamp,和这个特性相关的设置有两个:max versions和min versions,其含义也很明确。max versions的默认值是1,min versions的默认值是0,表示不启用多版本这个特性,即往一个Cell里面重复写数据会覆盖,而不是保留多个版本。
min versions和HBase的TTL(time-to-live)一起使用。我们可以给Column Family设置一个TTL时间(单位为秒),时间到期后的row会自动被删除。如果一个Storefile里面的row全部是过期的,那么在minor compaction阶段这个Storefile会被删除,我们可以通过把hbase.store.delete.expired.storefile设置为false或者把min versions设置为非0值来关闭删除这个特性。
一般来说,这个选择是比较好做的。HBase、Impala等数据库的诞生并不是为了替代传统的RDBMS,只是为了解决新的RDBMS解决不了或者解决起来比较困难的问题。所以,如果你的数据量并不大(一般以百万为分界线),那一般应该优先选择RDBMS,毕竟RDBMS支持完善的ACID,SQL,多级索引,各种Join,完善的类型等丰富的特性,这些都是HBase所不具备的。
但如果你的数据量非常大,RDBMS已经无法支撑了,那就可以考虑HBase等分布式数据库了。但如果你的业务离不开RDBMS的一些特性(比如各种Join、SQL、完善的ACID等),那可能就需要考虑类似于GreenPlum这种MPP数据库了。
HBase里面只支持受限的ACID(Atomicity, Consistency, Isolation, and Durability):仅支持行内的ACID,跨行不支持。也就是对于同一行的操作是可以保证ACID的,但是多行操作是不行的。更多信息可参考:ACID in HBase。
这两个不是一个层级的东西,没有可比性。如果你还在这二者之间纠结,那可能你对它们有些误会,或者还不清楚你自己的需求。非要比的话,那就是HBase一般做实时查询;而Hive一般作为离线数据仓库,Hive后面是MapReduce/Spark,所以无法做到实时。
先说结论:不支持。HBase读取数据时支持Get和Scan(Get的后台实现是Scan的一种特殊情况而已)操作,RDBMS里面的join在HBase里面是不支持的,但我们可以在表设计上支持一定程度上的"join"操作,比如将需要join的字段拼接起来作为row key。
HBase支持SQL吗?
HBase不支持SQL,只提供了各种API。但Apache下有个Phoenix项目,通过该项目可以使用SQL语句操作HBase。
注意,这里说的是region,不是RegionServer。一个region保持在10~50GB比较好。
一般一个表包含50~100个region和1~2个Column Family比较好。
没有,但一般不要超过10MB(对于MOB对象不要超过50MB)。如果超过了这个大小,可以将对象存到HDFS上面,然后再HBase里面存储HDFS路径。
HBase的region分裂的时候,分裂出来的两个新region称为"daughter",原来的称为"parent"。
Last but not the least,row key的设计是HBase表设计里面最重要的部分,没有之一。Row key设计不合理很容易会引起热点问题(hotspotting)——大量的客户端请求到达同一个节点(RegionServer)或少数一些节点,导致该节点负载急剧上升,性能下降,甚至不可用。这个问题容易出现的原因和HBase自身的设计有关系——row key是按照字典序排列的。当然这个设计是没有问题的,主要是为了快速访问。这里介绍一些常用的避免热点问题的方法。
第一种方式就是加盐值,这里的盐值不同于密码学里面的盐值。实质就是事先定几个随机的前缀,然后给每一个row key加上一个前缀,前缀的选择是随机的。如果你想让数据分布在N个region上,一般就有N个前缀。比如现在我们有如下形式的row key:
foo0001
foo0002
foo0003
foo0004
...
显然这样的row key在HBase里面会连续存储在一起,这样在写的时候就会出现热点问题。此时如果使用加盐值的方式解决的话,我们可以这样操作:假设我们想让数据分布到不同的四个region上,那我们可以选取四个前缀,比如a~d四个字母,这样我们新的row key就变成下面这样了:
a-foo0001
b-foo0002
c-foo0003
d-foo0004
...
这样就将数据打散了。由于这种前缀的选择是随机的,所以相同的row key可能拼接了不同的前缀:
b-foo0001
c-foo0002
a-foo0003
d-foo0004
...
所以这种方式提高了写的吐吞量,但读的时候就会比较麻烦。
Hash其实是对Salt的一种优化。在Salt里面,前缀的选择是随机的,这样相同的row key可能加了不同的前缀。而Hash就是在row key和前缀之间建立一种哈希,使得相同的row key总是加相同的前缀,其它和Salt相同。
这种应该是现在使用比较普遍的一种解决row key热点的技术,对于固定长度(比如卡号、时间戳等)或者固定结构(比如域名)等非常好用。但其缺点就是牺牲了原来key的顺序。
Row key的设计和读写模式、业务、数据都有很大关系,所以不可一概而论,介绍的几种方法没有哪个是普适的,也没有哪个是完美的,具体问题还需具体分析。上面介绍的都是一些理论,关于row key的设计实战的话,这里强力推荐一篇文章:Introduction to hbase Schema Design by AMANDEEP KHURANA)。篇幅稍微有点多,有兴趣的可以看一下原文,这里列一下作者提到的设计表的时候要考虑的一些比较重要的点:
- What should the row key structure be and what should it contain?
- How many column families should the table have?
- What data goes into what column family?
- How many columns are in each column family?
- What should the column names be? Although column names don’t need to be defined on table creation, you need to know them when you write or read data .
- What information should go into the cells?
- How many versions should be stored for each cell?
设计的时候需要将HBase一些重要的特性考虑到:
- Indexing is only done based on the Key.
- Tables are stored sorted based on the row key. Each region in the table is respon-sible for a part of the row key space and is identified by the start and end row key.The region contains a sorted list of rows from the start key to the end key.
- Everything in HBase tables is stored as a byte[ ]. There are no types.
- Atomicity is guaranteed only at a row level. There is no atomicity guarantee across rows, which means that there are no multi-row transactions.
- Column families have to be defined up front at table creation time.
- Column qualifiers are dynamic and can be defined at write time. They are stored as byte[ ] so you can even put data in them.
不论是上面的7个问题,还是后面的6个特性,基本上在本文或者之前HBase的介绍中都有提到。其实,我个人认为对于这些开源系统的学习分两个方面:一方面学习这些系统如何使用,另一方面也可以学习一下它的设计,很多理念和方法在我们设计自己产品的时候也是可以借鉴的。
References:
HBase是Apache下的一个顶级项目,是Hadoop Database的简写。虽然也是数据库,但它不同于传统的关系型数据库,也不同于很多NoSQL,它的诞生就是为了解决海量数据的存储查询.官方对于HBase项目的说明如下:
Use Apache HBase™ when you need random, realtime read/write access to your Big Data. This project's goal is the hosting of very large tables -- billions of rows X millions of columns -- atop clusters of commodity hardware. Apache HBase is an open-source, distributed, versioned, non-relational database modeled after Google's Bigtable: A Distributed Storage System for Structured Data by Chang et al. Just as Bigtable leverages the distributed data storage provided by the Google File System, Apache HBase provides Bigtable-like capabilities on top of Hadoop and HDFS.
概述就到此为止,细节内容我们后面介绍。
任何理论介绍都不如先实际使用一下来的实在,所以我们先介绍如何安装HBase。安装过程和之前的《Hadoop系列一——安装部署》类似,非常简单,步骤如下(安装环境为Linux,发行版选择自己熟悉的就行):
/opt/hbase-1.2.6
目录。为了使用方便,可以将/opt/hbase-1.2.6/bin
目录加到PATH环境变量里面。安装就完成了,理论上不需要进行任何其他配置就可以使用了。
在bin目录下有两个脚本,用于启停HBase:
启动HBase之后,如果通过jps
命令看到了HBase的进程HMaster
,那么恭喜你,你的HBase已经安装并且启动成功了,你已经可以进行简单的使用了。我们输入hbase shell
命令就可以通过shell命令行连接HBase:
➜ ~ hbase shell
SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in [jar:file:/opt/hbase-1.2.6/lib/slf4j-log4j12-1.7.5.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: Found binding in [jar:file:/opt/hadoop-2.7.3/share/hadoop/common/lib/slf4j-log4j12-1.7.10.jar!/org/slf4j/impl/StaticLoggerBinder.class]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual binding is of type [org.slf4j.impl.Log4jLoggerFactory]
HBase Shell; enter 'help<RETURN>' for list of supported commands.
Type "exit<RETURN>" to leave the HBase Shell
Version 1.2.6, rUnknown, Mon May 29 02:25:32 CDT 2017
hbase(main):001:0> status
1 active master, 0 backup masters, 1 servers, 0 dead, 4.0000 average load
hbase(main):002:0>
连接到HBase后,我们可以输入status
命令来查看HBase集群的状态(当然我们现在还不是集群)。虽然这样已经算安装好了,但要真正使用,还是要做一些简单的配置。HBase的配置在conf目录下,比较重要的有两个:
hbase-site.xml:这个文件是HBase的核心配置文件,极大多数配置都是配在该文件里面的,默认这个文件是空的。这里我们增加如下配置:
<configuration>
<property>
<name>hbase.rootdir</name>
<value>/your/path/data</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/your/path/zookeeper</value>
</property>
</configuration>
这里我们配置了HBase和ZooKeeper的数据存储路径,默认是存储在/tmp
目录下的,我们知道OS重启后,/tmp
目录就会被清空,也就是我们的数据会丢失,所以我们需要配置一下数据存储目录。
至此,HBase的安装...还没有完...(^_^)。从前面的介绍中我们知道HBase是分布式的,但刚才的部署过程完全是单机的;另外HBase是构建与HDFS之上的,我们似乎也没用到HDFS。是的,刚才我们的部署过程的确是单机的,而且也没有用到HDFS,用的是本地文件系统。我们现在的部署模式称之为Standalone模式,也就是单机模式,主要用于本地开发测试,这种模式下,HBase所依赖的所有东西都包在一个JVM进程里面,其实和Hadoop的单机模式类似。不过,对于我们平时的一些开发测试,这样部署基本上就够用了。但是对于生产环境,分布式的部署则是必须的了。下面我们介绍分布式部署。
分布式部署里面又分为伪分布式(pseudo-distributed)和真的分布式两种,其实质区别就是看所有的HBase进程是运行在一台主机上(伪分布式)还是多台主机上。但配置方式无实质区别,这里以伪分布式为例。其实也比较简单,只需要将刚才的hbase-site.xml修改如下即可:
<configuration>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.rootdir</name>
<value>hdfs://localhost:8020/hbase</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/your/path/zookeeper</value>
</property>
</configuration>
可以看到,首先需要将hbase.cluster.distributed
置为true,表示我们处于分布式模式,另外需要将hbase数据目录改为分布式目录,这里使用HDFS(如果你还没有部署Hadoop,请参照我之前的文章部署)。当然,HBase也支持其他分布式系统,比如S3等。配置完成后,然后重启HBase,通过jps
命令可以看到相比于之前多了几个进程:
10.9.1.13➜ jps
15952 SecondaryNameNode # HDFS 进程
27761 HRegionServer # HBase 进程
15137 NodeManager # YARN进程
27538 HQuorumPeer # HBase ZK进程
15714 DataNode # HDFS Slave进程
15002 ResourceManager # YARN Master进程
15580 NameNode # HDFS Master进程
28204 Jps
27612 HMaster # HBase Master进程
可以看到HBase现在有了3个进程,其他几个是Hadoop的进程,并不是由start-hbase.sh启动的。除了部署以外,其他的使用单机和分布式没有区别。需要注意一点的是在真正的分布式环境里面,我们使用hbase shell
的时候往往要指定配置文件(hbase-site.xml)的目录,比如hbase --config /etc/hbase/conf shell
,不然可能hbase会因为连接不到ZK而报错。
HBase提供了WebUI可以查看集群的信息:
至此,HBase的安装就介绍完了。
HBase虽然也称之为数据库,但它和我们所熟知的关系型数据库还是差异非常大的,甚至可以说没有什么相似的。另外,我们知道关系型数据库(RDBMS)一般都是行存储,而HBase和很多其他面向大数据的数据库一般都是列式存储,这两种存储的核心区别就在于它们对数据的物理存储方式不同,这里用HBase:The Definitive Guide(HBase权威指南)上面的一幅图来说明:
而这两种不同的存储方式也决定了它们许多截然不同的特性,有兴趣的可以看下这篇文章:行式数据库与列式数据库的对比。
下面我们介绍HBase里面的数据模型,虽然HBase与关系型数据库有着很大的差异,但为了方便理解,我们会做一些类比,方便读者理解。
Namespace从逻辑上将表分组,类似于RDBMS里面的数据库实例。HBase的Namespace主要是为了实现多租户的特性,可以从Namespace上面做一些资源分配、资源隔离、安全管理等功能。HBase有两个内置的Namespace:
HBase有一些和Namespace相关的命令:
# 列出所有namespace
hbase(main):001:0> list_namespace
NAMESPACE
default
hbase
2 row(s) in 0.4030 seconds
# 创建namespace
hbase(main):002:0> create_namespace 'test_ns'
0 row(s) in 0.0730 seconds
hbase(main):003:0> list_namespace
NAMESPACE
default
hbase
test_ns
3 row(s) in 0.0220 seconds
# 修改namespace:增加一个属性
hbase(main):006:0> alter_namespace 'test_ns', {METHOD=> 'set', 'PROPERTY_NAME' => 'PROPERTY_VALUE'}
0 row(s) in 0.0840 seconds
# 查看namespace
hbase(main):007:0> describe_namespace 'test_ns'
DESCRIPTION
{NAME => 'test_ns', PROPERTY_NAME => 'PROPERTY_VALUE'}
1 row(s) in 0.0070 seconds
# 修改namespace:删除某个属性
hbase(main):009:0> alter_namespace 'test_ns', {METHOD=> 'unset', NAME=>'PROPERTY_NAME'}
0 row(s) in 0.0410 seconds
hbase(main):010:0> describe_namespace 'test_ns'
DESCRIPTION
{NAME => 'test_ns'}
1 row(s) in 0.0110 seconds
# 在某个namespace下创建表
hbase(main):011:0> create 'test_ns:test_table', 'col_fam'
0 row(s) in 2.4850 seconds
=> Hbase::Table - test_ns:test_table
# 列举某个namespace下的所有表
hbase(main):014:0> list_namespace_tables 'test_ns'
TABLE
test_table
1 row(s) in 0.0130 seconds
### 删除namespace: 只有空的namespace才可以删除
# disable表
hbase(main):020:0> disable 'test_ns:test_table'
0 row(s) in 2.3380 seconds
# 删除表:表必须处于disabled状态才可以删除
hbase(main):021:0> drop 'test_ns:test_table'
0 row(s) in 1.3290 seconds
# 删除namespace
hbase(main):023:0> drop_namespace 'test_ns'
0 row(s) in 0.0520 seconds
上面的例子基本覆盖了极大多数Namespace相关的命令。注意,Namespace到后台HDFS上实质就是一个目录。
Column Family:一般译为“列簇”,是HBase里面特有的一个非常重要的概念。若干个列组成一个列簇,比如有一个列簇叫courses
,它里面包含math
和history
两个列,在HBase里面这个关系写为:courses:math、courses:history,冒号后面的部分称之为Column qualifier。二者有如下一些特性:
courses:
也是合法的。以上就是HBase数据模型里面的一些关键概念,这里看一张图(该图来自Amandeep khurana的Introduction to hbase Schema Design):
这里展示了HBase中的一张表以及里面的一些数据。可以看到,该表的row key是“0001”、“0002”、...,按字典序排的;表里面有两个Column Family:Personal、Office;Personal里面包含两个列或者叫Column qualifier:Name和Residence phone,Office里面包含两个列:Phone和Address。每一个Cell里面是具体的数据,并且可能有多个版本。再附一张**上面的图:
我们可以将HBase的表看成是一个多维map(multidimensional map),比如上面的表其实就是下面一个多维map:
当然,也可以将HBase的表看成是一个KeyValue存储系统,比如:
上面列了5种情况,我们可以将row key当做key,也可以和其他属性一起作为key。
了解了HBase数据模型的一些概念后,我们来看下HBase表的增删改查操作。
# 创建表
hbase(main):001:0> create 'tablename', 'colfam'
0 row(s) in 2.7860 seconds
=> Hbase::Table - tablename
# 查看表信息
hbase(main):003:0> desc 'tablename'
Table tablename is ENABLED
tablename
COLUMN FAMILIES DESCRIPTION
{NAME => 'colfam', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'false', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', T
TL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'}
1 row(s) in 0.2260 seconds
# 增加几条数据
hbase(main):004:0> put 'tablename', 'row1', 'colfam:q1', 'data1'
0 row(s) in 0.1990 seconds
hbase(main):005:0> put 'tablename', 'row2', 'colfam:q2', 'data2'
0 row(s) in 0.0210 seconds
hbase(main):006:0> put 'tablename', 'row3', 'colfam:q2', 'data3'
0 row(s) in 0.0170 seconds
hbase(main):007:0> put 'tablename', 'row4', 'colfam:', 'data4'
0 row(s) in 0.2040 seconds
# 查看表内所有数据
hbase(main):008:0> scan 'tablename'
ROW COLUMN+CELL
row1 column=colfam:q1, timestamp=1524301718825, value=data1
row2 column=colfam:q2, timestamp=1524301742009, value=data2
row3 column=colfam:q2, timestamp=1524301761977, value=data3
row4 column=colfam:, timestamp=1524301779956, value=data4
4 row(s) in 0.0560 seconds
# 查看某条数据
hbase(main):009:0> get 'tablename', 'row3'
COLUMN CELL
colfam:q2 timestamp=1524301761977, value=data3
1 row(s) in 0.0420 seconds
hbase(main):010:0> disable 'tablename'
0 row(s) in 2.3340 seconds
# 删除表
hbase(main):011:0> drop 'tablename'
0 row(s) in 1.3210 seconds
下面我们简单介绍下上面的操作:
put
命令,插数据的时候必须指定表名、row key、Column Family、数据。Column qualifier可选择性指定。如果row key相同,则HBase认为是Update操作,而不是插入新数据。scan
命令可以查看表里面所有的数据,使用get
命令可以查看某条数据。drop
命令,只有处于disabled状态的表才可以删除。使用help "<command>"
命令可以查看某个命令的详细使用方法,里面一般都有大量的例子,这里就不赘述了。
HBase的架构细说起来还是比较复杂的,有兴趣的可以去看下官方的参考手册,这里只对一些个人认为比较关键的点进行说明。
HBase作为一个分布式系统,也是主从模式:一个Master(HMaster进程)和多个RegionServer(HRegionServer)。
Master负责监控集群内所有的RegionServer,所有和元数据相关的操作也由Master负责,比如RegionServer的负载均衡、故障切换、自动分片等均由Master负责。但因为Master并不存储具体数据,只负责管理,所以相对于RegionServer,Master的负载一般是比较低的。在分布式环境里面,往往和HDFS的Namenode部署在一起。为了HA,一个HBase集群中可以有多个Master,这些Master是相互竞争的(通过ZooKeeper选举)。更多关于HBase Master的信息可以看官方手册和这篇文章:HBase HMaster Architecture.
RegionServer是HBase中真正存储数据的地方,我们先来了解下Region这个概念。在HBase里面,region是实现表的可用性(availability)和分布式特性(distribution)的最基本单元,也是HBase可扩展(scalability)和负载均衡(load balance)的基础。刚开始的时候,一个表只有一个region,数据都存在这个region里面。随着数据量的增大,超出一个region的最大限制值(可设置:hbase.hregion.max.filesize)的时候,region就会从中间的row key处自动分裂成两个region,每个region保存一半的数据,这个过程叫自动分片(auto sharding)。而每个RegionServer上包含若干个region,这也是RegionServer名字的由来。HBase的数据是存储在HDFS上面的,所以分布式环境中,一般RegionServer和HDFS的Datanode部署在一起。相比于Master,RegionServer的负载一般是比较重的,因为它不光要负责管理和存储region外,还要响应客户端的请求。这里需要注意,客户端获取数据的时候,是直接和RegionServer交互的,而不是和Master。具体见下节。
hbase:meta(老版本里面叫.META.)是HBase里面的系统表(hbase时namespace名,meta是表名),从名字就能看出来,该表是存储元数据的。这个表的位置存储在ZooKeeper里面,表的具体内容存在HDFS上面:
# 在ZooKeeper里面查看hbase:meta的位置
➜ ~ zkCli.sh -server 127.0.0.1:2181
...
# 位置信息存储在/hbase/meta-region-server节点
[zk: 127.0.0.1:2181(CONNECTED) 5] get /hbase/meta-region-server
?regionserver:162018?n6m?QPBUF
ubuntu?~?????,
cZxid = 0x157
ctime = Fri Apr 20 15:57:28 CST 2018
mZxid = 0x157
mtime = Fri Apr 20 15:57:28 CST 2018
pZxid = 0x157
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 59
numChildren = 0
# 在Hbase里面查看
hbase(main):019:0> scan 'hbase:meta'
ROW COLUMN+CELL
hbase:namespace,,1522381563209.ab55 column=info:regioninfo, timestamp=1524211050206, value={ENCODED => ab558a3f1c03149e92a71cfcc02d66c7, NAME
8a3f1c03149e92a71cfcc02d66c7. => 'hbase:namespace,,1522381563209.ab558a3f1c03149e92a71cfcc02d66c7.', STARTKEY => '', ENDKEY => ''}
hbase:namespace,,1522381563209.ab55 column=info:seqnumDuringOpen, timestamp=1524211050206, value=\x00\x00\x00\x00\x00\x00\x00\x12
8a3f1c03149e92a71cfcc02d66c7.
hbase:namespace,,1522381563209.ab55 column=info:server, timestamp=1524211050206, value=ubuntu:16201
8a3f1c03149e92a71cfcc02d66c7.
hbase:namespace,,1522381563209.ab55 column=info:serverstartcode, timestamp=1524211050206, value=1524211032085
8a3f1c03149e92a71cfcc02d66c7.
test,,1523524790332.8dbea38c5 column=info:regioninfo, timestamp=1524211049936, value={ENCODED => 8dbea38c5ae8e0e4126afa4215000f6c, NAME
ae8e0e4126afa4215000f6c. => 'test,,1523524790332.8dbea38c5ae8e0e4126afa4215000f6c.', STARTKEY => '', ENDKEY => ''}
test,,1523524790332.8dbea38c5 column=info:seqnumDuringOpen, timestamp=1524211049936, value=\x00\x00\x00\x00\x00\x00\x00\x11
ae8e0e4126afa4215000f6c.
test,,1523524790332.8dbea38c5 column=info:server, timestamp=1524211049936, value=ubuntu:16201
ae8e0e4126afa4215000f6c.
...
hbase:meta的表结构如下:
Row key格式:
[table],[region start key],[region id]
,即表名,起始row key值,region id
。
Column格式:
info:regioninfo
: region信息;info:server
:RegionServer的信息,server:port;info:serverstartcode
:包含该region的RegionServer的进程启动时间。hbase:meta包含了当前集群内所有region的信息及相关状态。
了解了hbase:meta之后,客户端读写HBase的整个操作就非常简单了:
读操作比较简单,写操作的话HBase里面有一块内存区域叫Memstore,数据到达服务器后是“先”写到这块内存区域的,只有当内存区域满的时候,数据才会写到(flush)磁盘上面,落地为文件,这个文件称之为Hfile。当然这样就会有个问题,如果数据还未落地,服务器挂了,那内存里面的数据就丢了。这个不是HBase才有的问题,传统的很多数据库也有这个问题。解决方案也早已经比较成熟了,那就是预写日志(WAL, write ahead log),有时也叫commit log,也就是数据来了以后,先写日志,日志写成功之后才给客户端返回成功,这样就算宕机等内存数据丢了,依旧WAL是可以找回数据的。在HBase里面,该日志是存在HDFS上面的,所以可靠性是有保证的。等数据落盘之后,相关的预写日志也就会删掉了。
读写操作理解起来比较简单,这里需要注意三个点:
HBase里面的存储层次在其官方参考手册里面有这样一处说明:
这里再附一张HBase:The Definitive Guide上面的图:
结合这两个图,我们就看的比较清楚了:
总结一下:一个region里面有多个store,而一个store对应于一个Column Family,也就是说store、Column Family、MemStore三者是一对一的关系,Column Family落盘后就是多个Storefile,或者HFile。也就是上面region里面的一个方块就是一个Column Family。所以Column Family是HBase里面是非常重要的一个概念。
这里我们再重复强调一下Column Family,从上面的分析可以看出,Column Family的数据是存储在一起的,这也是为什么推荐将经常一起读写、大小相似的字段放在同一Column Family里面,我们能看到在HBase里面,Column(列)的概念被弱化了,取而代之的是Column Family,所以与其说HBase是面向列的数据库,不如说是面向Column Family的数据库。
上面的图都是逻辑图,那实际在HDFS上面是怎么存储的呢?HBase官方参考手册上面也有说明:
数据目录:
但是在我安装的HBase 1.2.6上面,目录结构并不是这样的,正确的应该是:
/hbase/data/\<Namespace>/\<Table>/\<ColumnFamily>/\<StoreFile>
以前面示例代码中的testname表为例,如果你已经删了,重新创建回来,插入数据。put完数据之后,记得执行一下flush "testname"
,让数据落地,不然数据可能存储在Memstore里面,你看不到落地文件:
➜ ~ hadoop fs -ls /hbase/data/default/tablename/e010bb04332e265603d26b62778ba0fb/colfam
Found 1 items
-rw-r--r-- 1 root supergroup 5033 2018-04-21 21:09 /hbase/data/default/tablename/e010bb04332e265603d26b62778ba0fb/colfam/41d86b3a845f4b2782cdd19b70d3c59a
HLog目录官方给的目录结构如下hbase-wal-log-path.png:
但实际也也不是这样,正确的应该是:
/hbase/WALs/\<RegionServer>/\<WAL>
➜ ~ hadoop fs -ls /hbase/WALs/ubuntu,16201,1524211032085
Found 2 items
-rw-r--r-- 1 root supergroup 83 2018-04-21 20:57 /hbase/WALs/ubuntu,16201,1524211032085/ubuntu%2C16201%2C1524211032085..meta.1524315453910.meta
-rw-r--r-- 1 root supergroup 83 2018-04-21 20:57 /hbase/WALs/ubuntu,16201,1524211032085/ubuntu%2C16201%2C1524211032085.default.1524315451627
HBase是开源的、分布式、多版本的NoSQL数据库,其设计理念来自于Google的"Bigtable: A Distributed Storage System for Structured Data"论文,有兴趣的可以看看Google的这篇论文。本文涉及到的大都是一些基础的操作和理论,如何才能比较好的使用HBase,还是有非常多的技巧的,这方面我也才处于积累阶段,后续我会写一些类似最佳实践或者结合具体场景的东西,有兴趣的可以关注一下。
References: