NYC's Blog - HBase 2018-04-22T21:31:00+08:00 Typecho http://niyanchun.com/feed/atom/tag/hbase/ <![CDATA[HBase架构介绍]]> http://niyanchun.com/hbase-introduction-extend.html 2018-04-22T21:31:00+08:00 2018-04-22T21:31:00+08:00 NYC https://niyanchun.com 本文是对《Hadoop系列四——HBase简介》一文的补充,不过本文不会进行系统性介绍,只是针对一个个独立的点介绍,并且会不断更新,有点类似于FAQ吧。

1. HBase的架构图

简易版架构图:

简易版架构图

复杂版架构图:

复杂版架构图

架构讲解见《Hadoop系列四——HBase简介》。

2. 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.

这里有几个关键词我已经加粗了,从这几个关键词可以引出来多个问题。

2.1 HBase里面的"NULL值"处理

第一个就是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的逻辑存储和物理存储”部分。

2.2 分布式

想想如果因为性能或存储等不够了,需要将RDBMS扩展成分布式的,能否做到?也许可以做到,但一般是受限的,而且难度比较高,比如像业内的GreenPlum,如果还想要保证RDBMS的所有特性,那就只能用更好的硬件做垂直扩展了。但HBase本身就是分布式的,扩展一个HBase节点基本上没有什么难度。所以这也是很多企业选择HBase的一个非常重要的考量。

2.3 多维有序map

多维的概念在我的《Hadoop系列四——HBase简介》里面已经讲过了。

有序之前也提过,在HBase里面,不论是内部存储,还是查询返回的数据,都是有序的:依次按照row key、Column Family、Column qualifier、Timestamp四个维度排序,时间戳是按照从新到旧排的,其它都是字典序。

2.4 HBase里面的数据是否有类型

上面对于value的描述是“uninterpreted array of bytes”,也就是没有类型,默认按字节流解析(我们知道任何类型其实都可以按字节流其解析)。其实在HBase里面除了会被作为路径的名称之外,其它都是作为字节流的,也就是没有类型。具体就是:表名、Column Family会作为路径名,所以名称必须是合法的路径名,也就是必须是可打印字符;row key,Column qualifier、Cell都是没有类型的,也就是可以是任何数据。实际中,我们经常会在row key、Column qualifier中存储数据,而不光是cell里面。

3. HBase的逻辑存储和物理存储

引用HBase:The Definitive Guide上的一幅图:

HBase存储图

第一幅图(左上角)是HBase里面一个典型的表的逻辑图,里面有cf1cf2两个Column Family(以下简称CF),每个CF里面有两列。里面的红色和黄色小块表示有数据,其他地方都没有数据(如前面所说,是一个稀疏矩阵),多个层叠的部分表示有多个版本的数据。

第二幅图(右上角)也是一个逻辑图,主要为了说明以下几个点:

  • 不同CF里面的数据是分开存储的;
  • 同一个CF里面的数据是按照row key顺序存储的;
  • 统一cell里面有多个版本的数据的时候,新版本数据在前,旧版本数据在后,这样方便先取到新版本数据。

第三幅图(右下角)是上面的逻辑存储在物理文件上的存储形式,需要注意以下几个点:

  • 不同CF的数据是存储在不同的文件里面的(Storefile或HFile),这就是为什么我们要将要一起使用的数据字段定义在同一个Column Family的原因;
  • 同一个文件里面还是按照row key、CF、Column qualifier排序的;
  • 每一条数据里面都要存储Key(row key、Column Family、Column qualifier、Timestamp)和value。在RDBMS里面我们设计字段名时一般要求能够“见名知意”,但在HBase里面不推荐这样做,Key的设计在保证功能的前提下,越短越好(比如仅用一个字母表示),至于其含义可以其它地方记录,比如文档里面。

第四幅图(左下角)是为了说明查询时指定各个Key对性能的影响:

  • 指定row key可以大幅度提高查询性能,因为根据row key可以确定在哪些region上面查(也就是说可以跳过那些不包含该row key的region)。在scan命令里面,可以通过STARTROWSTOPROW指定row key范围。
  • 指定Column Family可以大幅度提高查询性能,因为根据CF可以确定跳过哪些Storefile/HFile,一般查询时都建议指定CF。
  • 指定Timestamp也可以较大幅度提高查询性能,因为每个Storefile会存储它所保存的所有数据的时间区间,如果所指定的Timestamp不在该区间内,则直接跳过。
  • 指定Column QualifierValue的过滤条件可以提高查询性能,但提高的很少。因为必须把每个Cell的值读出来和指定的条件做对比。

4. Tall-Narrow or Flat-Wide表

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的数量”部分。

5. HBase的表和Column Family能不能修改

先说结论:可以修改。使用alter命令可以修改表和Column Family,具体语法可以help "alter"查看。这里需要注意两点:

  1. 0.92版本之前,表必须先disable后才可以修改。之后的版本增加了一个配置项hbase.online.schema.update.enable,如果设为true,那可以直接修改,不需要disable。但官方推荐生产环境最好还是先disable再修改,在线修改可能会引发一些问题。
  2. 修改动作并非立即生效,而是等待下一次major compaction,Storefile重写之后才会生效。

6. HBase的Compaction

我们知道当MemStore里面的数据量达到一定值的时候,就会落盘形成StoreFile(HFile),这样就会形成很多文件,而Compaction就是将这些文件合并成大文件。HBase里面有两种Compaction:Minor compactionsMajor compactions,主要有如下区别:

  • Minor compactions一次只选取少量存储在一起的文件做合并压缩,其结果就是一个store(或Column Family)的数据被合并成了多个大一些的Storefile,而Major compactions合并之后的结果是一个store(或Column Family)的数据全部到一个Storefile里面去了。
  • Minor compactions合并时不删除已经标记删除或者过期版本的数据,而Major compactions会删掉那些标记删除或过期版本的数据。所以需要注意,HBase里面的删除是标记删除,真正的物理删除发生在Major compactions阶段。

虽然Compaction合并文件是为了提高性能,但合并这个操作却是消耗资源的,就跟Jvm的GC一样。默认Major compactions一周一次。

7. Column Family的数量

先说结论:一个表内的Column Family最好1个,最多不要超过3个。原因主要有两点:

  1. 现在HBase的flush(MemStore满了之后就会flush)和Compaction操作都是基于region的,从前面的架构图中可以看到一个region里面是包含多个Column Family的,所以当region内的某个Column Family需要flush或Compaction的时候,和它处于同一region内的其它Column Family也会一起flush或Compaction,但它们可能只有少量新增数据,这就会浪费IO。
  2. 假设有两个Column Family:CF-A和CF-B,其中CF-A有一百万条数据,CF-B有一千万条数据。假设CF-A占了100个HFile,CF-B占了1000个HFile,因为数据写入的先后顺序,很可能CF-A的100个文件会被CF-B的1000个文件打散,本来可能一个RegionServer上面的region足够存储所有的CF-A的HFile了,现在可能被打散到多个RegionServer上面去了,这样查询CF-A的数据的时候效率就会降低。这种现象称为“Cardinality of ColumnFamilies”。

8. Value的版本数和TTL

版本数指我们之前说的Timestamp,和这个特性相关的设置有两个:max versionsmin 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值来关闭删除这个特性。

9. 选择HBase还是RDBMS

一般来说,这个选择是比较好做的。HBase、Impala等数据库的诞生并不是为了替代传统的RDBMS,只是为了解决新的RDBMS解决不了或者解决起来比较困难的问题。所以,如果你的数据量并不大(一般以百万为分界线),那一般应该优先选择RDBMS,毕竟RDBMS支持完善的ACID,SQL,多级索引,各种Join,完善的类型等丰富的特性,这些都是HBase所不具备的。

但如果你的数据量非常大,RDBMS已经无法支撑了,那就可以考虑HBase等分布式数据库了。但如果你的业务离不开RDBMS的一些特性(比如各种Join、SQL、完善的ACID等),那可能就需要考虑类似于GreenPlum这种MPP数据库了。

10. FAQ

  • HBase是否支持ACID?
HBase里面只支持受限的ACID(Atomicity, Consistency, Isolation, and Durability):仅支持行内的ACID,跨行不支持。也就是对于同一行的操作是可以保证ACID的,但是多行操作是不行的。更多信息可参考:ACID in HBase
  • HBase和Hive如何选择?
这两个不是一个层级的东西,没有可比性。如果你还在这二者之间纠结,那可能你对它们有些误会,或者还不清楚你自己的需求。非要比的话,那就是HBase一般做实时查询;而Hive一般作为离线数据仓库,Hive后面是MapReduce/Spark,所以无法做到实时。
  • HBase支不支持join?
先说结论:不支持。HBase读取数据时支持GetScanGet的后台实现是Scan的一种特殊情况而已)操作,RDBMS里面的join在HBase里面是不支持的,但我们可以在表设计上支持一定程度上的"join"操作,比如将需要join的字段拼接起来作为row key
  • HBase支持SQL吗?

    HBase不支持SQL,只提供了各种API。但Apache下有个Phoenix项目,通过该项目可以使用SQL语句操作HBase。
  • HBase的一个region多大合适?
注意,这里说的是region,不是RegionServer。一个region保持在10~50GB比较好。
  • HBase一个表包含多少个region比较好?
一般一个表包含50~100个region和1~2个Column Family比较好。
  • HBase的Cell里面存储的Value有大小限制吗?
没有,但一般不要超过10MB(对于MOB对象不要超过50MB)。如果超过了这个大小,可以将对象存到HDFS上面,然后再HBase里面存储HDFS路径。
  • HBase里面的daughter是什么?
HBase的region分裂的时候,分裂出来的两个新region称为"daughter",原来的称为"parent"。

11. Row key设计

Last but not the least,row key的设计是HBase表设计里面最重要的部分,没有之一。Row key设计不合理很容易会引起热点问题(hotspotting)——大量的客户端请求到达同一个节点(RegionServer)或少数一些节点,导致该节点负载急剧上升,性能下降,甚至不可用。这个问题容易出现的原因和HBase自身的设计有关系——row key是按照字典序排列的。当然这个设计是没有问题的,主要是为了快速访问。这里介绍一些常用的避免热点问题的方法。

11.1 Salt

第一种方式就是加盐值,这里的盐值不同于密码学里面的盐值。实质就是事先定几个随机的前缀,然后给每一个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
...

所以这种方式提高了写的吐吞量,但读的时候就会比较麻烦。

11.2 Hash

Hash其实是对Salt的一种优化。在Salt里面,前缀的选择是随机的,这样相同的row key可能加了不同的前缀。而Hash就是在row key和前缀之间建立一种哈希,使得相同的row key总是加相同的前缀,其它和Salt相同。

11.3 Key逆序

这种应该是现在使用比较普遍的一种解决row key热点的技术,对于固定长度(比如卡号、时间戳等)或者固定结构(比如域名)等非常好用。但其缺点就是牺牲了原来key的顺序。

Row key的设计和读写模式、业务、数据都有很大关系,所以不可一概而论,介绍的几种方法没有哪个是普适的,也没有哪个是完美的,具体问题还需具体分析。上面介绍的都是一些理论,关于row key的设计实战的话,这里强力推荐一篇文章:Introduction to hbase Schema Design by AMANDEEP KHURANA)。篇幅稍微有点多,有兴趣的可以看一下原文,这里列一下作者提到的设计表的时候要考虑的一些比较重要的点:

  1. What should the row key structure be and what should it contain?
  2. How many column families should the table have?
  3. What data goes into what column family?
  4. How many columns are in each column family?
  5. 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 .
  6. What information should go into the cells?
  7. How many versions should be stored for each cell?

设计的时候需要将HBase一些重要的特性考虑到:

  1. Indexing is only done based on the Key.
  2. 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.
  3. Everything in HBase tables is stored as a byte[ ]. There are no types.
  4. Atomicity is guaranteed only at a row level. There is no atomicity guarantee across rows, which means that there are no multi-row transactions.
  5. Column families have to be defined up front at table creation time.
  6. 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

  1. Apache HBase Reference Guide(Version3.0.0-SNAPSHOT).
  2. HBase:The Definitive Guide.
]]>
<![CDATA[HBase安装使用]]> http://niyanchun.com/hbase-introduction.html 2018-04-15T21:45:00+08:00 2018-04-15T21:45:00+08:00 NYC https://niyanchun.com 1. 概述

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.

概述就到此为止,细节内容我们后面介绍。

2. 安装

任何理论介绍都不如先实际使用一下来的实在,所以我们先介绍如何安装HBase。安装过程和之前的《Hadoop系列一——安装部署》类似,非常简单,步骤如下(安装环境为Linux,发行版选择自己熟悉的就行):

  1. 安装Java环境,推荐JDK 1.8+。
  2. 去官网https://hbase.apache.org下载最新稳定版HBase安装包(我安装的是1.2.6,后面也以此版本为例说明),注意要下载二进制的包,而不是源码包。二进制包名字里面带bin关键字,比如hbase-1.2.6-bin.tar.gz,源码包类似hbase-1.2.6-src.tar.gz这样。
  3. 解压下载的安装包到某个目录,假设解压后目录为/opt/hbase-1.2.6目录。为了使用方便,可以将/opt/hbase-1.2.6/bin目录加到PATH环境变量里面。

安装就完成了,理论上不需要进行任何其他配置就可以使用了。

在bin目录下有两个脚本,用于启停HBase:

  • start-hbase.sh:启动HBase;
  • stop-hbase.sh:停止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-env.sh:这个里面主要有两个点比较重要:(1)JAVA_HOME:可以在这里指定你的java,特别是你使用非系统默认的java或者系统没有设置JAVA_HOME的时候该选项就非常重要了。(2)HBASE\_MANAGES\_ZK:HBase依赖于ZooKeeper,这个选项表示是否由HBase自己管理ZK,默认为True,表示HBase自己管理。换句话说,如果你单独部署了ZooKeeper,想让HBase使用你部署的这个,那就把该选项置为False,并在下面要介绍的配置文件里面配上你的ZK信息;否则的话置为True就行了,HBase自己内部会创建ZooKeeper供自己使用。
  • 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可以查看集群的信息:

  • Master:"http://<ip:16010>"
  • RegionServer: "http://<ip:16301>"

至此,HBase的安装就介绍完了。

3. 数据模型

HBase虽然也称之为数据库,但它和我们所熟知的关系型数据库还是差异非常大的,甚至可以说没有什么相似的。另外,我们知道关系型数据库(RDBMS)一般都是行存储,而HBase和很多其他面向大数据的数据库一般都是列式存储,这两种存储的核心区别就在于它们对数据的物理存储方式不同,这里用HBase:The Definitive Guide(HBase权威指南)上面的一幅图来说明:

行式存储与列式存储

而这两种不同的存储方式也决定了它们许多截然不同的特性,有兴趣的可以看下这篇文章:行式数据库与列式数据库的对比

下面我们介绍HBase里面的数据模型,虽然HBase与关系型数据库有着很大的差异,但为了方便理解,我们会做一些类比,方便读者理解。

3.1 基本概念

3.1.1 Namespace

Namespace从逻辑上将表分组,类似于RDBMS里面的数据库实例。HBase的Namespace主要是为了实现多租户的特性,可以从Namespace上面做一些资源分配、资源隔离、安全管理等功能。HBase有两个内置的Namespace:

  • hbase:系统Namespace,包含HBase内置的一些表;
  • default:默认Namespace,创建表时没有显式指定Namespace的表都在该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上实质就是一个目录。

3.1.2 Table & Row & Column Family & Column Qualifier & Cell & Timestamp

  1. Table:和RDBMS里面表的概念类似,表名到后台实质是一个目录,所以表名必须是合法的目录名字符串。
  2. Row:和RDBMS里面的行类似,但HBase里面每一行都要有一个唯一的row key标识,类似于RDBMS里面的主键。row key有两大特点:(1)没有类型,一般认为是字节流(byte[]),也就是说任何字节流可以表示的对象(比如二进制文件)都可以作为row key。(2)row key是按照字典序(lexicographically)排序的。row key设计是HBase表设计里面的最关键的一个点,后面介绍常见设计方式。
  3. Column Family:一般译为“列簇”,是HBase里面特有的一个非常重要的概念。若干个列组成一个列簇,比如有一个列簇叫courses,它里面包含mathhistory两个列,在HBase里面这个关系写为:courses:mathcourses:history,冒号后面的部分称之为Column qualifier。二者有如下一些特性:

    • Column Family后台实质是目录名,所以名称必须是合法的路径名;而Column qualifier则可以是任意字节流。
    • Column Family必须在表定义的时候就指定,而Column qualifier在插入数据的时候指定即可,甚至也可以不指定,比如courses:也是合法的。
    • 很多调优和存储属性设置都是基于Column Family的。
    • 物理上,Column Family里面的数据是存储在一起的,所以一般而言,同一个Column Family的数据列应该具有相同的访问特点,以及存储大小也应该相近。Column Family的使用对于HBase也至关重要,见后文。
  4. Cell:cell就是存储数据的地方,对应到RDBMS里面就是某行的某个字段,在HBase里面三元组{row key,Column Family,Column qualifier}唯一指定了一个cell。Cell里面存储的数据也是没有类型的,任何字节流可以表示的对象都可以。
  5. Versions:Cell里面存储的数据是有版本的,默认使用时间戳,保留三个版本。如果写入数据的时候没有指定版本,就会用当前时间作为版本号。读取数据时,如果没有指定版本号,默认获取最新版本的数据。

以上就是HBase数据模型里面的一些关键概念,这里看一张图(该图来自Amandeep khurana的Introduction to hbase Schema Design):

hbase Schema Design

这里展示了HBase中的一张表以及里面的一些数据。可以看到,该表的row key是“0001”、“0002”、...,按字典序排的;表里面有两个Column FamilyPersonalOfficePersonal里面包含两个列或者叫Column qualifierNameResidence phoneOffice里面包含两个列:PhoneAddress。每一个Cell里面是具体的数据,并且可能有多个版本。再附一张**上面的图:

hbase_table_design.png

我们可以将HBase的表看成是一个多维map(multidimensional map),比如上面的表其实就是下面一个多维map:

multidimensional map

当然,也可以将HBase的表看成是一个KeyValue存储系统,比如:

Key Value Store

上面列了5种情况,我们可以将row key当做key,也可以和其他属性一起作为key。

3.2 基本操作

了解了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

下面我们简单介绍下上面的操作:

  1. 首先我们创建了一个表:tablename。创建表的时候必须指定表名和Column Family,如果未指定Namespace,默认在default这个Namespace下面。使用desc可以查看表信息。
  2. 往表里面插数据使用put命令,插数据的时候必须指定表名、row key、Column Family、数据。Column qualifier可选择性指定。如果row key相同,则HBase认为是Update操作,而不是插入新数据。
  3. 使用scan命令可以查看表里面所有的数据,使用get命令可以查看某条数据。
  4. 删除表使用drop命令,只有处于disabled状态的表才可以删除。

使用help "<command>"命令可以查看某个命令的详细使用方法,里面一般都有大量的例子,这里就不赘述了。

4. 架构简析

HBase的架构细说起来还是比较复杂的,有兴趣的可以去看下官方的参考手册,这里只对一些个人认为比较关键的点进行说明。

4.1 主从关系

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。具体见下节。

4.2 读写操作

4.2.1 hbase:meta

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的信息及相关状态。

4.2.2 读写过程

了解了hbase:meta之后,客户端读写HBase的整个操作就非常简单了:

  1. 连接ZooKeeper,获取hbase:meta的地址;
  2. hbase:meta中获取需要读的数据分布在哪些region(RegionServer)上的信息;
  3. 直接与RegionServer交互,进行读写。

读操作比较简单,写操作的话HBase里面有一块内存区域叫Memstore,数据到达服务器后是“先”写到这块内存区域的,只有当内存区域满的时候,数据才会写到(flush)磁盘上面,落地为文件,这个文件称之为Hfile。当然这样就会有个问题,如果数据还未落地,服务器挂了,那内存里面的数据就丢了。这个不是HBase才有的问题,传统的很多数据库也有这个问题。解决方案也早已经比较成熟了,那就是预写日志(WAL, write ahead log),有时也叫commit log,也就是数据来了以后,先写日志,日志写成功之后才给客户端返回成功,这样就算宕机等内存数据丢了,依旧WAL是可以找回数据的。在HBase里面,该日志是存在HDFS上面的,所以可靠性是有保证的。等数据落盘之后,相关的预写日志也就会删掉了。

读写操作理解起来比较简单,这里需要注意三个点:

  • 客户端在第2步会缓存hbase:meta里面的信息,这样可以避免频繁读取hbase:meta,当遇到错误等一些情况时,才会重新读取hbase:meta
  • 读操作会优先在Memstore里面读,如果读到了,就不会去磁盘上扫描了。
  • 客户端的整个交互过程是没有涉及到Master的,这样在客户端请求比较多的情况下,Master不会成为系统的瓶颈,这是和很多其他主从式分布式系统的一个区别。

4.3 存储

HBase里面的存储层次在其官方参考手册里面有这样一处说明:

hbase storage

这里再附一张HBase:The Definitive Guide上面的图:

hbase storage architecture

结合这两个图,我们就看的比较清楚了:

  • 每个表包含一到多个region(图中的HRegion);
  • 每个RegionServer上面有多个region;
  • 每个region里面包含一到多个Store,每个Store对应一个Column Family,所以一般说到Store,就认为是Column Family即可;
  • 每个Store对应一个Memstore,Memstore里面的数据写文件后就是StoreFile,而Storefile底层就是HFile,很多时候我们不区分Storefile和HFile;
  • 最终不论是HLog(WAL或commit log)还是HFile,都会存储在HDFS上面。

总结一下:一个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 data path on HDFS

但是在我安装的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 data path on HDFS

但实际也也不是这样,正确的应该是:

/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

  1. Apache HBase Reference Guide(Version3.0.0-SNAPSHOT).
  2. HBase:The Definitive Guide.
  3. Hadoop The Definitive Guide(4th Edition).
]]>