HBase 随笔
数据存储模型分类
**技能支撑:**行式存储 && 列式存储
行式存储:关系型数据库【Oracle、MySQL、SQLServer】;查询效率高,统计效率低。 列式存储:NoSQL数据库【HBase、cassandra】;查询效率低,统计效率高,插入效率高。
行式存储:按照记录进行存储,存储的每一行数据都是一个数据记录。 列式存储:按照字段进行存储。
1 行式存储
概念:行式存储的数据是跳跃式的存储在磁盘上。
当SQL语句执行时,会增多磁盘的转动次数,磁头也需要多次长距离的移动,随之带来的饿问题即是————IO操作增多,压力加大。
数据量越大,SQL语句越复杂,关系型数据库的执行效率会出现“断崖式下降”!!!
2 列式存储
2.1 概念
概念:列式存储将同一列上的数据存储在磁盘的连续的位置上。
所有的SQL语句在执行查询时,只有2个过程
- 按照某些特定的字段进行查询
- 按照某些特定的字段进行输出
列式存储在进行数据查询时,磁盘只需要转动很少的次数,磁头只需要移动很少的距离,就能完成数据的查询,并能将数据返回。相比较“行式存储”而言,IO操作更少!!!
2.2 解决的问题
列式存储解决的问题:
-
数据库不读取无效数据 —— 提高了系统的IO
①:只查询指定列上的数据 ②:只返回指定列上的数据 ③:磁盘 IO 效率高
-
数据的压缩比 —— 节省磁盘空间
数据按照列来存储,每个数据包内都是同样结构的数据。 ①:数据相关性高 ②:数据压缩比高 ③:利用Cache IO
2.3 列族技术
【Column Family】
当碰上海量数据的原子操作时,需要使用列族技术通过对数据的“标记”提高效率。
针对列式存储的结构:
添加数据:添加同key的键值对进行数据的覆盖;
修改数据:在键值对的后面加上时间,通过时间的比对,时间值大表示晚,也就是新数据;
删除数据:在键值对的后面加上类似delete标记,表示删除,查询时对于标记有delete的数据不显示。
第一章 HBase简介
1.1HBase简介
1.1.1 HBase的定义
HBase是一种分布式、可扩展、支持海量数据存储的NoSQL数据库。
HBase是面向列存储,构建于Hadoop上的。
Redis作用:配合关系型数据库做高速缓存,降低关系型数据库的IO。
HBase作用:当有海量数据时,可以充当关系型数据库的替代品。
1.1.2 HBase的特点
-
海量存储
-
列式存储
这里的列式存储其实说的是列族存储,HBase是根据列族存储数据的,列族下面可以有非常多的列,列族在创建表的时候就必须指定说明。
-
易扩展
HBase的扩展能力体现在两个方面
- 提供数据存储方面:因为HBase数据存储在HDFS中,所以扩展HDFS的内存即可。
- 提供服务能力方面:通过横向添加RegionServer的机器,进行水平扩展,提升HBase上层的处理能力【RegionServer类似于Hadoop中的NameNode,只不过NameNode单点工作,而RegionServer可以多点同时工作】
-
高并发
-
稀疏
这里的稀疏主要是针对HBase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。
1.1.3 HBase的优缺点
优点
①HDFS有高容错,高扩展的特点,而Hbase基于HDFS实现数据的存储,因此Hbase拥有与生俱来的超强的扩展性和吞吐量。
②HBase采用的是Key/Value的存储方式,这意味着,即便面临海量数据的增长,也几乎不会导致查询性能下降。
③HBase是一个列式数据库,相对于于传统的行式数据库而言。当你的单张表字段很多的时候,可以将相同的列(以regin为单位)存在到不同的服务实例上,分散负载压力。
缺点
①架构设计复杂,且使用HDFS作为分布式存储,因此只是存储少量数据,它也不会很快。在大数据量时,它慢的不会很明显!
②**Hbase不支持表的关联操作,**因此数据分析是HBase的弱项。常见的 group by或order by只能通过编写MapReduce来实现!
③Hbase部分支持了ACID
1.1.4 HBase的使用场景
适合场景:单表超千万,上亿,且高并发!
不适合场景:主要需求是数据分析,比如做报表。数据量规模不大,对实时性要求高!
1.2 HBase的数据模型
逻辑上,HBase的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从HBase的底层物理存储结构(K-V)来看,HBase更像是一个multi-dimensional map。
1.2.1 HBase逻辑结构图
1.2.2 HBase的物理存储结构
1.2.3 表基本组成及概念
-
Name Space
命名空间,类似于关系型数据库的database概念,每个命名空间下有多个表。HBase两个自带的命名空间,分别是hbase和default,hbase中存放的是HBase内置的表,default表是用户默认使用的命名空间。
一个表可以自由选择是否有命名空间,如果创建表的时候加上了命名空间后,
这个表名字以:
作为区分! -
Table
类似于关系型数据库的表概念。不同的是,HBase定义表时只需要声明列族即可,数据属性,比如超时时间(TTL),压缩算法(COMPRESSION)等,都在列族的定义中定义,不需要声明具体的列。
这意味着,往HBase写入数据时,字段可以动态、按需指定。因此,和关系型数据库相比,HBase能够轻松应对字段变更的场景。
-
Row
HBase表中的每行数据都由一个RowKey和多个Column(列)组成。一个行包含了多个列,这些列通过列族来分类,行中的数据所属列族只能从该表所定义的列族中选取,不能定义这个表中不存在的列族,否则报错NoSuchColumnFamilyException。
-
RowKey
Rowkey由用户指定的一串不重复的字符串定义,是一行的唯一标识!类似于MySql中的主键。数据是按照RowKey的字典顺序存储的,并且查询数据时只能根据RowKey进行检索,所以RowKey的设计十分重要。
如果使用了之前已经定义的RowKey,那么会将之前的数据更新掉!
-
Column Family
列族是多个列的集合。一个列族可以动态地灵活定义多个列。表的相关属性大部分都定义在列族上,同一个表里的不同列族可以有完全不同的属性配置,但是同一个列族内的所有列都会有相同的属性。
列族存在的意义是HBase会把相同列族的列尽量放在同一台机器上,所以说,如果想让某几个列被放到一起,你就给他们定义相同的列族。
官方建议一张表的列族定义的越少越好,列族太多会极大程度地降低数据库性能,且目前版本Hbase的架构,容易出BUG。
-
Column Qualifier
Hbase中的列是可以随意定义的,一个行中的列不限名字、不限数量,只限定列族。因此列必须依赖于列族存在!列的名称前必须带着其所属的列族!例如info:name,info:age。
因为HBase中的列全部都是灵活的,可以随便定义的,因此创建表的时候并不需要指定列!列只有在你插入第一条数据的时候才会生成。其他行有没有当前行相同的列是不确定,只有在扫描数据的时候才能得知!
-
TimeStamp
用于标识数据的不同版本(version)。时间戳默认由系统指定,也可以由用户显式指定。
在读取单元格的数据时,版本号可以省略,如果不指定,Hbase默认会获取最后一个版本的数据返回!
-
Cell
一个列中可以存储多个版本的数据。而每个版本就称为一个单元格(Cell)。
Cell由{rowkey, column Family:column Qualifier, time Stamp}确定。
Cell中的数据是没有类型的,全部是字节码形式存贮。
1.3 HBase的基本架构
[详细架构见下方 5.1](# 5.1 HBase架构详情)
架构角色介绍:
1)Region Server
RegionServer是一个服务,存在多个,同时运行,负责多个Region的管理。其实现类为HRegionServer,主要作用如下:
对于数据的操作:get, put, delete;
对于Region的操作:splitRegion、compactRegion。
客户端从ZooKeeper获取RegionServer的地址,从而调用相应的服务,获取数据。
2)Master
Master是所有Region Server的管理者,其实现类为HMaster,主要作用如下:
对于表的操作:create, delete, alter,这些操作可能需要跨多个ReginServer,因此需要Master来进行协调!
对于RegionServer的操作:分配regions到每个RegionServer,监控每个RegionServer的状态,负载均衡和故障转移。
即使Master进程宕机,集群依然可以执行数据的读写,只是不能进行表的创建和修改等操作!当然Master也不能宕机太久,有很多必要的操作,比如创建表、修改列族配置,以及更重要的分割和合并都需要它的操作。
3)Zookeeper
RegionServer非常依赖ZooKeeper服务,ZooKeeper管理了HBase所有RegionServer的信息,包括具体的数据段存放在哪个RegionServer上。
客户端每次与HBase连接,其实都是先与ZooKeeper通信,查询出哪个RegionServer需要连接,然后再连接RegionServer。Zookeeper中记录了读取数据所需要的元数据表
hbase:meata,因此关闭Zookeeper后,客户端是无法实现读操作的!
HBase通过Zookeeper来做master的高可用、RegionServer的监控、元数据的入口以及集群配置的维护等工作。
4)HDFS
HDFS为Hbase提供最终的底层数据存储服务,同时为HBase提供高可用的支持。
第二章HBase的安装
通过学习HBase的架构可以得知,HBase的运行时依赖于Zookeeper和Hadoop(HDFS)的,所以要保证我们的Zookeeper和Hadoop是可以正常运行的。
2.1 启动Zookeeper集群
2.2 启动Hadoop集群
2.3上传HBase并解压
2.4 HBase的安装和配置
2.4.1 配置环境变量
1、
sudo vim /etc/profile.d/my_env.sh
添加
#HBASE_HOME
export HBASE_HOME=/opt/module/hbase
export PATH=$PATH:$HBASE_HOME/bin
2、
使环境变量生效
source /etc/profile
2.4.2 HBase的配置
修改HBase对应的配置文件
-
修改HBase-evn.sh:
不使用hbase自带的zookeeper工具,使用我们自己配置的zookeeper作为管理工具。 export HBASE_MANAGES_ZK=false
-
修改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.zookeeper.quorum</name> <value>hadoop01:2181,hadoop02:2181,hadoop03:2181</value> </property> <property> <name>hbase.zookeeper.property.dataDir</name> <value>/opt/module/zookeeper/zookeeper-3.5.7/zkdata</value> </property> <property> <name>hbase.unsafe.stream.capability.enforce</name> <value>false</value> </property> <property> <name>hbase.wal.provider</name> <value>filesystem</value> </property> </configuration>
- 修改regionservers文件:
hadoop01 hadoop02 hadoop03
- 删除hbase中和hadoop冲突的jar包【log4j】
[heather@hadoop01 lib]$ rm -rf slf4j-log4j12-1.7.25.jar
2.5 资源分发
[heather@hadoop01 conf]$ rsync.sh /etc/profile.d/my_env.sh
[heather@hadoop01 module]$ rsync.sh hbase/
2.6 启动HBase
[heather@hadoop01 module]$ start-hbase.sh
running master, logging to /opt/module/hbase/logs/hbase-heather-master-hadoop01.out
hadoop02: running regionserver, logging to /opt/module/hbase/bin/../logs/hbase-heather-regionserver-hadoop02.out
hadoop03: running regionserver, logging to /opt/module/hbase/bin/../logs/hbase-heather-regionserver-hadoop03.out
hadoop01: running regionserver, logging to /opt/module/hbase/logs/hbase-heather-regionserver-hadoop01.o
注意:
我们虽然在3台机器上搭建了HBase集群,但是并没有指定哪一个机器是master。
结论:
我们在哪一台机器上执行了start-hbase.sh 指令,那么这台机器就是master。
2.7 浏览器检测
2.8 高可用
在HBase中HMaster负责监控HRegionServer的生命周期,均衡RegionServer的负载,如果HMaster挂掉了,那么整个HBase集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以HBase支持对HMaster的高可用配置。
-
在conf目录下创建backup-masters文件
[heather@hadoop01 hbase]$ touch conf/backup-masters
-
在backup-masters文件中配置高可用HMaster节点
hadoop02 hadoop03
-
将整个conf目录scp到其他节点
[heather@hadoop01 conf]$ rsync.sh backup-masters
-
启动hbase
[heather@hadoop01 conf]$ start-hbase.sh
第三章 HBase Shell
3.1 HBase中数据存储目录
-
默认有两个库
hbase : hbase自带的库。 default : 提供给用户操作的库。
-
hbase库默认有两张表:
hbase:meta — 保存的是用户的表和region的对应关系。 hbase:namespace — 保存的是用户自己创建的namespace的信息。
-
HBase中的数据对象的表现形式:
数据库 > 表 > region > 列族 > 具体的数据对象【只有数据对象是文件,其他的都是目录形式。】
库是以目录的形式存放在 /HBase/data 中
表是以目录的形式存放在 /HBase/data/库名 中
region是以目录形式存放在 /HBase/data/库名/表名 中
列族是以目录形式存放在 /HBase/data/库名/表名/region 中
数据是以文件的形式存放在 /HBase/data/库名/表名/region/列族 中,【时间戳为文件名】
3.2 HBase Shell的使用
-
在hbase shell中不要敲 “;”,如果敲了";",需要敲两个 单引号结束!
-
在hbase shell中如果需要使用上下方向键查找历史命令,需要查看xshell的设置配置图!
-
查看帮助
help: 查看所有命令的帮助 help ‘组名’: 查看某组命令的帮助
help ‘命令’: 查看某个命令的帮助
-
在创建表时,如果不写库名,则该表创建在default库下。
3.3 基本使用
3.3.1 库和表的操作
-
进入命令
[heather@hadoop01 ~]$ hbase shell hbase(main):001:0>
-
创建库
create_namespace '库名'
-
查询当前一共有多少个库
list_namespace
-
查询当前一共有多少个表
list
-
创建数据表
格式:create '库名 : 表名' , '列族名1' , '列族名2' 【列族名可以跟多个】 create 'ns1:t1' , 'cf1' , 'cf2'
-
查看表是否创建成功
list_namespace_tables '库名'
-
查看某个表是否存在
exists ' 库名 :表名 '
-
禁用一张表
disable ' ns1 : t1'
-
启用一张表
enable ' ns1 : t1 '
-
删库跑路
①:在删除一个库之前,一定要保证此库是一个空的数据库,先要把里面的表删除掉
②:在删除一张表之前一定要保证此表是禁用状态下的才可以使用删除操作,所以先禁用表
禁用表
disable '库名 : 表名'
删除表
drop '库名 : 表名'
删除库
drop_namespace '库名'
```
#### 3.3.2 数据的操作
1. 创建一个表
create 't1 ' , ' cf1 ' , ' cf2 '
2. 查看一张表的描述信息
describe ' t1 '
3. 向表中插入数据 && 更新数据
put命令格式:
```bash
put ' 库名:表名 ' ,'RowKey' ,'列族名:字段名' ,' value值 '
指明哪个库的哪张表,指明RowKey【类似于主键】,当前数据的字段名,此字段归属哪个列族,具体的值
put ' t1 ' , ' r1 ' , ' cf1:name ' , ' Heather ' ————插入数据操作
put ' t1 ' , ' r1 ' , ' cf1:name ' , ' qishiwei ' ————更新数据操作
-
获取数据
get命令格式:
get ' 库名:表名 ' , 'RowKey' 指明哪个库的哪张表,指明RowKey get 't1','r1'
-
删除数据
delete命令格式:
delete '库名 : 表名' , 'RowKey','列族名:字段名' delete 't1','r1','cf1:age'
-
当删除完数据后,还想查看当前以及删除的历史所有数据,可以使用scan命令
scan命令格式:
scan '库名:表名',{RAW => true,VERSIONS => 10} 或者 scan '库名:表名',{STARTROW => '列族名1', STOPROW => '列族名n'}
如果不加{RAW => true,VERSIONS => 10}或{STARTROW => ‘列族名1’, STOPROW => ‘列族名n’},
则只能看到当前存在的数据,删除的历史数据不可见
hbase(main):011:0> scan 't1' ROW COLUMN+CELL r1 column=cf1:gender, timestamp=1614010156205, value=man r1 column=cf1:name, timestamp=1614011653968, value=qishiwei 1 row(s) Took 0.0588 seconds
第一种方式: hbase(main):014:0> scan 't1',{RAW => true,VERSIONS => 10} 第二种方式: hbase(main):006:0> scan 'student2',{STARTROW => 'r1',STOPROW => 'r2'}
第四章 HBase的API操作
基本上,我们大数据所需要使用到的基础框架,它们的API操作都是一样的,分为3步走
第一步:创建对象,进行连接
第二步:进行API具体操作
第三步:释放资源
代码示例
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.heather</groupId>
<artifactId>myhbase</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
public class Client {
private static TableName student2 = TableName.valueOf("student2");
private Connection hbaseConn = null;
@Before
public void before() throws IOException {
/*
1.创建对象
*/
Configuration conf = HBaseConfiguration.create();
//设置服务器地址
conf.set("hbase.zookeeper.quorum", "hadoop01:2181,hadoop02:2181,hadoop03:2181");
//连接
hbaseConn = ConnectionFactory.createConnection(conf);
}
@After
public void after() throws IOException {
hbaseConn.close();
}
//创建表
@Test
public void createTable() throws IOException {
//从连接获取管理对象
Admin admin = hbaseConn.getAdmin();
/*
2.操作
创建表
*/
//创建列族描述器
ColumnFamilyDescriptor columnFamilyDescriptor = ColumnFamilyDescriptorBuilder
.newBuilder("info".getBytes(StandardCharsets.UTF_8))
.build();
//创建表描述器
TableDescriptor tableDescriptor = TableDescriptorBuilder
.newBuilder(student2)
.setColumnFamilies(Collections.singleton(columnFamilyDescriptor))
.build();
admin.createTable(tableDescriptor);
/*
3.释放资源
*/
admin.close();
}
//插入数据
@Test
public void put() throws IOException {
//1.获取表
Table table = hbaseConn.getTable(student2);
//2.插入数据
Put put = new Put("r1".getBytes(StandardCharsets.UTF_8));
put.addColumn(
"info".getBytes(StandardCharsets.UTF_8),
"name".getBytes(StandardCharsets.UTF_8),
"Jack".getBytes(StandardCharsets.UTF_8)
);
put.addColumn(
"info".getBytes(StandardCharsets.UTF_8),
"age".getBytes(StandardCharsets.UTF_8),
"13".getBytes(StandardCharsets.UTF_8)
);
put.addColumn(
"info".getBytes(StandardCharsets.UTF_8),
"gender".getBytes(StandardCharsets.UTF_8),
"man".getBytes(StandardCharsets.UTF_8)
);
table.put(put);
//3.释放资源
table.close();
}
//查询数据scan
@Test
public void scan() throws IOException {
//1.获取Table对象
Table table = hbaseConn.getTable(student2);
//2.查询数据
Scan scan = new Scan();
//设置扫描的起止列族
scan.withStartRow("r1".getBytes(StandardCharsets.UTF_8));
scan.withStopRow("r2".getBytes(StandardCharsets.UTF_8));
//获取扫描器对象扫描数据的结果集
ResultScanner results = table.getScanner(scan);
//results是多行数据,迭代results
for (Result result : results) {
//result是一行的数据,通过rawCells()方法获取cell数据的集合
getResult(result);
}
}
//查询数据get
@Test
public void get() throws IOException {
Table table = hbaseConn.getTable(student2);
Get get = new Get("r1".getBytes(StandardCharsets.UTF_8));
//get可以添加约束条件,查询某一列
get.addColumn(
"info".getBytes(StandardCharsets.UTF_8),
"name".getBytes(StandardCharsets.UTF_8)
);
Result result = table.get(get);
getResult(result);
table.close();
}
//查询数据的代码封装
private void getResult(Result result){
Cell[] cells = result.rawCells();
//遍历cells数组
for (Cell cell : cells) {
//获取RowKey、列族、字段名、时间戳和具体的数据。【需要使用CellUtil工具类】
byte[] rowKey = CellUtil.cloneRow(cell);
byte[] cf = CellUtil.cloneFamily(cell);
byte[] cq = CellUtil.cloneQualifier(cell);
long timestamp = cell.getTimestamp();
byte[] value = CellUtil.cloneValue(cell);
System.out.println(
new String(rowKey, StandardCharsets.UTF_8)+","+
new String(cf,StandardCharsets.UTF_8)+","+
new String(cq,StandardCharsets.UTF_8)+","+
timestamp + "," +
new String(value,StandardCharsets.UTF_8)
);
}
}
}
第五章 HBase进阶
5.1 HBase架构详情
5.1.1 各角色之间的关系
Region = RowKey + 若干个store
Store : 同一个列族修饰下的所有数据信息
Store = MemStore + StoreFile(实现类是HFile)
1)StoreFile
保存实际数据的物理文件,StoreFile以Hfile的形式存储在HDFS上。每个Store会有一个或多个StoreFile(HFile),数据在每个StoreFile中都是有序的。
2)MemStore
写缓存,由于HFile中的数据要求是有序的,所以数据是先存储在MemStore中,排好序后,等到达刷写时机才会刷写到HFile,每次刷写都会形成一个新的HFile。
3)WAL / HLog
由于数据要经MemStore排序后才能刷写到HFile,但把数据保存在内存中会有很高的概率导致数据丢失,为了解决这个问题,数据会先写在一个叫做Write-Ahead logfile的文件中,然后再写入MemStore中。所以在系统出现故障的时候,数据可以通过这个日志文件重建。
每间隔hbase.regionserver.optionallogflushinterval(默认1s), HBase会把操作从内存写入WAL。
一个RegionServer上的所有Region共享一个WAL实例。
WAL的检查间隔由hbase.regionserver.logroll.period定义,默认值为1小时。检查的内容是把当前WAL中的操作跟实际持久化到HDFS上的操作比较,看哪些操作已经被持久化了,被持久化的操作就会被移动到.oldlogs文件夹内(这个文件夹也是在HDFS上的)。一个WAL实例包含有多个WAL文件。WAL文件的最大数量通过hbase.regionserver.maxlogs(默认是32)参数来定义。
4)BlockCache
读缓存,每次查询出的数据会缓存在BlockCache中,方便下次查询。
5.2 写数据流程
1.首先,请求zookeeper,在zk中找到hbase:meta表region所在regionserver。
2.其次,需要查询hbase:meta表,查询region和regionserver的对应关系。
3.最后,需要找到写入的记录所在的region的regionserver,向这个regionserver发起请求。
5.2.1 写流程基本流程图
5.2.2 宏观写数据流程
写流程:
1)Client先访问zookeeper,获取hbase:meta表位于哪个Region Server。
2)访问对应的Region Server,获取hbase:meta表,根据读请求的namespace:table/rowkey,查询出目标数据位于哪个Region Server中的哪个Region中。并将该table的region信息以及meta表的位置信息缓存在客户端的meta cache,方便下次访问。
3)与目标Region Server进行通讯;
4)将数据顺序写入(追加)到WAL;
5)将数据写入对应的MemStore,数据会在MemStore进行排序;
6)向客户端发送ack;
7)等达到MemStore的刷写时机后,将数据刷写到HFile。
5.2.3 微观写数据流程
代码层次方面解读:
向region中写入数据时有以下步骤
1.获取到锁【尽可能获取更多的锁,至少得获取到一个】
2.生成数据的时间戳
- CP :协处理器(类似于mysql中的trigger【触发器】),通常有2个方法
- 方法1:被触发前处理某些任务
- 方法2:被触发后处理某些任务
3.构建WAL对象
4.将数据写入到WAL的buffer(WAL对象内存中的区域)中
- buffer中新添加的数据暂不同步到WAL的磁盘日志文件中
- 获取最新的MVCC(multi-version cucurency control)【乐观锁】号
5.将数据写入到memstore中
6.将数据从WAL的buffer,sync到磁盘
7.如果成功,滚动MVCC,客户端可见,写成功
8.否则,回滚之前已经写入到memstore中的数据,写入失败
5.3 读数据流程
找读取数据对应的rs:
① 请求zookeeper,查询 /hbase/meta-region-server 节点,获取meta表所在的rs
② 向meta表所在的rs发送读请求,讲读取到的内容缓存到客户端本地,此后就不需要频繁查询meta表
③ 从meta表中,根据region和regionserver的对应关系,找到rowkey所属的region的regionserver
读取数据:
读取的数据存储在列族(store)中!列族在HDFS上就是一个目录,这个目录下存储了很多文件(storefile)数 据如果是刚写入到store中,还没有刷写到磁盘,当前数据就存储在memstore中,有可能这个列的历史版本的数据已 经刷写到磁盘存在storefile中,
- 在扫描时,需要既扫memstore,又扫磁盘上的storefile,扫描出当前列的所有 版本的数据, 从这些数据中挑选出ts最大的返回!
如果扫描历史版本的数据,是扫storefile,那么会发送磁盘IO,效率低,因此可以把扫描到的数据所在的块 (block)缓存到内存中,在内存中保存缓存块的区域,称为blockcache!
在以后的查询中,如果查询的数据在blockcache中有,那么就不需要再扫描storefile了!如果没有,再扫描 storefile,再将数据所在的block缓存到blockcache!
Blockcache在RS中的读缓存,blockcache默认大小为当前RS所在堆缓存的40%,有LRU的回收策略!
block不是HDFS上中的block,是HFile中的block(64k)!
=========================================================================================
具体解读:
get t1,r1 :
扫描r1所在region的所有列族的memstore,从中找r1行的所有列的每个版本的最近数据
扫描r1所在region的所有列族的storefile,从中找r1行的所有列的每个版本的历史数据
将最近的数据和历史数据,汇总,挑选每个列最新的数据!
将刚刚扫描storefile数据所在的block,缓存到blockcache中
put t1,r1,cf1:name,jack: 当对数据作了修改时,此时blockcache中缓存就失效了!
底层借助于时间戳进行了判断。
scan t1 ,{STARTROW=>r1,STOPROW=>r4}: 扫描r1到r3行
扫描r1所在region的所有列族的memstore,从中找r1-r3行的所有列的每个版本的最近数据
扫描r1所在region的所有列族的storefile,从中找r2-r3行的所有列的每个版本的历史数据
从blockcache中扫描r1行所有的数据
将刚刚扫描storefile数据所在的block,缓存到blockcache中。
5.4 flush时VERSION个数
-
每个不同时间戳的cell就是一个版本,时间戳就是版本
-
可以设置列族的VERSIONS属性,当执行flush操作时,put的记录
会根据时间戳选择最新的VERSIONS个版本的数据flush到磁盘中!
总结:
每次flush,最多flush VERSIONS个版本的数据!
5.5 MemStore Flush
MemStore存在的意义是在写入HDFS前,将其中的数据整理有序。
MemStore刷写时机:
1.当某个memstore的大小达到了hbase.hregion.memstore.flush.size(默认值128M),其所在region的所有memstore都会刷写。
当memstore的大小达到了
hbase.hregion.memstore.flush.size(默认值128M)
乘以
hbase.hregion.memstore.block.multiplier(默认值4)
时,会阻止继续往该memstore写数据。
2.
①:当region server中memstore的总大小达到
java_heapsize
乘以
hbase.regionserver.global.memstore.size(默认值0.4)
乘以
hbase.regionserver.global.memstore.size.lower.limit(默认值0.95),
region会按照其所有memstore的大小顺序(由大到小)依次进行刷写。直到region server中所有memstore的总大小减小到上述值以下。
②:当region server中memstore的总大小达到
java_heapsize
乘以
hbase.regionserver.global.memstore.size(默认值0.4)
时,会阻止继续往所有的memstore写数据。
3. 到达自动刷写的时间,也会触发memstore flush。自动刷新的时间间隔由该属性进行配置hbase.regionserver.optionalcacheflushinterval(默认1小时)。
4.当WAL文件的数量超过hbase.regionserver.max.logs,region会按照时间顺序依次进行刷写,直到WAL文件数量减小到hbase.regionserver.max.log以下(该属性名已经废弃,现无需手动设置,最大值为32)。
5.6 StoreFile Compaction
由于Hbase依赖HDFS存储,HDFS只支持追加写。所以,当新增一个单元格的时候,HBase在HDFS上新增一条数据。当修改一个单元格的时候,HBase在HDFS又新增一条数据,只是版本号比之前那个大(或者自定义)。当删除一个单元格的时候,HBase还是新增一条数据!只是这条数据没有value,类型为DELETE,也称为墓碑标记(Tombstone)
HBase每间隔一段时间都会进行一次合并(Compaction),合并的对象为HFile文件。合并分为两种
minor compaction和major compaction。
在HBase进行major compaction的时候,它会把多个HFile合并成1个HFile,在这个过程中,一旦检测到有被打上墓碑标记的记录,在合并的过程中就忽略这条记录。这样在新产生的HFile中,就没有这条记录了,自然也就相当于被真正地删除了
由于memstore每次刷写都会生成一个新的HFile,且同一个字段的不同版本(timestamp)和不同类型(Put/Delete)有可能会分布在不同的HFile中,因此查询时需要遍历所有的HFile。为了减少HFile的个数,以及清理掉过期和删除的数据,会进行StoreFile Compaction。
Compaction分为两种,分别是Minor Compaction和Major Compaction。Minor Compaction会将临近的若干个较小的HFile合并成一个较大的HFile,但**不会**清理过期和删除的数据。Major Compaction会将一个Store下的所有的HFile合并成一个大HFile,并且**会**清理掉过期和删除的数据。
5.7 Region Split
默认情况下,每个Table起初只有一个Region,随着数据的不断写入,Region会自动进行拆分。刚拆分时,两个子Region都位于当前的Region Server,但处于负载均衡的考虑,HMaster有可能会将某个Region转移给其他的Region Server。
Region Split时机:
1.当1个region中的某个Store下所有StoreFile的总大小超过hbase.hregion.max.filesize,该Region就会进行拆分(0.94版本之前)。
2. 0.94版本之后的切分策略
默认使用IncreasingToUpperBoundRegionSplitPolicy策略切分region, getSizeToCheck()是检查region的大小以判断是否满足切割切割条件。
protected long getSizeToCheck(final int tableRegionsCount) {
// safety check for 100 to avoid numerical overflow in extreme cases
return tableRegionsCount == 0 || tableRegionsCount > 100
? getDesiredMaxFileSize()
: Math.min(getDesiredMaxFileSize(),
initialSize * tableRegionsCount * tableRegionsCount * tableRegionsCount);
}
tableRegionsCount:为当前Region Server中属于该Table的region的个数。
getDesiredMaxFileSize() 这个值是hbase.hregion.max.filesize参数值,默认为10GB。
initialSize的初始化比较复杂,由多个参数决定。
@Override
protected void configureForRegion(HRegion region) {
super.configureForRegion(region);
Configuration conf = getConf();
//默认hbase.increasing.policy.initial.size 没有在配置文件中指定
initialSize = conf.getLong("hbase.increasing.policy.initial.size", -1);
if (initialSize > 0) {
return;
}
// 获取用户表中自定义的memstoreFlushSize大小,默认也为128M
HTableDescriptor desc = region.getTableDesc();
if (desc != null) {
initialSize = 2 * desc.getMemStoreFlushSize();
}
// 判断用户指定的memstoreFlushSize是否合法,如果不合法,则为hbase.hregion.memstore.flush.size,默认为128.
if (initialSize <= 0) {
initialSize = 2 * conf.getLong(HConstants.HREGION_MEMSTORE_FLUSH_SIZE,
HTableDescriptor.DEFAULT_MEMSTORE_FLUSH_SIZE);
}
}
具体的切分策略为tableRegionsCount在0和100之间,则为
initialSize(默认为2*128) * tableRegionsCount^3,例如:
第一次split:1^3 * 256 = 256MB
第二次split:2^3 * 256 = 2048MB
第三次split:3^3 * 256 = 6912MB
第四次split:4^3 * 256 = 16384MB > 10GB,因此取较小的值10GB
后面每次split的size都是10GB了。
tableRegionsCount超过100个,则超过10GB才会切分region。
n(region);
Configuration conf = getConf();
//默认hbase.increasing.policy.initial.size 没有在配置文件中指定
initialSize = conf.getLong(“hbase.increasing.policy.initial.size”, -1);
if (initialSize > 0) {
return;
}
// 获取用户表中自定义的memstoreFlushSize大小,默认也为128M
HTableDescriptor desc = region.getTableDesc();
if (desc != null) {
initialSize = 2 * desc.getMemStoreFlushSize();
}
// 判断用户指定的memstoreFlushSize是否合法,如果不合法,则为hbase.hregion.memstore.flush.size,默认为128.
if (initialSize <= 0) {
initialSize = 2 * conf.getLong(HConstants.HREGION_MEMSTORE_FLUSH_SIZE,
HTableDescriptor.DEFAULT_MEMSTORE_FLUSH_SIZE);
}
}
具体的切分策略为tableRegionsCount在0和100之间,则为
initialSize(默认为2*128) * tableRegionsCount^3,例如:
第一次split:1^3 * 256 = 256MB
第二次split:2^3 * 256 = 2048MB
第三次split:3^3 * 256 = 6912MB
第四次split:4^3 * 256 = 16384MB > 10GB,因此取较小的值10GB
后面每次split的size都是10GB了。
tableRegionsCount超过100个,则超过10GB才会切分region。