HBase实战(二)

发布于:2025-09-03 ⋅ 阅读:(17) ⋅ 点赞:(0)

关于HBase:基础操作、原理。
接上文:HBase实战(一)https://core815.blog.csdn.net/article/details/150944040

1.HBase基础操作

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

1.1 基础指令

  • 使用HBase的客户端:
# 查看HBase基础指令
hbase --help
# hbase命令行
hbase shell
# 查看帮助(在命令行)
hbase:001:0> help
# 列出已有的表
hbase:002:0> list
  • 基础的表操作:
# 创建表 user表,有一个列簇 basicinfo
create 'user','basicinfo';
# 插入一条数据
put 'user','1001','basicinfo:name','roy';
put 'user','1001','basicinfo:age',18;
put 'user','1001','basicinfo:salary',10000;
# 插入第二条数据
put 'user','1002','basicinfo:name','sophia';
put 'user','1002','basicinfo:sex','female';
put 'user','1002','basicinfo:job','manager';
# 插入第三条数据
put 'user','1003','basicinfo:name','yula';
put 'user','1003','basicinfo:school','phz school';
  • 数据操作:
# 按照RowKey,查找单条记录
get 'user','1001';
get 'user','1001','basicinfo:name';
# 按照版本查询数据,每次put都会给这条数据的VERSIONS+1
get 'user','1001',{COLUMN => 'basicinfo:name',VERSIONS=>3};
# 使用scan,查询多条记录
scan 'user';
scan 'user',{STARTROW => '1001',STOPROW => '1002'};

HBase查询数据只能依据Rowkey来进行查询,而Rowkey是由客户端直接指定的,所以在使用HBase时, Rowkey如何设计非常重要,要带上重要的业务信息。

scan指令后面的查询条件,STARTROW和STOPROW是必须大写的。查询的结果是左开右闭的。

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

# 查看表结构这个结果很重要。列出了列簇的所有属性。
hbase:018:0> desc 'user';
Table user is ENABLED
user, {TABLE_ATTRIBUTES => {METADATA => {'hbase.store.file-tracker.impl' => 'DEFAULT'}}}
COLUMN FAMILIES DESCRIPTION
{NAME => 'basicinfo', INDEX_BLOCK_ENCODING => 'NONE', VERSIONS => '1', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOC
K_ENCODING => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', IN
_MEMORY => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536 B (64KB)'}

# 查询表中的记录数。
hbase:019:0> count 'user';
3 row(s)

# 修改表结构
hbase:020:0> alter 'user',{NAME => 'basicinfo',VERSIONS => 3};
Updating all regions with the new schema...
1/1 regions updated.
Done.

hbase:021:0> desc 'user';
Table user is ENABLED
user, {TABLE_ATTRIBUTES => {METADATA => {'hbase.store.file-tracker.impl' => 'DEFAULT'}}}
COLUMN FAMILIES DESCRIPTION
{NAME => 'basicinfo', INDEX_BLOCK_ENCODING => 'NONE', VERSIONS => '3', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOC
K_ENCODING => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', IN
_MEMORY => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536 B (64KB)'}

# 删除某一列
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部分。

1.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,这个是默认的命名空间。不指定命名空间的表都会创建在这个命名空间下。

2.HBase原理

2.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.2 HBase写数据流程

HBase写数据的流程如下图:
在这里插入图片描述
(1)client向HRegionServer发送写请求;
(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的几个时机:
(1)MemStore级别限制:habse.hregion.memstore.flush.size 默认128M。 当Region中任意一个MemStore的大小达到这个上限,就会触发一次MemStore操作,生成一个HFile。
(2)Region级别限制:hbase.hregion.memstore.block.multiplier 默认4. 当Region中所有MemStore的大小总和达到上限,也会触发MemStore的刷新。这个上限就是hbase.hregion.memstore.flush.size * habse.hregion.memstore.flush.size。
(3)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。

(4)WAL级别限制:hbase.regionserver.maxlogs WAL数量上限。当RegionServer的WAL文件数量达到这个上下后,系统就会选取最早的Hlog对应的一个或多个Region进行
Flush。这时候会在日志中有一条报警信息 Too many WALs。count=…
(5)定期刷新MemStore:hbase.regionserver.optionalcacheflushinterval 默认是3600000 单位是毫秒,即1个小时。这是HBase定期刷新MemStore的时间间隔。通常在生产中,为了尽量保证业务性能会将这个参数配置为0,表示关闭定时自动刷写。
(6)手动调用flush执行。

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

2.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,会呈现出写数据比读数据更快的效果。

2.4 HBase文件压缩流程

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

2.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刷写数据的效果。具体可以参看一个官方的博客:https://blogs.apache.org/hbase/entry/accordion-hbase-breathes-with-in。

在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模式。

2.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:033: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:036: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的数据操作频繁, 你可以看到他的文件是分久必合合久必分,经常变来变去的。

3.相关资源

百度网盘:https://pan.baidu.com/s/1eaN_8YWIquJKESAmMxhtDg?pwd=vg5n
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到