大数据存储基石-HBase2.4.4全方位解析|8月更文挑战

·  阅读 390

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

大数据存储基石-HBase

HBase全解析

=== 楼兰 ===
只做最纯粹的学习

@[toc]

一、关于HBase

1、HBase是什么?

​ HBase的官网地址: hbase.apache.org/ 标志是一个漂亮的虎鲸

在这里插入图片描述

​ 官网上对HBase的介绍非常的简单直接:

Apache HBase™ is the Hadoop database, a distributed, scalable, big data store.
复制代码

​ Apache HBase是一个Hadoop上的数据库。一个分布式,可扩展的大数据存储引擎。HBase有个最明显的特征:

1、HBase支持非常大的数据集,数十亿行*数百万列。如此庞大的数据量级,足以撑爆我们在J2EE阶段学习过的所有数据存储引擎。 2、HBase支持大数据量的随机、实时读写操作。在海量数据中,可以实现毫秒级的数据读写。 3、HBase从一开始就深度集成了Hadoop。HBase基于Hadoop进行文件持久化,还继承了Hadoop带来的强大的可扩展性。Hadoop可以基于廉价PC机组建庞大的应用集群。HBase也深度集成了Hadoop的MapReduce计算框架,并且也正在积极整合Spark。这使得HBase能够很轻松的融入到整个大数据生态圈。 4、HBase的数据是强一致性的,从CAP理论来看,HBase是属于CP的。这种设计可以让程序员不需要担心脏读、幻读这些事务最终一致性带来的问题。 5、最后最重要的还是HBase的框架性能是足够高效的。HBase的开源社区非常活跃,他的性能经过很多大型商业产品的验证。Facebook的整个消息流转的基础设施就构建于HBase之上。

​ 可以说正是Hadoop+HBase一起提供的强悍无敌的性能,支撑起了大数据的整个技术体系。这就好比必须要先有操作系统,能够通过文件、内存来存储资料,后面开发出来的各种应用程序才有用武之地。

​ 聊到了Hadoop+HBase,就不得不说Google发布的三篇论文。长久以来,各大互联网公司都在注重构建服务,而对于业务数据,很难达到海量这个级别。而对于海量数据的处理,还只是Google这样的搜索引擎巨头在进行深入的研究。

​ 2003年,Google发布Google File System论文,(GFS)这是一个可扩展的分布式文件系统,用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上,提供容错功能。从根本上说:文件被分割成很多块,使用冗余的方式储存于商用机器集群上。

​ 紧随其后,2004年,Google公布了MapReduce论文,论文描述了大数据的分布式计算方式,主要思想是将任务分解然后在多台处理能力较弱的计算节点中同时处理,然后将结果合并从而完成大数据处理。

​ 接着到了2006年,Google公布了BigTable论文,BigTable是一种构建于GFS和MapReduce之上的多维稀疏图管理工具。

​ 正是这三篇论文,掀起了开源软件的大数据热潮。人们根据GFS,开发出了HDFS文件存储。MapReduce计算框架,也成了海量数据处理的标准。而HDFS与MapReduce结合在一起,形成了Hadoop。而BigTable更是启发了无数的NoSQL数据库。而HBase正是继承了正统的BigTable思想。所以,Hadoop+HBase是模拟了Google处理海量网页的三大基石实现的,他们也就成了开源大数据处理的基石。

2、HBase的数据结构

​ HBase也可以作为一个数据库使用,但是为了应对海量数据,他存储数据的方式与我们理解的传统关系型数据库有很大的区别。虽然他也有表、列这样的逻辑结构,但是整体上,他是以一种k-v键值对的方式来存储数据的。

在这里插入图片描述

​ 纵向来看,HBase中的每张表由Rowkey和若干个列族或者称为列簇组成。其中Rowkey是每一行数据的唯一标识,在对数据进行管理时,必须自行保证Rowkey的唯一性。接下来HBase依然会以不同的列来管理数据,但是这些列分别归属于不同的列簇。在HBase中,同一张表的数据,只需要保证列簇是相同的,而列簇下的列,可以是不相同的。所以由此可以扩展出非常多的列。在HBase中,对于同一张表,不建议定义过多的列簇,通常不要超过三个。而更多的数据,可以以列的方式来扩展。

​ 从横向来看,HBase中的记录,会划分为一个一个的Region,存储在不同的RegionServer上。并且会在不同的RegionServer之前形成备份,以Region为单位提供了故障后自动恢复的机制。

​ 最后,从整体来看,HBase虽然还是以HDFS作为文件存储,但是他存储的数据不再是简单的文本文件,而是经过HBase优化压缩过的二进制文件,所以他的存储文件通常是不能够直接查看的。

3、HBase的基础架构

​ HBase整体的基础架构如下图:

在这里插入图片描述

​ 其中,

  • client客户端包含了访问HBase的接口,另外也维护了对应的缓存来加速对HBase的访问。
  • RegionServer直接对接用户的读写请求,是真正干活的节点。他会将数据以StoreFile的形式存储到不同的HDFS目录中。
  • HMaster主要是维护一些集群的元数据信息,同时监控RegionServer的服务状态,并且通过Zookeeper提供集群服务,向客户端暴露集群的服务端信息。

4、HBase适用场景

​ 还记得我们之前讲过的Hive框架吗?这里不妨拿Hive来比较一下,有助于我们理解HBase的适用场景。

​ Hive提供了基于SQL的对海量数据进行查询统计的功能,但是Hive不存储数据,所有数据操作都是对HDFS上的文件进行操作,所以他对数据的查询操作能做的优化比较有限。同时Hive也无法直接管理数据,对数据的管理依赖于MapReduce,所以延迟非常高。所以Hive通常只适用于一些OLAP的场景,并且通常是与其他组件结合一起进行使用。

​ 而HBase与Hive的区别就非常明显。HBase基于HDFS来存储数据,但是他存储的数据都是经过自己优化索引后的数据,所以他对数据的存储是非常高效的,比HDFS直接存储文件的性能要高很多,可以作为整个大数据的存储基石。而HBase以类似于Redis的列式存储来管理数据,对数据的增删改都会非常高效,可以达到毫秒级响应。同时,也提供了完善的客户端API,所以他完全可以作为传统意义上的数据库使用,适用于大部分的OLTP的场景。但是他的缺点也比较明显,基于列式存储的数据,天生就不太适合大规模的数据统计,所以在很多OLAP的场景,需要结合其他一些组件如spark、hive等,来提供大规模数据统计的功能。

​ 在后面的实战部分,我们不光要理解HBase这个产品,更要理解他与其他大数据组件如何合作。

二、HBase安装

1、实验环境与前置软件

​ 实验环境:准备了三台服务器,预装CentOS7,分别分配机器名 hadoop01,hadoop02,hadoop03。IP地址不重要,因为集群都会通过机器名来配置。三台服务器需要配置时间同步,这个很重要。在后面的课程中预备在hadoop01机器上安装HBase的HMaster,所以需要配置hadoop01到三台机器的免密登录。

免密登录的配置方式: 在hadoop01上生成秘钥: ssh-keygen。然后将密钥分发到不同的机器 ssh-copy-id hadoop02。这样在hadoop01机器上,使用ssh hadoop02就不需要再输入密码了。

​ 前置必须安装的软件:JDK8;Hadoop采用3.2.2版本;Spark采用3.1.1版本。

​ 可选的软件:hadoop01上安装mysql服务,三台机器上安装spark集群,采用3.1.1版本。在hadoop02机器上安装hive3.1.2版本,并部署hive服务hiveserver2。(这些软件不是必须的,但是我们在学习HBase时会用到)

2、安装Zookeeper

​ HBase需要依赖Zookeeper来进行集群内的数据协调。虽然HBase的发布包中内置了Zookeeper,但是在搭建集群时,通常会另外搭建一个Zookeeper集群,减少组件之间的直接依赖。

​ 到Zookeeper官网下载zookeeper3.5.8版本(zookeeper版本不能低于3.4.x)。apache-zookeeper-3.5.8-bin.tar.gz。上传到服务器后,解压到/app/zookeeper目录。然后进入zookeeper的conf目录。

mv zoo_sample.cfg zoo.cfg
vi zoo.cfg
修改zookeeper的数据目录
dataDir=/app/zookeeper/data
增加集群节点配置
server.1=hadoop01:2888:3888
server.2=hadoop02:2888:3888
server.3=hadoop03:2888:3888
复制代码

​ 修改完成后,将zookeeper目录整体分发到集群中的三台服务器上。然后需要在zookeeper的数据目录/app/zookeeper/data中增加一个myid文件。文件的内容就是当前服务器在zookeeper中的节点ID。例如Hadoop01机器的myid是1,Hadoop02机器的myid是2,Hadoop03机器的myid是3。

​ 配置完成后,启动zookeeper集群: bin/zkServer.sh start。 启动完成后,使用jsp指令能看到一个QuorumPeerMain进程,表示集群启动成功。

2、安装HBase

​ 可以到 hbase.apache.org/downloads.h… 页面下载对应的HBase。选择HBase版本时,需要注意的是要与Hadoop版本兼容。这是目前官网上贴出的hadoop版本兼容表格:

在这里插入图片描述

​ 我们这次就选择目前最新的HBase2.4.4版本。(其实要注意,在工作中通常都不建议使用最新的版本)

3、搭建HBase集群模式

​ 下载HBase部署包: hbase-2.4.4-bin.tar.gz,上传到服务器hadoop01。

1 解压后移动到/app/hbase目录。

tar -zxvf hbase-2.4.4-bin.tar.gz
mv hbase-2.4.4 /app/hbase
复制代码

​ 进入hbase的conf配置目录,修改配置文件。

2 先修改hbase-env.sh。在文件最后加入如下配置:

export JAVA_HOME=/app/java/jdk1.8.0_212
#是否使用HBase内置的ZK。默认是true。
export HBASE_MANAGES_ZK=false
复制代码

3 修改hbase-site.xml,加入如下配置

<configuration>
	<property> 
		<name>hbase.rootdir</name> 
		<value>hdfs://hadoop01:8020/hbase</value> 
	</property>
	<property> 
		<name>hbase.cluster.distributed</name>
		<value>true</value>
	</property>
	<property>
		<name>hbase.master.port</name>
		<value>16000</value>
	</property>
	<property>
		<name>hbase.zookeeper.quorum</name>
		<value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value>
	</property>
	<property> 
		<name>hbase.zookeeper.property.dataDir</name>
		<value>/app/zookeeper/data</value>
	</property>
</configuration>
复制代码

​ 这里配置了一个hbase在hdfs上的根目录。这个目录不需要手动创建,并且最好保证这个目录在HDFS上不存在。

4 配置regionservers文件,在文件里列出集群中所有的节点。

hadoop01
hadoop02
hadoop03
复制代码

5 同步Hadoop的配置文件。为了保证文件内容一致,采用软连接的方式。

ln -s /app/hadoop/hadoop-3.2.2/etc/hadoop/core-site.xml /app/hbase/hbase-2.4.4/conf/
ln -s /app/hadoop/hadoop-3.2.2/etc/hadoop/hdfs-site.xml /app/hbase/hbase-2.4.4/conf/
复制代码

6 将hbase整体分发到其他服务器

scp -r /app/hbase/ root@hadoop02:/app
scp -r /app/hbase/ root@hadoop03:/app
复制代码

7、配置Hbase环境变量。将HBase安装目录配置到环境变量中,并将bin目录配置到PATH环境变量

vi ~/.bash_profile
增加 export HBASE_HOME=/app/hbase/hbase-2.4.4
并在PATH变量中增加 $HBASE_HOME:BIN

复制代码

8 启动hbase

bin/start-hbase.sh

复制代码

​ 启动完成后,使用jps指令能够查看到在当前机器上启动了HMaster进程。三台机器上都启动了HRegionServer进程。

​ 然后可以访问HBase的管理页面: http://hadoop01:16010 查看集群的状态。

在这里插入图片描述

注意:

1、三台服务必须要进行时钟同步,否则启动hbase时会报错。抛出ClockOutOfSyncException异常。 2、在官方文档中提到, HBase是基于Hadoop的,所以HBase内置了Hadoop的jar包。但是jar包的版本要跟实际部署的hadoop一致。例如我们使用的HBase2.4.4版本,内置的Hadoop的jar包版本都是2.10.0的。在实际部署时,最好使用hadoop集群中的jar包。但是我们在学习过程中,这个jar包版本不一致并没有带来什么问题,所以就没有去替换了。但是实际部署时要注意。 3、部署完成后,可以在HDFS上看到HBase的元数据。/hbase

三、HBase基础操作

​ 关于HBase的使用,在官网上有一个很重要的文档,下载地址 hbase.apache.org/apache_hbas… 这是学习HBase最好的资料。不过这个资料是全英文的,并且内容非常非常多,需要有一定的理解能力。

1> 基础指令

1、使用HBase的客户端:

#查看HBase基础指令
[root@192-168-65-174 hbase-2.4.4]# bin/hbase --help 
#hbase命令行
[root@192-168-65-174 hbase-2.4.4]# bin/hbase shell
#查看帮助
hbase:001:0> help
#列出已有的表
hbase:002:0> list

复制代码

2、基础的表操作

#创建表 user表,有一个列簇 basicinfo
hbase:003:0> create 'user','basicinfo'
#插入一条数据
hbase:004:0> put 'user','1001','basicinfo:name','roy'    
hbase:005:0> put 'user','1001','basicinfo:age',18    
hbase:006:0> put 'user','1001','basicinfo:salary',10000
#插入第二条数据
hbase:007:0> put 'user','1002','basicinfo:name','sophia'         
hbase:008:0> put 'user','1002','basicinfo:sex','female'
hbase:009:0> put 'user','1002','basicinfo:job','manager'
#插入第三条数据
hbase:005:0> put 'user','1003','basicinfo:name','yula'
hbase:006:0> put 'user','1003','basicinfo:school','phz school'

复制代码

3、数据操作

#按照RowKey,查找单条记录
hbase:007:0> get 'user','1001'
hbase:011:0> get 'user','1001','basicinfo:name'
#按照版本查询数据,每次put都会给这条数据的VERSIONS+1
hbase:031:0> get 'user','1001',{COLUMN => 'basicinfo:name',VERSIONS=>3}
#使用scan,查询多条记录
hbase:008:0> scan 'user'
hbase:009:0> scan 'user',{STARTROW => '1001',STOPROW => '1002'}

复制代码

1、HBase查询数据只能依据Rowkey来进行查询,而Rowkey是由客户端直接指定的,所以在使用HBase时, Rowkey如何设计非常重要,要带上重要的业务信息。 2、scan指令后面的查询条件,STARTROW和STOPROW是必须大写的。查询的结果是左开右闭的。

3、其他查询数据的操作可以使用help 'get' 或者 help 'scan',来查看更多的查询方式。例如对数据进行过滤

#查看表结构  这个结果很重要。列出了列簇的所有属性。
hbase:012:0> desc 'user'
Table user is ENABLED  
user 
COLUMN FAMILIES DESCRIPTION     
{NAME => 'basicinfo', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSIONS => '1', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRESSION => 'NONE', TTL => 'FOREVER', 
MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'}  
#查询表中的记录数。
hbase:013:0> count 'user'
#修改表结构
hbase:019:0> alter 'user',{NAME => 'basicinfo',VERSIONS => 3}
hbase:020:0> desc 'user'  
COLUMN FAMILIES DESCRIPTION  
{NAME => 'basicinfo', BLOOMFILTER => 'ROW', IN_MEMORY => 'false', VERSIONS => '3', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', COMPRESSION => 'NONE', TTL => 'FOREVER', 
MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'} 

#删除某一列
delete 'user','1002','basicinfo:sex'
#删除某一条数据
deleteall 'user','1003'
#清空表数据
truncate 'user'
#删除表 删除之前需要先disable。
disable 'user'
drop 'user'

复制代码

在desc指令中能够看到Hbase的表有很多重要的属性,这些属性都对应HBase底层维护数据的一些方式。例如COMPRESSION 属性指定了HBase的数据压缩格式,默认是NONE,另外还可以修改为JDK内置的GZ或者Hadoop集成的LZ4,另外还可以配置其他的属性。具体可以参看官方文档中的Appendix D部分。

2> HBase的数据结构

​ 从上面的一系列实验,我们可以理解HBase的基础数据结构

RowKey:

​ Rowkey是用来检索记录的唯一主键,类似于Redis中的key。访问HBase中的表数据,只能通过Rowkey来查找。访问HBase的数据只有三种方式:

​ 1、通过get指令访问单个rowkey对应的数据。 ​ 2、通过scan指令,默认全表扫描 ​ 3、通过scan指令,指定rowkey的范围,进行范围查找。

​ Rowkey可以是任意字符串,最大长度是64KB,实际中通常用不到这么长。在HBase内部,Rowkey保存为字节数组。存储时,会按照Rowkey的字典顺序排序存储。

​ 在实际使用时,对Rowkey的设计是很重要的,往往需要将一些经常读取的重要列都包含到Rowkey中。并且要充分考虑到排序存储这个特性,让一些相关的数据尽量放到一起。比如我们创建一个用户表,经常会按用户ID来查询, 那Rowkey中一定要包含用户ID字段。而如果把用户ID放在Rowkey的开头,那数据就会按照用户ID排序存储,查询Rowkey时效率就会比较快。

Column Family(列簇) 与 Column(列)

​ HBase中的列都是归属于某一个列簇的,HBase在表定义中只有对列簇的定义,没有对列的定义。也就是说,列是可以在列簇下随意扩展的。要访问列,也必须以列簇作为前缀,使用冒号进行连接。列中的数据是没有类型的, 全部都是以字节码形式存储。同一个表中,列簇不宜定义过多。

​ 物理上,一个列簇下的所有列都是存储在一起的。由于HBase对于数据的索引和存储都是在列簇级别进行区分的,所以,通常在使用时,建议一个列簇下的所有列都有大致相同的数据结构和数据大小,这样可以提高HBase管理数据的效率。

Versions:

​ 在HBase中,是以一个{row,column,version}这样的数据唯一确定一个存储单元,称为cell。在HBase中,可能存在很多cell有相同的row和column,但是却有不同的版本。多次使用put指令,指定相同的row和column,就会产生同一个数据的多个版本。

​ 默认情况下,HBase是在数据写入时以时间戳作为默认的版本,也就是用scan指令查找数据时看到的timestamp内容。HBase就是以这个时间戳降序排列来存储数据的,所以,HBase去读取数据时,默认就会返回最近写入的数据。客户端也可以指定写入数据的版本,并且这个版本并不要求严格递增。

​ 当一个数据有多个版本时,HBase会保证只有最后一个版本的cell数据是可以查询的,而至于其他的版本,会由HBase提供版本回收机制,在某个时间进行删除。

例如:以下指令可以指定要存储多少个版本

#给basicinfo声明最多保存5个版本
alter 'user',NAME => 'basicinfo',VERSIONS=>5
#指定最少两个版本
alter 'user',NAME => 'basicinfo',MIN_VERSIONS => 2 
#查询多个版本的数据。
get 'user','1001',{COLUMN => 'basicinfo:name',VERSIONS => 3}
#查询10个历史版本
scan 'user',{RAW => true, VERSIONS => 10}

复制代码

put与delete时,也可以指定版本。具体可以使用help指令查看。

另外,在使用scan查询批量数据时,Hbase会返回一个已经排好序的结果。按照 列=>列簇=>时间戳 的顺序进行排序。也可以在scan时,指定逆序返回。

Namespace 命名空间

​ 在创建表的时候,还可以指定表所属的命名空间。例如

create_namespace 'my_ns'
create 'my_ns:my_table','fam'
alter_namespace 'my_ns',{METHOD => 'set','PROPERTY_NAME' => 'PROPERTY_VALUE'}

list_namespace

drop_namespace 'my_ns'


复制代码

​ 在HBase中,每个命名空间会对应HDFS上的/hbase/data目录下的一个文件夹。不同命名空间的表存储是隔离的。

​ 在HDFS上可以看到, HBase默认创建了两个命名空间,一个是hbase,这是系统的命名空间,用来存放HBase的一些内部表。另一个是default,这个是默认的命名空间。不指定命名空间的表都会创建在这个命名空间下。

四、HBase原理

1、HBase文件读写框架

​ 对HBase有了一定的了解了之后,再回头来看一下之前介绍过的HBase的架构图,对HBase的整体结构就可以有更深入的理解。

在这里插入图片描述

1、StoreFile

​ 实际保存数据的物理文件,StoreFile以HFile的形式存储在HDFS上,每个Store会有一个或多个StoreFile,数据在每个StoreFile内都是有序的。在HDFS /hbase/data/default/user目录下。

2、MemStore

​ 写缓存。由于HFile中的数据要求是有序的,所以数据是先存储在MemStore中,排好序后,等到达刷写时机才会写入到HFile。每次刷写都会形成一个新的HFile。

3、WAL

​ 由于数据要经过MemStore排序后才能刷写到HFile,但是数据在内存中会有很高的概率丢失。为了解决这个问题,数据会先写在一个叫做Write-Ahead-logfile的文件中,然后再写入MemStore中。当系统出现故障时,就可以从这个日志文件进行重建。

2、HBase写数据流程

HBase写数据的流程如下图:

在这里插入图片描述

1、client向HReginoServer发送写请求 2、HRegionServer将数据写到WAL. 3、HRegionServer将数据写到内存MemStore 4、反馈Client写成功

图中meta表信息为hbase默认维护的一个表。 可以用scan 'hbase:meta'指令查看。他维护了系统中所有的Region信息。这个表的HDFS路径会维护在Zookeeper中。这个表里的info:server字段就保存了Region所在的机器以及端口。

客户端要写入的数据,实际上写入到MenStore就算完成了,HBase会在后续的过程中定期将MemStore内的数据写入到StoreFile中。在客户端可以通过flush指令手动触发这一过程。

HBase刷写MemStore的几个时机:

  • MemStore级别限制

    habse.hregion.memstore.flush.size 默认128M。 当Region中任意一个MemStore的大小达到这个上限,就会触发一次MemStore操作,生成一个HFile。

  • Region级别限制:

    hbase.hregion.memstore.block.multiplier 默认4. 当Region中所有MemStore的大小总和达到上限,也会触发MemStore的刷新。这个上限就是 hbase.hregion.memstore.flush.size * habse.hregion.memstore.flush.size

  • RegionServer级别限制

    对整个RegionServer里写入的所有MemStore数据大小,配置了一个低水位阈值和高水位阈值。 当所有MemStore文件大小达到低水位阈值时,会开始强制执行flush。并按照MemStore文件从大到小一次刷写。

    而当所有MenStore文件大小达到高水位时,就会阻塞所有的写入操作,强制执行flush。直到总MenStore大小下降到低水位阈值。

    涉及到两个参数:

    hbase.regionserver.global.memstore.size 表示RegionServer的高水位阈值。默认配置None。分配JVM的Heap堆内存大小的40%(0.4)。 老版本的参数是hbase.regionserver.global.memstore.upperLimit

    hbase.regionserver.global.memstore.size.lower.limit 表示RegionServer的低水位占据高水位阈值的百分比。 默认配置也是None,表示是高水位阈值的95%(0.95)。老版本的参数是hbase.regionserver.global.memstore.lowerLimit

  • WAL级别限制

    hbase.regionserver.maxlogs WAL数量上限。当RegionServer的WAL文件数量达到这个上下后,系统就会选取最早的Hlog对应的一个或多个Region进行Flush。这时候会在日志中有一条报警信息 Too many WALs。count=....

  • 定期刷新MemStore

    hbase.regionserver.optionalcacheflushinterval 默认是3600000 单位是毫秒,即1个小时。这是HBase定期刷新MemStore的时间间隔。通常在生产中,为了尽量保证业务性能会将这个参数配置为0,表示关闭定时自动刷写。

  • 手动调用flush执行

在HBase的刷写机制下,只有RegionServer达到高水位阈值时才会阻塞写入操作,对业务产生直接影响。其他的几个限制级别都不会产生阻塞,但是通常还是会对性能产生一定的影响。所以在很多生产系统中,会根据业务的进展情况定制MemStore文件刷写策略。比如在业务不繁忙的时候进行定期手动刷写。

3、HBase读数据流程

HBase读数据的流程大致如下:

在这里插入图片描述

1、Client先访问zookeeper,从meta表读取region的位置,然后读取meta表中的数据。meta中存储了用户表的region信息。 2、根据namespace、表名和rowkey在meta表中找到对应的region信息。 3、找到这个Region对应的Regionserver 4、读取数据时,会启动多个StoreFileScanner和一个MemStoreScanner,最终的结果会同时读取内存和磁盘中的数据,并按照数据的版本号也就是时间戳,获取最新的一条数据返回给客户端。 5、在读取StoreFile时,为了提高读取数据的效率,会构建一个BlockCache作为读缓存。MemStore和StoreFile中查询到的目标数据,都会先缓存到BlockCache中,再返回给客户端。

从整体上来看, HBase写数据的操作只需要把数据写入内存就算完成,反而读数据要从文件开始读。所以,对于HBase,会呈现出写数据比读数据更快的效果。

4、HBase文件压缩流程

上面从HBase读写文件的流程简单的推出了一个结论:HBase的写操作会表现得比读操作更快。但是如果在面试过程中问个为什么,这样简单的流程推导显然无法让面试官信服。这个时候,就需要更加深入HBase的底层,寻找答案。

4.1 HBase底层的LSM树

​ HBase的每个Region中存储的是一系列可搜索的键值映射,底层会以LSM树( Log Structured Merge Tree )的结构来对key进行索引。LSM树也是B-树的一种扩展,很多NoSQL数据库都会采用这种LSM树来存储数据。

​ LSM树的基础思想是将对数据的修改增量保存在内存中,当内存达到指定大小限制后,将这些修改操作批量写入磁盘。这样写的性能得到极大的提升,不过读取的时候就会稍微麻烦一些,需要合并磁盘中的隶属数据和内存中最近修改的操作。LSM树通过这种机制,将一棵大树拆分成N棵小树,这些小树首先写入内存。随着小树越来越大,内存中的小树会flush到磁盘中,磁盘中的树再异步定期进行merge操作,最终将数据合并成一棵大树,以优化读性能。

​ 在HBase中,因为小树要先写入内存,为了防止内存数据丢失,写内存的同时需要暂时持久化到磁盘(HBase的磁盘对应HDFS上的文件 ),这就对应了HBase的MemStore和Hlog。MemStore对应一个可变的内存存储,记录了最近的写(put)操作。当MemStore上的树达到一定大小后,就需要进行flush操作,这样MemStore就变成了HDFS上的磁盘文件StoreFile。之前介绍过,HBase是将所有数据修改存储为单独的版本,因此,对同一个key,会有多个版本保留在MemFile和StoreFile中,这些过时的数据是有冗余的,HBase会定期对StoreFile做merge合并操作,彻底删除无效的空间,多棵小树在这个时候合并成大树,来增强读性情。

​ 在HBase 2.0版本中,还引入了一个重要的机制IN_MEMORY Compact内存压缩,来优化LSM中的内存树。内存压缩是HBase2.0中的一个重要特性,通过在内存中引入LSM结构,减少多余的数据,实现降低flush频率和减少MemFile刷写数据的效果。具体可以参看一个官方的博客:blogs.apache.org/hbase/entry…

​ 在HBase2.0中,可以通过修改hbase-site.xml来对内存压缩方式进行统一配置

<property>
     <name>hbase.hregion.compacting.memstore.type</name>
     <value><none|basic|eager|adaptive></value>
</property>

复制代码

​ 或者也可以对某个列簇进行单独的设定:

alter 'user',{NAME=>'basicinfo',IN_MEMORY_COMPACTION=>'<none|basic|eager|adaptive>'}

复制代码

内存压缩有四种模式可以选择。默认是basic。另外一个eager模式,会在内存中过滤重复的数据,这也意味着eager模式相比basic模式,内存过滤时会有额外的性能开销,但是刷写文件时的数据量会相对较小。eager模式更适合于数据大量淘汰的场景,比如MQ、购物车等。而另外一个adaptive是一个实验性的选项,其基本作用就是自动判断是否需要启用eager模式。

4.2 HBase文件压缩过程

​ HBase使用LSM树的方式,可以将应用程序级别的随机IO转换成为顺序磁盘IO,对于写性能的提升非常明显。但是LSM树对读数据的性能影响也是非常大的。所以,整体上,相比于MySQL的B+树结构,HBase的写性能会比MySQL高很多,同时读性能又会比MySQL低很多。另外,LSM结构是一种append-only-tree,文件不支持修改,只支持添加,这也正贴合Hadoop的文件机构,再加上HBase设计时,所面临的是TB级别的数据量,这种机制基本也就成了必选的方式了。

​ 而HBase也对读操作做了一定的优化。例如,为了加快对MemFile映射的内存做数据读取,HBase会构建一个布隆过滤器,对内存中的数据进行快速过滤,从而减少对内存的搜索。这也就对应了desc指令中看到的BLOOMFILTER属性。

​ 在HBase中,flush指令,将内存中的数据刷写到HDFS上。这时,只刷写,不过滤数据。每次刷写都会在HDFS上新刷写一个文件。

​ 另外compact和major-compact两个指令,会用来将文件进行合并。

hbase:016:0> help 'compact'
Compact all regions in passed table or pass a region row
to compact an individual region. You can also compact a single column
family within a region.
You can also set compact type, "NORMAL" or "MOB", and default is "NORMAL"
Examples:
Compact all regions in a table:
hbase> compact 'ns1:t1'
hbase> compact 't1'
Compact an entire region:
hbase> compact 'r1'
Compact only a column family within a region:
hbase> compact 'r1', 'c1'
Compact a column family within a table:
hbase> compact 't1', 'c1'
Compact table with type "MOB"
hbase> compact 't1', nil, 'MOB'
Compact a column family using "MOB" type within a table
hbase> compact 't1', 'c1', 'MOB'

hbase:015:0> help 'major_compact'
Run major compaction on passed table or pass a region row
to major compact an individual region. To compact a single
column family within a region specify the region name
followed by the column family name.
Examples:
Compact all regions in a table:
hbase> major_compact 't1'
hbase> major_compact 'ns1:t1'
Compact an entire region:
hbase> major_compact 'r1'
Compact a single column family within a region:
hbase> major_compact 'r1', 'c1'
Compact a single column family within a table:
hbase> major_compact 't1', 'c1'
Compact table with type "MOB"
hbase> major_compact 't1', nil, 'MOB'
Compact a column family using "MOB" type within a table
hbase> major_compact 't1', 'c1', 'MOB'

复制代码

​ 这个compact指令是将相邻的部分小文件合并成大文件,并且他不会删除过时的数据,所以性能消耗不会太大。而major-compact指令是将所有的storefile文件合并成一个大文件,这时他就会删除过时的数据,就会消耗很多的机器性能。

​ 当我们发送一个delete指令删除一个列时,HBase并不会直接删除数据,而是给数据添加一个删除标记,这样客户端就查不到当前列的值。而在flush阶段,只会删除那一列最新版本的数据,但是删除标记同样不会删除,以保证历史的版本不会让客户端查询出来。compact阶段,由于数据依然没有统一,所以删除标记依然不会删除,以保证客户端始终查不到历史版本的数据。只到major-compact阶段,数据全部合并到一个StoreFile中时,才会将历史版本的数据以及删除标记一起删除。

不同版本的数据可以使用下面的指令查看。包含历史版本以及删除标记。

hbase> scan 't1', {RAW => true, VERSIONS => 10}

​ HBase提供了按照HFile文件大小以及文件个数,定时触发compact和major-compact的机制。例如 hbase.hregion.majorcompaction这个参数就用来配置自动文件压缩的时间间隔

但是这个参数在生产环境一般都是建议设置为0,关闭的。由手动来定时触发major-compact操作。这是因为文件压缩需要对数据做大量的合并和删除,会影响线上的性能。所以通过定时脚本保证集群在晚上业务不太繁忙时进行major-compact。

​ 如果storeFile文件过大,HBase还会有另外的机制将storefile重新拆分成几个大小合适的文件,即Region,分到不同的RegionServer上。所以整体上,如果HBase的数据操作频繁, 你可以看到他的文件是分久必合合久必分,经常变来变去的。

五、HBase客户端

HBase支持多种客户端的API操作。从官方文档中可以看到, HBase支持Rest、Thrift、C/C++、Scala、Jython多种客户端,其中这个Jython比较有趣,是Python语言的一种纯JAVA实现,整体上兼容了JAVA和Python。

​ 我们这次重点就关注HBase的Rest和Java两种API。

1、Rest API

基础服务指令:

#前台启动
bin/hbase rest start -p <port>
#后台启动 日志记录在$HBASE_LOGS_DIR
bin/hbase-daemon.sh start rest -p <port>
#停止Rest服务
bin/hbase-daemon.sh stop rest

复制代码

这里要注意下HBase的Rest服务默认的启动接口是8080,这个接口用得非常多,非常容易端口冲突。spark默认就会使用这个端口。所以可以另行指定一个端口,比如9090。

HBase提供了非常丰富的Rest端口来管理HBase,这里列出了几个比较常用的端口。具体所有的窗口可以查看官方文档。

Rest地址HTTP方法意义
/status/clusterGET查看集群状态
/namespacesGET查看hbase的命名空间。后面可以带上具体的命名空间,查看某个namespace的详情
/namespaces/{namespace}/tablesGET列出命名空间下的所有表。
/namespaces/{namespace}POST创建命名空间。
/namespaces/{namespace}DELETE删除命名空间。命名空间必须是空的才可以删除。
/{table}/schemaGET查看表结构。同时POST请求更新表结构,PUT创建或更新一张表。DELETE删除表
/{table}/regionsGET查看表所在的region
/{table}/{row_key}PUT往表中写入一条数据。
/{table}/{row_key}/{column:qualifier}/{version_id}DELETE删除表中某一个历史版本数据

2、Java API

​ HBase作为一个NoSQL的数据库,他的客户端就设计得相当简单了。构建HBase客户端,需要引入Maven依赖:

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>2.4.4</version>
</dependency>

<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-client</artifactId>
    <version>2.4.4</version>
</dependency>

复制代码

​ 然后就可以调用HBase提供的API进行数据操作了。主要记住几个关键对象即可。例如对HBase的所有操作都基于Connection对象。表结构管理都通过HBaseAdmin对象来操作,表数据都通过Table对象来操作。其他的代码不用强记,用到哪里点哪里就行。 只不过要注意下,HBase2.X版本的源码中有非常多已经过时的类,现在版本勉强还可以用,但是源码中很多都会提示在未来的3.X版本会删除,所以尽量使用不过时的类。

1、获取HBase管理实例

public Connection getConfig() throws IOException {
        final Configuration configuration = HBaseConfiguration.create();
        //必选参数
        configuration.set("hbase.zookeeper.quorum","hadoop01,hadoop02,hadoop03");
        configuration.set("hbase.zookeeper.property.clientPort", "2181");
        //其他可选参数
        configuration.set("hbase.client.scanner.timeout.period", "10000");
        configuration.set("hbase.hconnection.threads.max", "0");
        configuration.set("hbase.hconnection.threads.core", "0");
        configuration.set("hbase.rpc.timeout", "60000");
        return ConnectionFactory.createConnection(configuration);
    }

复制代码

2、判断表是否存在

//查询表是否存在。默认使用default命名空间。其他命名空间需要传入 {namespace}:tableName
    public Boolean isTableExist(Connection connection,String tableName) throws IOException {
        final HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        return admin.tableExists(TableName.valueOf(tableName));
    }

复制代码

3、创建表

    public void createTable(Connection connection,String tableName,String... columnFamilys) throws IOException {
        final HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        if(columnFamilys.length<=0){
            System.out.println("请设置列簇信息");
        }else if (admin.tableExists(TableName.valueOf(tableName))) {
            System.out.println("表"+tableName+"已经存在。");
        }
        else{
            //2.0.0版本新的API。官网说明2.0.0版本有很多API已经过时,预计会在3.0版本后删除。如HTableDescriptor等
            TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
            List<ColumnFamilyDescriptor> families = new ArrayList<>();
            for(String columnFamily : columnFamilys){
                final ColumnFamilyDescriptorBuilder cfdBuilder = ColumnFamilyDescriptorBuilder.newBuilder(ColumnFamilyDescriptorBuilder.of(columnFamily));
                final ColumnFamilyDescriptor cfd = cfdBuilder.setMaxVersions(10).setInMemory(true).setBlocksize(8 * 1024)
                        .setScope(HConstants.REPLICATION_SCOPE_LOCAL).build();
                families.add(cfd);
            }
            final TableDescriptor tableDescriptor = builder.setColumnFamilies(families).build();
            admin.createTable(tableDescriptor);
        }
    }

复制代码

4、删除表

    public void dropTable(Connection connection,String tableName) throws IOException {
        final HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
        if(admin.tableExists(TableName.valueOf(tableName))){
            admin.disableTable(TableName.valueOf(tableName));
            admin.deleteTable(TableName.valueOf(tableName));
            System.out.println("表"+tableName+"成功删除");
        }else {
            System.out.println("表"+tableName+"不存在");
        }
    }

复制代码

5、向表中插入数据

public void addRowData(Connection connection,String tableName,String rowkey,
                           String columnFamily,String column,String value) throws IOException {
        Table table = connection.getTable(TableName.valueOf(tableName));
        //构建Put指令,rowkey必传
        Put put = new Put(Bytes.toBytes(rowkey));
        //指定版本时间戳
//        put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(column),System.currentTimeMillis(),Bytes.toBytes(value));
        put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(column),Bytes.toBytes(value));
        //也可以传入一个List<Put> 插入多列数据
        table.put(put);
        //表操作之后记得要close,关闭线程池
        table.close();
       

复制代码

6、根据rowkey删除数据

    public void deleteMultiRow(Connection connection,String tableName,String... rows) throws IOException {
        Table table = connection.getTable(TableName.valueOf(tableName));
        List<Delete> deletes = new ArrayList<>();
        for(String row: rows){
            Delete delete = new Delete(Bytes.toBytes(row));
            deletes.add(delete);
        }
        if(deletes.size()>0){
            table.delete(deletes);
        }
        table.close();
        System.out.println("表数据删除成功");
    }

复制代码

7、Get获取某一行数据

public void getRowData(Connection connection,String tableName,String row) throws IOException {
        Table table = connection.getTable(TableName.valueOf(tableName));
        Get get = new Get(Bytes.toBytes(row));
        final Result result = table.get(get);
        for (Cell cell : result.rawCells()) {
            System.out.println("rowkey:"+Bytes.toString(CellUtil.cloneRow(cell)));
            System.out.println(" 列簇:"+Bytes.toString(CellUtil.cloneFamily(cell)));
            System.out.println(" 列:"+Bytes.toString(CellUtil.cloneQualifier(cell)));
            System.out.println(" 值:"+Bytes.toString(CellUtil.cloneValue(cell)));
            System.out.println(" 版本时间戳:"+cell.getTimestamp());
            System.out.println("==========================");
        }
        table.close();
    }

复制代码

8、Scan获取全部数据

public void scanRowData(Connection connection,String tableName,String startRow,String stopRow) throws IOException{
        Table table = connection.getTable(TableName.valueOf(tableName));
        Scan scan = new Scan();
        if(null != startRow  && !"".equals(startRow)){
            scan.withStartRow(Bytes.toBytes(startRow));
        }
        if(null != stopRow && !"".equals(stopRow)){
            //包含最后一行数据。 默认是左开右闭。
            scan.withStopRow(Bytes.toBytes(stopRow),true);
        }
        ResultScanner scanner = table.getScanner(scan);
        for (Result result : scanner) {
            for (Cell cell : result.rawCells()) {
                System.out.println("rowkey:"+Bytes.toString(CellUtil.cloneRow(cell)));
                System.out.println(" 列簇:"+Bytes.toString(CellUtil.cloneFamily(cell)));
                System.out.println(" 列:"+Bytes.toString(CellUtil.cloneQualifier(cell)));
                System.out.println(" 值:"+Bytes.toString(CellUtil.cloneValue(cell)));
                System.out.println(" 版本时间戳:"+cell.getTimestamp());
                System.out.println("==========================");
            }
            System.out.println("-----------------------");
        }
        table.close();
    }

复制代码

9、main测试方法

public static void main(String[] args) throws IOException {
        BaseDemo baseDemo = new BaseDemo();
        final Connection connection = baseDemo.getConnection();
        String tableName = "my_ns:Student";
        String basicCF="basicinfo";
        String studyCF="studyinfo";
        //建表
//        baseDemo.createTable(connection,tableName,new String[]{"basicinfo","studyinfo"});
        //删除表
//        baseDemo.dropTable(connection,tableName);
        //插入数据
//        baseDemo.addRowData(connection,tableName,"1001",basicCF,"age","10");
//        baseDemo.addRowData(connection,tableName,"1001",basicCF,"name","roy");
//        baseDemo.addRowData(connection,tableName,"1001",studyCF,"grade","100");
//        baseDemo.addRowData(connection,tableName,"1002",basicCF,"age","11");
//        baseDemo.addRowData(connection,tableName,"1002",basicCF,"name","yula");
//        baseDemo.addRowData(connection,tableName,"1002",studyCF,"grade","99");
//        baseDemo.addRowData(connection,tableName,"1003",basicCF,"age","12");
//        baseDemo.addRowData(connection,tableName,"1003",basicCF,"name","sophia");
//        baseDemo.addRowData(connection,tableName,"1003",studyCF,"grade","120");
        //删除数据
//        baseDemo.deleteMultiRow(connection,tableName,new String[]{"1003"});
        //读取某一行数据
        baseDemo.getRowData(connection,tableName,"1001");
        //搜索数据
        baseDemo.scanRowData(connection,tableName,null,null);
        connection.close();
    }

复制代码

六、HBase 优化

1、列簇设计

​ 在设计HBase的表结构时,官网建议,一张表的列簇不要太多,最好是一个列簇。实际项目中,列簇最多也不要超过三个。因为纵向来看,HBase是以列簇为单位来组织文件,对应HDFS中一个单独的目录。如果列簇过多,会造成小文件太多,增加文件频繁合并拆分时的性能消耗。而从横向来看,当表的数据量比较大时,HBase会将一张表拆分成多个Region。如果列簇太多,容易产生数据倾斜问题,即某些热点Region的数据特别多,读写操作也特别多。而其他Region的数据特别少,读写也非常少,这样就会造成性能损耗。所以官方建议每个表就是单列簇就行了,这样HBase对数据文件的管理会更为高效。而如果需要多个列簇的话,尽量保证各个列簇的数量能够均匀增长。

2、预分区与Rowkey设计

​ 默认情况下,HBase对大的StoreFile随机的进行Region划分,但是这样划分是非常不靠谱的,很容易产生数据倾斜的问题。所以,在实际使用时,一般都会进行预分区。这种方式就是手动维护一个region的划分规则,对每个region中的rowkey分布范围进行配置。如果加入的数据符合某个region维护的rowkey范围,就将数据交给这个region维护。用这样的方式,我们可以将数据所要存放的分区提前大致的维护好,以提高HBase的性能。

1、手动划分分区

create 'staff','info',SPLITS=>['1000','2000','3000','4000']

复制代码

​ 这种分区方式会按照Rowkey的前四位进行分区。例如152033333这样的rowkey,就会被分到 1000~2000 的这个分区内。

2、按照文件分区

创建一个splits.txt,文件内容如下:

0
1
2
3
4
5
6
7
8
9

复制代码

然后执行建表语句

create 'staff2','info',SPLITS_FILE=>'splits.txt'

复制代码

这种分区方式就会按照rowkey的第一个数字划分为10个分区。

3、生成十六进制序列预分区

create 'staff3','info',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}

复制代码

这几种方式中,第二种方式一般是用得最多的,第三种方式很少用。

4、在JavaAPI中也可以指定分区。

参见之前的建表Demo,在admin.createTable()方法中有两个重载的方法

   * @param desc table descriptor for table
   * @param startKey beginning of key range
   * @param endKey end of key range
   * @param numRegions the total number of regions to create
void createTable(TableDescriptor desc, byte[] startKey, byte[] endKey, int numRegions) throws IOException;

 * @param desc table descriptor for table
   * @param splitKeys array of split keys for the initial regions of the table
 default void createTable(TableDescriptor desc, byte[][] splitKeys) throws IOException {

复制代码

分区完成后,可以在HBase的管理页面看到表的分区情况。

在这里插入图片描述

对分区的规划一般都会参考集群中节点的数量,一般来说,每个节点上的Region数目不要超过3个。

3、Rowkey设计

​ Rowkey是用来查找HBase表数据的唯一方式,所以Rowkey要包含未来关键的查询属性。例如一个用户表,未来需要按照userName和userPass两个字段查询,那rowkey就应该以这两个字段为主要元素,计算出一个唯一的序列号作为rowkey。

​ 另一方面,当一条数据有了rowkey后,这条数据存储于哪个分区,就取决于rowkey处在哪个分区内了。而我们对rowkey进行设计的目的,主要就是为了能够让数据尽量均匀的分布于所有的region上。这样能在一定程度上防止数据倾斜。但是业务数据的rowkey分布往往不是按照预分区的规则均匀分布的。所以通常需要对rowkey进行一定的散列,以帮助有一定规则的数据能够尽量平均的分配到不同的region中。例如我们之前将staff表分为5个分区,而rowkey的最靠前部分userid很有可能都集中在1000~2000这样的范围内,那这样数据倾斜就非常明显了。一般来说,有几下几种常见的散列方式。

1、hash散列

​ 对每个根据关键业务信息计算出来的原始rowkey进行MD5、AES之类的hash计算。

2、字符串翻转

​ 将整个原始rowkey按照字符顺序进行翻转。 例如13579,反转成为97531。

3、字符串拼接

​ 在原始rowkey的前面添加一个相关的字符串来控制分区。例如原始rowkey计算成为0000{rowkey}。

4、Hive与HBase集成

​ 之前介绍过hive,他也是大数据常用的一个数据仓库软件。他可以将SQL语句转换成为MapReduce计算任务,然后直接索引HDFS上的数据文件。这样就可以以SQL的方式进行大数据的查询分析任务。

​ HBase与Hive的特性在很多方面正好形成互补,天生就适合集成在一起使用。 1、HBase和Hive都是基于HDFS来存数数据。 2、Hive由于需要启动MapReduce计算,所以是非常慢的。而HBase的数据读写操作非常快。 3、HBase只能根据Rowkey来检索数据,对业务场景的限制还是比较大的,而Hive可以通过SQL的方式对数据进行更灵活的检索。

​ 所以在很多项目中,都会将Hive和HBase集成在一起使用。利用HBase高效的读写性能,来提供OLTP的快速数据服务,同时利用Hive快速全面的SQL功能,提供OLAP的数据统计服务。

​ Hive与HBase集成的基本形式是在Hive中创建一个表,并指定映射成HBase中的一张表。而Hive中对这张关联表的操作都会改为索引HBase表中的数据。

Hive与HBase集成步骤:

1、Hive与HBase的版本兼容性

​ 这是很重要的一个前提。在搭建大数据项目时,组件之间的版本兼容性是一个非常头疼的问题。通常会使用CDH来统一解决各个组件的版本兼容。而我们现在全是使用Apache的开源版本组件,就需要我们自己来处理版本兼容问题。

​ 我们使用的Hive版本是3.1.2。这个版本的Hive中默认已经集成了HBase。他集成的HBase版本是2.0.0-alpha4。而我们使用的HBase版本是2.4.4。经过测试,发现这个版本的HBase是可以成功集成Hive的。如果版本之间存在冲突的话,我们下面的实验是无法成功的。这个时候需要下载Hive的源码,自行更新HBase的版本进行更新。

1、关于Hive中的HBase版本,可以查看$HIVE_HOME/lib目录下的hbase开头的几个jar包。

2、编译Hive时,可以下载Hive源码,然后修改Hive中对HBase的maven依赖版本,然后使用maven重新编译,并将Hive中的HBase相关的jar包更新到Hive部署环境中。

2、配置HBase环境变量

​ 在之前介绍hive时,每次启动hive客户端时都会有一个错误日志。

which: no hbase in (:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/es/node-v8.1.0-linux-x64/bin:/app/hadoop/hadoop-3.2.2//bin:/app/hadoop/hadoop-3.2.2//sbin:/app/java/jdk1.8.0_212//bin:/root/bin)

复制代码

​ 这就是因为Hive在启动时会检测有没有安装HBase。可以在Hive的安装节点下编辑HBase的环境变量。

vi ~/.bash_profile
在文件中添加
export HBASE_HOME=/app/hbase/hbase-2.4.4
然后将$HBASE_HOME/bin目录添加到Path环境变量中。

复制代码

​ 这样,再次启动Hive,就没有这个错误日志了。Hive执行过程中打印在控制台的日志量也大了很多。

3、在Hive的hive-site.xml配置文件中增加配置

<property>
  <name>hive.zookeeper.quorum</name>
  <value>hadoop01,hadoop02,hadoop03</value>
</property>
<property>
  <name>hive.zookeeper.client.port</name>
  <value>2181</value>
</property>

复制代码

​ 增加关于HBase使用的Zookeeper集群的配置,这样Hive就可以像HBase一样去寻找Region,读取HDFS上的数据了。

4、Hive中建立映射表

CREATE TABLE hive_hbase_emp_table(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal double,
comm double,
deptno int)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,info:ename,info:job,info:mgr,info:hiredate,info:sal,info:comm,info:deptno")
TBLPROPERTIES ("hbase.table.name" = "hbase_emp_table");

复制代码

其中 TBLPROPERTIES中的hbase.table.name表示要映射到HBase中的表。这个表要是在HBase中不存在的。Hive在建表时会在HBase上同时建表。

SERDEPROPERTIES中的hbase.columns.mapping指定了Hive中的hive_hbase_emp_table表与HBase中的hbase_emp_table表的对应关系。其配置方式就按照字段一个一个映射。比如上面的配置中, :key是一个固定写法,表示HBase表中的Rowkey。而配置中的:key就对应Hive表中的第一个字段empno。后面的info:name就对应hive中的ename字段。后面依次配置各个字段的对应关系。

5、往Hive中插入测试数据

​ 接下来,可以尝试往Hive的hive_hbase_emp_table表中使用insert语句插入数据进行尝试。并在HBase对应的表中查数据。

​ 不过要注意,对于Hive,不能使用load指令的方式直接导入数据。这样导入的数据将无法正确映射到HBase中。但是我们可以找一张Hive的中间表,以insert into xxx select的方式来导入数据。

​ 我们之间在介绍Hive时,创建了一张emp表,其实这个测试表结构就跟emp表的结构是一致的。

CREATE TABLE emp(
empno int,
ename string,
job string,
mgr int,
hiredate string,
sal double,
comm double,
deptno int)
row format delimited fields terminated by ',';

复制代码

​ 然后在这个表中,可以导入我们在文本文件中预先准备的测试数据

hive> load data local inpath '/root/emp' into table emp;

复制代码

​ 接下来,通过中间表往映射表中插入数据

hive> insert into table hive_hbase_emp_table select * from emp;

复制代码

6、检查测试结果

​ 如果没有报错话,将可以在Hive和HBase双方查看到导入的数据。也可以在Hive中执行一些需要计算的复杂SQL。例如 select deptno,sum(sal) from emp group by deptno;

​ 然后我们可以去HDFS上查看下数据是如何组织的。在HDFS上,可以看到,Hive中建立了一个/user/hive/warehouse/hive_hbase_emp_table目录,这是Hive中的表对应的目录。但是这个目录中是没有数据文件的。

​ 再到HBase对应的HDFS目录下去看一下。HDFS中同样创建了一个HBase表对应的目录 /hbase/data/default/hbase_emp_table 。这个目录下会创建一个随机字符串命名的Storefile目录。进入这个目录/hbase/data/default/hbase_emp_table/c4440f96cf62f5394722fd28bef9a734 可以看到HBase中的数据文件。这时会发现这个目录下的数据文件依然是空的。这就是因为现在HBase表中的数据还在内存当中,而从Hive中映射过来的数据并没有及时写入到Memfile。当在HBase中执行 flush 'hbase_emp_table' 指令后,HBase内存中的数据才会刷写到HDFS中。例如在HDFS的/hbase/data/default/hbase_emp_table/c4440f96cf62f5394722fd28bef9a734/info目录下可以看到一个随机字符串命名的文件,这就是HBase的落地文件。

​ 可以看到,Hive与HBase的集成,还是由HBase来保存数据,而此时Hive依然是通过MapReduce计算程序来统计HBase中的数据。

其实从这里可以看到, HBase是深度集成了Hadoop的。虽然HBase落地到HDFS上的是二进制文件,不能直接读取,但是通过Hadoop的MapReduce,可以很轻松的读到HBase中的数据。关于MapReduce如何读取HBase中的数据,其实平常用得会比较少。在官网的介绍文档apache_hbase_reference_guide.pdf中,也有详细的说明。另外在HBase中,也提供了具体的示例代码,我们课程中就不再多介绍了。

另外,还有另外一个重要的框架,Spark。 Spark与HBase的集成,也是HBase中非常重要的一个特性。有兴趣的同学可以自行参考下。

最后,关于HBase的SQL查询支持,还有另外一个框架可以提供。Phoenix,官网地址: phoenix.apache.org/ 。这个框架比较小众,目前的使用场景就限于给HBase提供标准的JDBC支持。然后搭建完成后,需要使用一个SQuirrel SQL的客户端工具来连接。同样留给同学们自行研究。

​ 最后,在Hive中删除这个表,HBase中对应的表也跟着一起删除了。

七、课程总结

​ HBase作为一个基于Hadoop的大数据NoSQL产品,在商业项目中提供了非常强悍的性能。能够支持每天超过千亿级别的读写操作,支持PB级别的数据存储,并且能够支持峰值百万级别的并发读写操作。并且他的稳定性在各大企业的业务实践中也得到了检验。像FaceBook、阿里这些巨头公司更是HBase的深度使用者。这些都体现出了HBase产品的重要性。

​ 在我们的课程中,以目前最新的Hadoop、Spark、Hive、HBase,一步步搭建了一个完整的大数据处理平台,并演示了如何使用这些大数据组件,也介绍了这些组件背后的核心机制与原理。但是随着互联网数据的爆炸式增长,大数据开始日渐成为互联网技术的主流,其技术更新迭代的速度也在加快,新的组件也不断冒出来。再加上大数据的生态体系,决定了往往一个组件的新特性会迅速扩散到其他组件中,因此在大数据体系中,自学能力尤为重要,这样才能跟得上新的版本和新的技术。

​ 实际上,Hadoop+Spark+HBase脱胎于Google的核心论文,也构建起了开源大数据的基石。再加上Hive,这四个组件,已经足够支撑很多的大数据开发要求。后面我们会以此为基础设计实战课程,带大家一起熟悉。

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改