1. 简介
HBase是一种分布式、可扩展、支持海量数据存储的NoSQL数据库。Hbase面向列存储,,基于hadoop的数据库,提供一个十亿级行*百万级列级别的表存储,对表中的数据提供实时的随机读写操作!
1.1 优点
- 海量存储:HBase适合存储PB级别的海量数据,在PB级别的数据以及采用廉价PC存储的情况下,能在几十到百毫秒内返回数据。这与HBase的极易扩展性息息相关。正式因为HBase良好的扩展性,才为海量数据的存储提供了便利。
- 列式存储:这里的列式存储其实说的是列族存储,HBase是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定。对列的定义很灵活
- 扩展性强:主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)。通过横向添加RegionSever的机器,进行水平扩展,提升HBase上层的处理能力,提升Hbsae服务更多Region的能力。
- 高并发:在并发的情况下,HBase的单个IO延迟很低。能获得高并发、低延迟的服务。
- 稀疏:稀疏主要是针对HBase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。
1.2 缺点
- 架构设计复杂:使用HDFS作为分布式存储,因此只是存储少量数据,它也不会很快。在大数据量时,它慢的不会很明显!
- 不支持表的关联操作,因此数据分析是HBase的弱项。常见的 group by或order by只能通过编写MapReduce来实现!
总结:适合单表超千万,上亿,且高并发的场景,不适合数据分析,比如做报表及数据量规模不大,对实时性要求高!
2. 物理存储结构
每插入一行数据,其实都是在向hdfs中写一行数据,数据包括行键,列族,列名,时间戳,该行记录的状态,值等。
HBase支持随机写,虽然其构建于hdfs上(hdfs不允许随机写,只允许追加写),但是其通过特殊的方式实现:当更新一条操作时,其实是将原来的数据进行标记为删除,再新增一条数据,并且会有一个时间戳,也称为版本标识,只允许客户端查询时返回时间戳最新的数据!
2.1 名词解释
- NameSpace:命名空间,类似于关系型数据库的database概念,每个命名空间下有多个表。HBase两个自带的命名空间,分别是hbase和default,hbase中存放的是HBase内置的表,default表是用户默认使用的命名空间。
- table:类似于关系型数据库的表概念。不同的是,HBase定义表时只需要声明列族即可,数据属性,比如超时时间(TTL),压缩算法(COMPRESSION)等,都在列族的定义中定义,不需要声明具体的列。这意味着,往HBase写入数据时,字段可以动态、按需指定。因此,和关系型数据库相比,HBase能够轻松应对字段变更的场景。
- row:HBase表中的每行数据都由一个RowKey和多个Column(列)组成。一个行包含了多个列,这些列通过列族来分类,行中的数据所属列族只能从该表所定义的列族中选取,不能定义这个表中不存在的列族
- RowKey: Rowkey由用户指定的一串不重复的字符串定义,是一行的唯一标识!数据是按照RowKey的字典顺序存储的,并且查询数据时只能根据RowKey进行检索,如果使用了之前已经定义的RowKey,那么会将之前的数据更新掉!
-
Column Family:列族是多个列的集合。一个列族可以动态地灵活定义多个列。表的相关属性大部分都定义在列族上,同一个表里的不同列族可以有完全不同的属性配置,但是同一个列族内的所有列都会有相同的属性。列族存在的意义是HBase会把相同列族的列尽量放在同一台机器上,所以说,如果想让某几个列被放到一起,你就给他们定义相同的列族。
-
Colum:Hbase中的列是可以随意定义的,一个行中的列不限名字、不限数量,只限定列族。因此列必须依赖于列族存在!列的名称前必须带着其所属的列族!例如info:name,info:age。创建表的时候并不需要指定列!列只有在你插入第一条数据的时候才会生成。其他行有没有当前行相同的列是不确定,只有在扫描数据的时候才能得知
-
TimeStamp:用于标识数据的不同版本(version)。时间戳默认由系统指定,也可以由用户显式指定。在读取单元格的数据时,版本号可以省略,如果不指定,Hbase默认会获取最后一个版本的数据返回!
-
Cell:一个列中可以存储多个版本的数据。而每个版本就称为一个单元格(Cell)。Cell中的数据是没有类型的,全部是字节码形式存贮。
-
Region:由一个表的若干行组成!在Region中行的排序按照行键(rowkey)字典排序。其不能跨RegionSever,且当数据量大的时候,HBase会拆分Region,Region由RegionServer进程管理。HBase在进行负载均衡的时候,一个Region有可能会从当前RegionServer移动到其他RegionServer上。Region就是存储真实数据的地方,所以基于HDFS
2.2 架构中的角色
Region Server : 是一个服务,负责多个Region的管理。其实现类为HRegionServer,主要作用:
对于数据的操作:get, put, delete;对于Region的操作:splitRegion、compactRegion。客户端从ZooKeeper获取RegionServer的地址,从而调用相应的服务,获取数据。
Master: Master是所有Region Server的管理者,其实现类为HMaster,主要作用如下:
1. 对于表的操作:create, delete, alter,这些操作可能需要跨多个ReginServer,因此需要Master来进行协调!
2. 对于RegionServer的操作:分配regions到每个RegionServer,监控每个RegionServer的状态,负载均衡和故障转移。
3.即使Master进程宕机,集群依然可以执行数据的读写,只是不能进行表的创建和修改等操作!
Zookeeper:RegionServer非常依赖ZooKeeper服务,ZooKeeper管理了HBase所有RegionServer的信息,包括具体的数据段存放在哪个RegionServer上。
客户端每次与HBase连接,其实都是先与ZooKeeper通信,查询出哪个RegionServer可以连接。Zookeeper中记录了读取数据所需要的元数据表hbase:meata,因此关闭Zookeeper后,客户端是无法实现读操作的!HBase通过Zookeeper来做master的高可用、RegionServer的监控、元数据的入口以及集群配置的维护等工作。
hdfs: HDFS为Hbase提供最终的底层数据存储服务,同时为HBase提供高可用的支持
3. hbase的安装
3.1 修改配置
3.1.1 修改hbase-env.sh
配置位于/HBASE_HOME/conf/hbase-env.sh
#放开注释,并设置为false,true代表使用hbase自带的zk
export HBASE_MANAGES_ZK=false
3.1.2 修改hbase-site.xml
<configuration>
<!-- 启动分布式模式 -->
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- hbase存储自身临时数据的地方,默认/tmp,会在每次系统重启时清除数据,为了保证数据不丢失,设置为新增目录 -->
<property>
<name>hbase.tmp.dir</name>
<value>/usr/local/workspace/hbase-2.4.5/tmp</value>
</property>
<!-- 不是很明白,反正设置为false -->
<property>
<name>hbase.unsafe.stream.capability.enforce</name>
<value>false</value>
</property>
<!-- region server的共享目录,持久化hbase数据 -->
<property>
<name>hbase.rootdir</name>
<value>hdfs://hadoop101:9000/HBase</value>
</property>
<!-- Zookeeper集群的地址列表,用逗号分割 -->
<property>
<name>hbase.zookeeper.quorum</name>
<value>hadoop102:2181,hadoop103:2181,hadoop101:2181</value>
</property>
<!-- Zookeeper存放数据的位置,与zk的zoo.cfg配置文件中的dataDir属性值一致即可 -->
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/usr/local/workspace/zookeeper/zkData</value>
</property>
</configuration>
3.1.3 修改regionservers配置文件
hadoop101
hadoop102
hadoop103
配置hbase集群机器主机名
3.2 添加环境变量
3.3 启动
3.3.1 启动zookeeper
#进入zookeeper安装目录下的bn目录,执行以下命令,3台机器均需启动
zkServer.sh start
3.3.2 启动hadoop
start-all.sh
3.3.3 启动hbase
start-hbase.sh
启动后,通过访问http://hadoop101:16010/或使用jps命令查看判断hbase是否启动成功
端口说明:
- 16000是master进程的RPC端口
- 16010是master进程的http端口
- 16020是RegionServer进程的RPC端口
- 16030是RegionServer进程的http端口
4. 命令
4.1 基本命令
# 开启一个hbase shell来操作hbase
hbase shell
开启hbase shell后可使用以下命令来操作hbase
#查看hbase集群状态
status
说明:在hbase shell中不要敲 ;,如果敲了;,需要敲两个单引号结束
4.2 表的操作
- list: 查看所有的表,list后可以使用*等通配符来进行表的过滤!
- create:创建表,需要指定表名和列族名,而且至少需要指定一个列族
格式:create '表名', { NAME => '列族名1', 属性名 => 属性值}, {NAME => '列族名2', 属性名 => 属性值}, … 如果你只需要创建列族,而不需要定义列族属性,那么可以采用快捷写法:create'表名','列族名1' ,'列族名2', … - desc:查看某张表
- disable:禁止表,可以防止在对表做一些维护时,客户端依然可以持续写入数据到表。一般在删除表前,必须停用表。在对表中的列族进行修改时,也需要停用表。
- drop:删除表
- truncate:清空表中的数据
4.3 数据操作
- scan:可以按照rowkey的字典顺序来遍历指定的表的数据。
scan '表名':默认当前表的所有列族。 scan '表名',{COLUMNS=> ['列族:列名'],…} : 遍历表的指定列 scan '表名', { STARTROW => '起始行键', ENDROW => '结束行键' }:指定rowkey范围。如果不指定,则会从表的开头一直显示到表的结尾。区间为左闭右开。 scan '表名', { LIMIT => 行数量}: 指定返回的行的数量 scan '表名', {VERSIONS => 版本数}:返回cell的多个版本 scan '表名', { TIMERANGE => [最小时间戳, 最大时间戳]}:指定时间戳范围, 注意:此区间是一个左闭右开的区间,因此返回的结果包含最小时间戳的记录,但是不包含最大时间戳记录 scan '表名', { RAW => true, VERSIONS => 版本数} scan '表名', { FILTER => "过滤器"} and|or { FILTER => "过滤器"}: 使用过滤器扫描 示例如下: scan 'student' scan 'student',{STARTROW => '1001', STOPROW => '1001'} scan 'student',{STARTROW => '1001'}
Scan时可以设置是否开启Raw模式,开启Raw模式会返回包括已添加删除标记但是未实际删除的数据。在HBase被删除掉的记录并不会立即从磁盘上清除,而是先被打上墓碑标记,然后等待下次major compaction的时候再被删除掉。注意RAW参数必须和VERSIONS一起使用,但是不能和COLUMNS参数一起使用。
-
put:可以新增记录还可以为记录设置属性
put '表名', '行键', '列名', '值' put '表名', '行键', '列名', '值',时间戳 put '表名', '行键', '列名', '值', { '属性名' => '属性值'} put '表名', '行键', '列名', '值',时间戳, { '属性名' =>'属性值'} 示例如下:student是表明,info为列族名 put 'student','1001','info:name','Nick' put 'student','1001','info:sex','male'
- get:和scan查询类似,不过get只能查询出最新的一条记录
get 'student','1001' get 'student','1001','info:name'
-
delete
#删除某rowkey的全部数据: deleteall 'student','1001' #删除某rowkey的某一列数据 delete 'student','1002','info:sex'
5. hbase的表现
1.默认有两张系统表
hbase:meta: 保存的是用户的表和region的对应信息
hbase:namespace: 保存的是用户自己创建的namespace的信息
2.hbase中的对象的表现形式
库以目录的形式存放在 /HBase/data中
表是以子目录的形式存在在 /HBase/data/库名 中
region也是以子目录的形式存在 /HBase/data/库名/表名 中
列族也是以子目录的形式存在 /HBase/data/库名/表名/region 中
数据以文件的形式存放在 /HBase/data/库名/表名/region/列族 目录中
6. RegionServer 架构
- StoreFile:保存实际数据的物理文件,StoreFile以Hfile的形式存储在HDFS上。每个Store会有一个或多个StoreFile(HFile),数据在每个StoreFile中都是有序的。
-
MemStore:写缓存,由于HFile中的数据要求是有序的,所以数据是先存储在MemStore中,排好序后,等到达刷写时机才会刷写到HFile,每次刷写都会形成一个新的HFile
-
WAL:由于数据要经MemStore排序后才能刷写到HFile,但把数据保存在内存中会有很高的概率导致数据丢失,为了解决这个问题,数据会先经过WAL处理,然后再写入MemStore中。所以在系统出现故障的时候,数据可以通过WAL恢复。一个RegionServer上的所有Region共享一个WAL实例
-
BlockCache:读缓存,每次查询出的数据会缓存在BlockCache中,方便下次查询
7. 写流程
- Client先访问zookeeper的/hbase/meta-region-server,获取hbase:meta表位于哪个Region Server。
- 访问对应的Region Server,获取hbase:meta表,根据写请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。
- 与目标Region Server进行通讯
- 将数据顺序写入(追加)到WAL
- 将数据写入对应的MemStore,数据会在MemStore进行排序
- 向客户端发送ack
- 等达到MemStore的刷写时机后,将数据刷写到HFile。
代码所体现的写流程:
- 尝试获取尽可能多的锁,至少得获取一个(为了防止多台主机同时写入数据)
- 更新时间,在插入记录时,timestamp是可选的,一旦不提供,使用服务器最近的时间戳来作为
- 构建WAL对象
- 将最新的编辑操作添加到WAL对象的buffer(WAL对象内存中的一块区域)中,buffer中新添加的数据暂时先不同步到wal的磁盘日志文件中!
- 获取最新的MVCC(multi-version concurency control)号(相当于数据库的乐观锁,有一个版本号)
- 将数据写入到memstore,(当前我们的数据只是在WAL对象的buffer中,还没有sync到磁盘文件) 此时就把数据写入到memstore,是完全没问题的!因为我们还没有滚动mvcc版本号,(只有将WAL中的buffer中的数据同步到磁盘文件后,MVCC号才会滚动!在MVCC未滚动时,向memstore中写的数据,scanner是查不到的!)
- 把行锁释放,将WALbuffer中的数据sync到磁盘
- 滚动mvcc版本号,滚动成功之后,scan和get操作就可以查询到此条数据
- 如果wal buffer中的数据同步到磁盘失败,回滚已经写入到memstore中的cell,同时保持MVCC不变
8. 读流程
- Client先访问zookeeper的/hbase/meta-region-server,获取hbase:meta表位于哪个Region Server
-
访问对应的Region Server,获取hbase:meta表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。
-
与目标Region Server进行通讯
-
分别在Block Cache(读缓存),MemStore和Store File(HFile)中查询目标数据,并将查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不同的类型(Put/Delete)
-
将查询到的数据块(Block,HFile数据存储单元,默认大小为64KB)缓存到Block Cache
-
将合并后的最终结果返回给客户端
8.1 读取数据具体流程
1. 读取的数据存储在列族(store)中!列族在HDFS上就是一个目录,这个目录下存储了很多文件(storefile),数据如果是刚写入到store中,还没有刷写到磁盘,当前数据就存储在memstore中(有可能这个列的历史版本的数据已经刷写到磁盘存在storefile中),在扫描时,需要既扫memstore,又扫磁盘上的storefile,扫描出当前列的所有版本的数据,从这些数据中挑选出timestamp最大的返回!
2. 如果扫描历史版本的数据(即扫storefile),那么会发送磁盘IO,效率低,因此可以把扫描到的数据 所在的块(block)缓存到内存中,在内存中保存缓存块的区域,称为blockcache!
在以后的查询中,如果查询的数据在blockcache中有,那么就不需要再扫描storefile了!如果没有,再扫描storefile,将数据所在的block缓存到blockcache!
3. Blockcache是RegionServer中的读缓存,默认大小为当前RS所在堆缓存的40%,有LRU的回收策略!
4. block不是HDFS上中的block,是HFile中的block(默认64k)!
8.2 VERSION
每个不同时间戳的cell就是一个版本,时间戳就是版本.
可以设置列族的VERSIONS属性,当执行flush操作时,put的记录会根据时间戳选择最新的VERSIONS个版本的数据flush到磁盘中!(列族中version属性表示每列保留多少个版本数量,历史版本太多会占大量空间)
9 MemStore Flush
MemStore存在的意义是在写入HDFS前,将其中的数据整理有序
刷写时间:
1. 当某个memstore的大小达到了hbase.hregion.memstore.flush.size(默认值128M),其所在region的所有memstore都会刷写。(为什么所有的memstore都会刷写?因为同一个region中的memstore会涉及到所有的列族,而行数据是由列族组成的,如果只刷写一个memstore,会导致行数据不统一)
2. 当region server中memstore的总大小达到设置的最大内存(jvm heapsize)时
3. 到达自动刷写的时间,自动刷新的时间间隔由该属性进行配置
hbase.regionserver.optionalcacheflushinterval(默认1小时)
10 StoreFile Compaction
compaction(合并小文件)意义:
- 每次对数据进行增删改时,都会新增一条数据,进而由memstore刷写到磁盘,会导致storeFile小文件越来越多,占用HDFS的存储能力(HDFS存储量由内存决定,每个小文件都会占内存)
- 清理已被废弃的数据
HBase每间隔一段时间都会进行一次合并(Compaction),合并的对象为HFile文件。合并分为两种minor compaction和major compaction。调用方式有调用API、使用hbase shell命令、系统到达一定条件后自动执行
10.1 minor合并(minor compaction)
将多个小文件(通过参数配置决定是否满足合并的条件)重写为数量较少的大文件,减少存储文件数量(多路归并),因为hfile的每个文件都是经过归类的,所以合并速度很快,主要受磁盘IO性能影响,经过测试0.94以后的版本,minor合并也会忽略做了删除标记的数据,即物理删除已废弃的数据
10.2 major合并(major compaction)
将一个region中的一个列簇的若干个hfile重写为一个新的hfile。而且major合并能扫描所有的键/值对,顺序重写全部数据,重写过程中会忽略过做了删除标记的数据(超过版本号限制、超过生存时间TTL、客户端API移除等数据)。默认一周切一次,生产最好关闭此配置,如果切分时存在大量的读写操作会导致响应时间变慢
11. Region Split
默认情况下,每个Table起初只有一个Region,随着数据的不断写入,Region会自动进行拆分。刚拆分时,两个子Region都位于当前的Region Server,但处于负载均衡的考虑,HMaster有可能会将某个Region转移给其他的Region Server。
可以手动切分也可由配置指定切分条件,自动切分,切分时不同版本采用不同的切分策略, 之后也可能会出现新的切分策略。
12 API
12.1 准备工作
12.1.1 引入pom
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.4.5</version>
</dependency>
12.1.2 添加配置文件到resource目录
由于hbase需要创建Connection对象连接hbase集群,所以需要设置配置参数供Connection对象使用,在resource目录下创建hbase-site.xml文件,内容如下
<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://hadoop101:9000/HBase</value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.zookeeper.quorum</name>
<value>hadoop102:2181,hadoop103:2181,hadoop101:2181</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/opt/module/zookeeper-3.4.10/datas</value>
</property>
</configuration>
12.2 操作Connection对象
Connection代表客户端和集群的一个连接!这个连接包含对master的连接,和zk的连接!可以使用ConnectionFactory来创建,有以下几点说明:
- Connection的创建是重量级的,因此建议一个应用只创建一个Connection对象
- Connection是线程安全的,可以在多个线程中共享同一个Connection实例
- Connection的生命周期由用户自己控制
- 从Connection中获取Table和Admin对象的实例!Table和Admin对象的创建是轻量级,且不是线程安全的,因此不建议池化或缓存Table和Admin对象的实例,每个线程有自己的Table和Admin对象的实例!
/*
* 1.创建和关闭Connection对象
* 2.在创建Connection对象时需要传入Configuration对象,那么如何在HBase中创建一个Configuration对象
* 可以使用HBaseConfiguration.create(),返回的Configuration,既包含hadoop8个配置文件的参数,又包含hbase-default.xml和hbase-site.xml中所有的参数配置!
* 3. ConnectionFactory.createConnection()创建出来的Connection对象,Configuration就是使用HBaseConfiguration.create()方法创建出来的
*/
public class ConnectionUtil {
//创建一个Connection对象
public static Connection getConn() throws IOException {
return ConnectionFactory.createConnection();
}
public static void close(Connection conn) throws IOException {
if (conn !=null) {
conn.close();
}
}
}
12.3 操作Namespace对象
/*
* 1. 创建/删除/查询/判断是否存在 名称空间
* hbase shell : 开启一个客户端对象
* hbase shell : create_namespace 库名
* 2. Admin : 提供对hbase管理的一些api,例如创建,删除,查询表等!
* 可以使用Connection.getAdmin()来获取Admin的一个实例,使用完成后,调用close关闭!
* 3. NamespaceDescriptor: 用来代表和定义名称空间
*/
public class NameSpaceUtil {
private static Logger logger=LoggerFactory.getLogger(NameSpaceUtil.class);
//查询所有的名称空间
public static List<String> listNameSpace(Connection conn) throws IOException{
List<String> nss=new ArrayList();
//提供一个Admin
Admin admin = conn.getAdmin();
//查询所有的库
NamespaceDescriptor[] namespaceDescriptors = admin.listNamespaceDescriptors();
for (NamespaceDescriptor namespaceDescriptor : namespaceDescriptors) {
//取出每个库描述中库的名称
nss.add(namespaceDescriptor.getName());
}
//关闭admin
admin.close();
return nss;
}
//判断是否库存在
public static boolean ifNSExists(Connection conn,String nsname) throws IOException {
//库名校验
if (StringUtils.isBlank(nsname)) {
logger.error("请输入正常的库名!");
return false;
}
//提供一个Admin
Admin admin = conn.getAdmin();
//根据库名查询对应的NS,如果找不到就抛异常
try {
admin.getNamespaceDescriptor(nsname);
return true;
} catch (Exception e) {
return false;
}finally {
admin.close();
}
}
//创建库
public static boolean creatNS(Connection conn,String nsname) throws IOException {
//库名校验
if (StringUtils.isBlank(nsname)) {
logger.error("请输入正常的库名!");
return false;
}
//提供一个Admin
Admin admin = conn.getAdmin();
//新建库
try {
//先创建库的定义或描述
NamespaceDescriptor descriptor = NamespaceDescriptor.create(nsname).build();
admin.createNamespace(descriptor);
return true;
} catch (Exception e) {
return false;
} finally {
admin.close();
}
}
//删除库
public static boolean deleteNS(Connection conn,String nsname) throws IOException {
if (StringUtils.isBlank(nsname)) {
return false;
}
//提供一个Admin
Admin admin = conn.getAdmin();
//只能删除空库,判断当前库是否为empty,不为空无法删除
//查询当前库下有哪些表
List<String> tables = getTablesInNameSpace(conn, nsname);
if (tables.size()==0) {
admin.deleteNamespace(nsname);
//关闭admin
admin.close();
return true;
}else {
//关闭admin
admin.close();
logger.error(nsname+"库非空!无法删除!");
return false;
}
}
//查询库下有哪些表
public static List<String> getTablesInNameSpace(Connection conn,String nsname) throws IOException{
//库名校验
if (StringUtils.isBlank(nsname)) {
logger.error("请输入正常的库名!");
//在后台提示,库名非法
return null;
}
List<String> tables=new ArrayList();
//提供一个Admin
Admin admin = conn.getAdmin();
//查询当前库所有的表
HTableDescriptor[] tableDescriptors = admin.listTableDescriptorsByNamespace(nsname);
for (HTableDescriptor tableDescriptor : tableDescriptors) {
//取出每个表描述中表的名称
tables.add(tableDescriptor.getNameAsString());
}
//关闭admin
admin.close();
return tables;
}
}
12.4 表操作
/*
* 1. 创建表和删除表
* 2. TableName: 代表表名,调用valueof(String 库名,String 表名),返回表名!如果库名为null,此时使用default作为库名
* 3. HTableDescriptor: 代表表的细节(描述),包含表中列族的描述!
*/
public class TableUtil {
private static Logger logger=LoggerFactory.getLogger(TableUtil.class);
//验证表名是否合法并返回
public static TableName checkTableName(String tableName,String nsname) {
if (StringUtils.isBlank(tableName)) {
logger.error("请输入正确的表名!");
return null;
}
return TableName.valueOf(nsname, tableName);
}
//判断表是否存在
public static boolean ifTableExists(Connection conn,String tableName,String nsname) throws IOException {
//校验表名
TableName tn = checkTableName(tableName, nsname);
if (tn == null) {
return false;
}
Admin admin = conn.getAdmin();
//判断表是否存在,需要传入TableName对象
boolean tableExists = admin.tableExists(tn);
admin.close();
return tableExists;
}
//创建表
public static boolean createTable(Connection conn,String tableName,String nsname,String...cfs) throws IOException {
//校验表名
TableName tn = checkTableName(tableName, nsname);
if (tn == null) {
return false;
}
//至少需要传入一个列族
if (cfs.length < 1) {
logger.error("至少需要指定一个列族!");
return false;
}
Admin admin = conn.getAdmin();
//创建表的描述
HTableDescriptor hTableDescriptor = new HTableDescriptor(tn);
//讲列族的描述也添加到表的描述中
for (String cf : cfs) {
HColumnDescriptor hColumnDescriptor = new HColumnDescriptor(cf);
//添加列族的设置
hColumnDescriptor.setMinVersions(3);
hColumnDescriptor.setMaxVersions(10);
hTableDescriptor.addFamily(hColumnDescriptor);
}
//根据表的描述创建表
admin.createTable(hTableDescriptor);
admin.close();
return true;
}
//删除表
public static boolean dropTable(Connection conn,String tableName,String nsname) throws IOException {
//检查表是否存在
if (!ifTableExists(conn, tableName, nsname)) {
return false;
}
//校验表名
TableName tn = checkTableName(tableName, nsname);
Admin admin = conn.getAdmin();
//删除之前需要先禁用表
admin.disableTable(tn);
//删除表
admin.deleteTable(tn);
admin.close();
return true;
}
}
12.5 数据的操作
/*
* 1.数据的增删改查,需要使用的是Table
* 2.Put: 代表对单行数据的put操作
* 3.在hbase中,操作的数据都是以byte[]形式存在,需要把常用的数据类型转为byte[]
* hbase提供了Bytes工具类:
* Bytes.toBytes(x): 基本数据类型转byte[],Bytes.toXxx(x): 从byte[]转为Xxx类型!
* 4. Get: 代表对单行数据的Get操作!
* 5. Result: scan或get的单行的所有的记录!
* 6. Cell: 代表一个单元格,hbase提供了CellUtil.clonexxx(Cell),来获取cell中的列族、列名和值属性!
*/
public class DataUtil {
//先获取到表的table对象
public static Table getTable(Connection conn,String tableName,String nsname) throws IOException {
//验证表名是否合法
TableName tn = TableUtil.checkTableName(tableName, nsname);
if (tn == null) {
return null;
}
//根据TableName获取对应的Table
return conn.getTable(tn);
}
//put 表名,rowkey,列名(列族名:列名),value
public static void put(Connection conn,String tableName,String nsname,String rowkey,String cf,
String cq,String value) throws IOException {
//获取表对象
Table table = getTable(conn, tableName, nsname);
if (table==null) {
return;
}
//创建一个Put对象
Put put = new Put(Bytes.toBytes(rowkey));
//向put中设置cell的细节信息
put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(cq), Bytes.toBytes(value));
//.addColumn(family, qualifier, value)
table.put(put);
table.close();
}
// get 表名 rowkey
public static void get(Connection conn,String tableName,String nsname,String rowkey) throws IOException {
//获取表对象
Table table = getTable(conn, tableName, nsname);
if (table==null) {
return ;
}
Get get = new Get(Bytes.toBytes(rowkey));
//设置单行查询的详细信息
//设置查哪个列
//get.addColumn(family, qualifier)
//设置查哪个列族
//get.addFamily(family)
//只查某个时间戳的数据
//get.setTimeStamp(timestamp)
//设置返回的versions
//get.setMaxVersions(maxVersions)
Result result = table.get(get);
//System.out.println(result);
parseResult(result);
table.close();
}
//遍历result
public static void parseResult(Result result) {
if (result != null) {
Cell[] cells = result.rawCells();
for (Cell cell : cells) {
System.out.println("行:"+Bytes.toString(CellUtil.cloneRow(cell))+
" 列族:"+Bytes.toString(CellUtil.cloneFamily(cell))+" 列名:"+
Bytes.toString(CellUtil.cloneQualifier(cell))+
" 值:"+Bytes.toString(CellUtil.cloneValue(cell)));
}
}
}
}
13 HBase集成Hive
由于HBase不擅长分析数据,而Hive可以,所以可以将他们结合起来,HBase负责存储,Hive负责分析!
13.1 环境准备
后续可能会在操作Hive的同时对HBase也会产生影响,所以Hive需要持有操作HBase的Jar。
13.1.1 将hbase中的部分jar包赋值到hive中
将hbase中的jar通过软连接赋值到hive中(不用赋值jar包本身,因为会占不少磁盘空间)
ln -s $HBASE_HOME/lib/hbase-common-2.4.5.jar $HIVE_HOME/lib/hbase-common-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-server-2.4.5.jar $HIVE_HOME/lib/hbase-server-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-client-2.4.5.jar $HIVE_HOME/lib/hbase-client-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-protocol-2.4.5.jar $HIVE_HOME/lib/hbase-protocol-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-it-2.4.5.jar $HIVE_HOME/lib/hbase-it-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-hbtop-2.4.5.jar $HIVE_HOME/lib/hbase-hbtop-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-hadoop2-compat-2.4.5.jar $HIVE_HOME/lib/hbase-hadoop2-compat-2.4.5.jar
ln -s $HBASE_HOME/lib/hbase-hadoop-compat-2.4.5.jar $HIVE_HOME/lib/hbase-hadoop-compat-2.4.5.jar
13.1.2 在hive-site.xml中修改zookeeper的属性
在${HIVE_HOME}/conf/hive-site.xml中添加以下配置
<property>
<name>hive.zookeeper.quorum</name>
<value>hadoop102,hadoop103,hadoop101</value>
</property>
<property>
<name>hive.zookeeper.client.port</name>
<value>2181</value>
</property>
13.2 hbase创建数据
13.3 hive创建表来加载hbase数据
create external table hbase_t3(
id int,
age int,
sex string,
name string
)
STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
WITH SERDEPROPERTIES ("hbase.columns.mapping" = ":key,info:age,info:sex,info:name")
TBLPROPERTIES ("hbase.table.name" = "student");
查询结果即可,注意点如下:
1. 表一定要外部表,不能是管理表,因为数据并不来自于hive,而是由hbase管理
2. 在写hbase.columns.mapping值时,顺序一定要与hive定义的表字段顺序相同,(:key表示hbase的rowkey,写第一个表示将hbase中student表的rowkey作为id,位置随意,但是要与字段对应,info表示列族)
3.hbase.table.name表示hbase中的表明,不能自定义
4. STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'是固定写法,表示hive使用hbase的数据格式读取数据
13.4 当hbase还不存在数据而使用hive来插入数据
数据还尚未插入到hbase,可以在hive中建表,建表后,在hive中执行数据的导入,将数据导入到hbase,再分析。 表必须是managed non-native table!
先在hive上创建一张表,并定义hbase的表
CREATE TABLE `hbase_emp`(
`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" = "emp");
insert into table hbase_emp select * from emp
13.5 注意事项
- 在建表时,hive中的表字段的类型要和hbase中表列的类型一致,以避免类型转换失败造成数据丢失
- row format 的作用是指定表在读取数据时,使用什么分隔符来切割数据,只有正确的分隔符,才能正确切分字段.但是hbase中的数据并不能通过常规字符串来分割,必须使用STORED BY 'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
14. 理论知识
1.Storage Handlers:是一个扩展模块,帮助hive分析不在hdfs存储的数据!例如数据存储在hbase上,可以使用hive提供的对hbase的Storage Handlers(HBaseStorageHandler),来读写hbase中的数据!
2.native table(本地表):hive无需通过Storage Handlers就能访问的表。例如之前创建的表,都是native table! 创建native表:[ROW FORMAT row_format] [STORED AS file_format]
file_format: ORC|TEXTFILE|SEQUNCEFILE|PARQUE,都是hive中支持的文件格式,由hive负责数据的读写!
3.non-native table(非本地表):hive必须通过Storage Handlers才能访问的表,例如和hbase集成的表!创建non-native表: STORED BY 'storage.handler.class.name' [WITH SERDEPROPERTIES (...)],数据在外部存储,hive通过Storage Handlers来读写数据!
4. SERDE:序列化器和反序列化器,即表中的数据是什么样的格式,就必须使用什么样的SerDe!
- 纯文本:row format delimited ,默认使用LazySimpleSerDe
- JSON格式: 使用JsonSerde
- ORC: 使用读取ORC的SerDe
- Paquet: 使用读取PaquetSerDe
例如: 数据中全部是JSON格式
{"name":"songsong","friends":["bingbing","lili"]}
{"name":"songsong1","friends": ["bingbing1" , "lili1"]}
错误写法:
create table testSerde(
name string,
friends array<string>
)
row format delimited fields terminated by ','
collection items terminated by ','
lines terminated by '\n';
如果指定了row format delimited ,此时默认使用LazySimpleSerDe!LazySimpleSerDe只能处理有分隔符的普通文本!现在数据是JSON,格式{},只能用JSONSerDE
create table testSerde2(
name string,
friends array<string>
)
ROW FORMAT SERDE
'org.apache.hive.hcatalog.data.JsonSerDe'
STORED AS TEXTFILE
15 优化
15.1 高可用
在HBase中Hmaster负责监控RegionServer的生命周期,均衡RegionServer的负载,如果Hmaster挂掉了,那么整个HBase集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以HBase支持对Hmaster的高可用配置。
配置步骤:在conf目录下创建backup-masters文件,并将需要使用备份的master节点的主机名写在里面
15.2 预分区
通常情况下,每次建表时,默认只有一个region!随着这个region的数据不断增多,region会自动切分!自动切分方式: 将当前region中的所有的rowkey进行排序,排序后取start-key和stopkey中间的rowkey,由这个rowkey一分为二,一分为二后,生成两个region,新增的Region会交给其他的RS负责,目的是为了达到负载均衡,但是通常往往会适得其反!比如切分之后,某一个region成了热点区,导致负载不均衡
为例避免某些热点Region同时分配到同一个RegionServer,可以在建表时,自己提前根据数据的特征规划region!
1.手动设定预分区
create 'staff1','info','partition1',SPLITS => ['1000','2000','3000','4000']
2. 生成16进制序列预分区
create 'staff2','info','partition2',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}
3.当预分区的分区数过多导致shell命令不好操作时,可以编写一个文件,按照文件中设置的规则预分区,例如,常见一个splits.txt文件
create 'staff3','partition3',SPLITS_FILE => 'splits.txt'
4. 使用JavaAPI创建预分区
//自定义算法,产生一系列Hash散列值存储在二维数组中
byte[][] splitKeys = 某个散列值函数
//创建HBaseAdmin实例
HBaseAdmin hAdmin = new HBaseAdmin(HBaseConfiguration.create());
//创建HTableDescriptor实例
HTableDescriptor tableDesc = new HTableDescriptor(tableName);
//通过HTableDescriptor实例和散列值二维数组创建带有预分区的HBase表
hAdmin.createTable(tableDesc, splitKeys);
15.3 rowkey设计
一条数据的唯一标识就是rowkey,那么这条数据存储于哪个分区,取决于rowkey处于哪个一个预分区的区间内,设计rowkey的主要目的 ,就是让数据均匀的分布于所有的region中,在一定程度上防止数据倾斜。
原则:
①rowkey作为数据的唯一主键,需要紧密和业务相关,从业务中选择某个代表性的字段作为rowkey
②保证rowkey字段选取时的唯一性,不重复性
③rowkey足够散列,负载均衡
④让有业务关联的rowkey尽量分布到一个region中
rowkey常用的设计方案如下:
1. 生成随机数、hash、散列值.原本rowKey为1001的,SHA1后变成:dd01903921ea24941c26a48f2cec24e0bb0e8cc7,在做此操作之前,一般我们会选择从数据集中抽取样本,来决定什么样的rowKey来Hash后作为每个分区的临界值
2. 字符串反转
3.字符串拼接
15.4 布隆过滤器
这个组件一般是用作过滤,在海量数据中,用非常高的效率和性能,判断一个数据是否在集合中存在,布隆过滤器只能判断一个数据要么一定在集合中不存在,要么在集合中可能存在!布隆过滤器判断数据可能存在,实际扫描后,发现不存在,这种情况有存在的几率
当经常要判断一个元素是否在一个集合中(一般来讲,计算机中的集合是用哈希表(hash table)来存储的。它的好处是快速准确,缺点是费存储空间)就可以使用布隆过滤器,其大致工作原理为:初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0,Bloom Filter使用n个相互独立的哈希函数来计算集合中每个元素的hash值,并存储在自己的位数组中,当要查询某个元素时,就将利用n个哈希函数计算该元素在位数组所在的位置,如果发现n个哈希函数所计算的结果中有一个不能在位数组中找到,那么就说明数据一定不存在,否则即使n个哈希函数计算的结果全部都能在位数组中找到,也只能是可能存在,因为不同的数据可能存在相同的hash值,会导致误判,一般来说,数据量再大,误判率也就在30%左右
15.5 hbase中的布隆过滤器
HBase支持两种布隆过滤器: ROW|ROWCOL
ROW: 布隆过滤器在计算时,使用每行的rowkey作为参数,进行判断,举例:
storefile1: (r1,info:age,20) ,(r2,info:age,20)
storefile2: (r3,info1:age,20) ,(r4,info1:age,20)
查询r1时,如果命中,在storefile1中可能有,storefile2中一定没有r1的数据,!
ROWCOL: 布隆过滤器在计算时,使用每行的rowkey和column一起作为参数,进行判断!依然用上面的数据举例:
查询rowkey=r1,只查info:age=20 列时,如果命中,在storefile1中可能有,storefile2中一定没有此数据,
启用布隆过滤器后,会占用额外的内存,hbase的布隆过滤器通常是在blockcache和memstore中!
举例:执行get 't1','r1'
①扫描r1所在region的所有列族的memstore,扫memstore时,先通过布隆过滤器判断r1是否存在,如果不存在,就不扫!可能存在,再扫描!
②扫描Storefile时,如果storefile中,r1所在的block已经缓存在blockcache中,直接扫blockcache在扫描blockcache时,先使用布隆过滤器判断r1是否存在,如果不存在,就不扫!可能存在,再扫描!
16 错误解析
16.1 关闭HBase时无法找到Master:no hbase master found
修改pid文件存放路径
进入conf 目录,找到 hbase-env.sh 进行修改将文件中的对应行修改,大体确定报错原因,系统找不到HBase的pid文件,pid文件里面是HBase的进程号,找不到进程号系统就没有办法去结束这个进程。HBase的pid文件默认存放路径为 /tmp 路径,可以进去看一下有没有和HBase相关的文件。肯定没有,因为很有可能被操作系统删掉了。
# 改成自定义地址
export HBASE_PID_DIR=/var/hbase/pids
16.2 没有自动选择HMATER节点或启动失败即访问页面无响应
解决:重启zookeeper并重启hbase服务,
如果在conf目录下有创建过backup-masters文件,那么直接将此文件删除,因为通过start-hbase.sh启动时会自动选择hmater节点,无需此配置