1. 简介
HBase是Hadoop Database的简称 。HBase是一个分布式的、面向列的开源数据库,它不同于一般的关系数据库,是一个适合于非结构化数据存储的数据库。另一个不同的是HBase基于列的而不是基于行的模式。HBase使用和 BigTable非常相同的数据模型。用户存储数据行在一个表里。一个数据行拥有一个可选择的键和任意数量的列,一个或多个列组成一个ColumnFamily
,一个ColumnFamily下的列位于一个HFile
中,易于缓存数据。表是疏松的存储的,因此用户可以给行定义各种不同的列。在HBase中数据按主键排序,同时表按主键划分为多个Region。
HDFS
为HBase提供可靠的底层数据存储服务,MapReduce
为HBase提供高性能的计算能力,Zookeeper
为HBase提供稳定服务和Failover
机制,因此我们说HBase是一个通过大量廉价的机器解决海量数据的高速存储和读取的分布式数据库解决方案。
1.1 特点
1.1.1 海量存储
HBase适合存储PB级别的海量数据,在PB级别的数据以及采用廉价PC存储的情况下,能在几十到百毫秒内返回数据。
1.1.2 列式存储
这里的列式存储其实说的是列族存储,HBase是根据列族来存储数据的。列族下面可以有非常多的列,列族(ColumnFamily)在创建表的时候就必须指定。为了加深对HBase列族的理解,下面是一个简单的关系型数据库的表和HBase数据库的表:
1.1.3 极易扩展
HBase的扩展性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)。
通过横向添加
RegionSever
的机器,进行水平扩展,提升HBase上层的处理能力,提升Hbsae服务更多Region的能力。RegionServer的作用是管理region、承接业务的访问,这个后面会详细的介绍通过横向添加
DataNode
的机器,进行存储层扩容,提升HBase的数据存储能力和提升后端存储的读写能力。
1.1.4 高并发
由于目前大部分使用HBase的架构,都是采用的廉价PC,因此单个IO的延迟其实并不小,一般在几十到上百ms之间。这里说的高并发,主要是在并发的情况下,HBase的单个IO延迟下降并不多。能获得高并发、低延迟的服务。
1.1.5 稀疏
稀疏主要是针对HBase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。
1.2 概念介绍
1.2.1 Column Family
Column Family又叫列族,HBase通过列族划分数据的存储,列族下面可以包含任意多的列,实现灵活的数据存取。HBase表的创建的时候就必须指定列族。就像关系型数据库创建的时候必须指定具体的列是一样的。
HBase的列族不是越多越好,官方推荐的是列族最好小于或者等于3。我们使用的场景一般是1个列族。
1.2.2 Rowkey
Rowkey的概念和mysql中的主键是完全一样的,HBase使用Rowkey来唯一的区分某一行的数据。由于HBase只支持3中查询方式:
基于Rowkey的单行查询
基于Rowkey的范围扫描
全表扫描
因此,Rowkey对HBase的性能影响非常大,Rowkey的设计就显得尤为的重要。设计的时候要兼顾基于Rowkey的单行查询也要键入Rowkey的范围扫描。具体Rowkey要如何设计后续会整理相关的文章做进一步的描述。
1.2.3 Region
**Region的概念和关系型数据库的分区或者分片差不多。**HBase会将一个大表的数据基于Rowkey的不同范围分配到不通的Region中,每个Region负责一定范围的数据访问和存储。这样即使是一张巨大的表,由于被切割到不通的region,访问起来的时延也很低。
1.2.4 TimeStamp
TimeStamp对HBase来说至关重要,因为它是实现HBase多版本的关键。在HBase中使用不同的timestame来标识相同rowkey行对应的不同版本的数据。在写入数据的时候,如果用户没有指定对应的timestamp,HBase会自动添加一个timestamp,timestamp和服务器时间保持一致。在HBase中,相同rowkey的数据按照timestamp倒序排列。默认查询的是最新的版本,用户可指定timestamp的值来读取旧版本的数据。
2. 架构
2.1 介绍
(1)Client
Client包含了访问HBase的接口,另外Client还维护了对应的cache来加速HBase的访问,比如cache的.META.元数据的信息。
(2)Zookeeper
HBase通过Zookeeper来做master的高可用、RegionServer的监控、元数据的入口以及集群配置的维护等工作。
master的高可用:通过Zoopkeeper来保证集群中只有1个master在运行,如果master异常,会通过竞争机制产生新的master提供服务。
通过Zoopkeeper来监控RegionServer的状态,当RegionSevrer有异常的时候,通过回调的形式通知Master RegionServer上下限的信息。
通过Zoopkeeper存储元数据的统一入口地址
(3)HMaster
master节点的主要职责如下:
为RegionServer分配Region
维护整个集群的负载均衡
维护集群的元数据信息
发现失效的Region,并将失效的Region分配到正常的RegionServer上
当RegionSever失效的时候,协调对应Hlog的拆分
(4)RegionServer
RegionServer直接对接用户的读写请求,是真正的“干活”的节点。它的功能概括如下:
管理master为其分配的Region
处理来自客户端的读写请求
负责和底层HDFS的交互,存储数据到HDFS
负责Region变大以后的拆分
负责Storefile的合并工作
(5)HDFS
HDFS为HBase提供最终的底层数据存储服务,同时为HBase提供高可用(Hlog存储在HDFS)的支持,具体功能概括如下:
提供元数据和表数据的底层分布式存储服务
数据多副本,保证的高可靠和高可用性
2.2 HBase的使用场景
HBase是一个通过廉价PC机器集群来存储海量数据的分布式数据库解决方案。它比较适合的场景概括如下:
数据量巨量大(百T、PB级别)
查询简单(基于rowkey或者rowkey范围查询)
不涉及到复杂的关联
比如:
海量订单流水数据(长久保存)
交易记录
数据库历史数据
3. Region的写逻辑
3.1 Region的寻址
HMaster将对应的region分配给不同的RergionServer,由RegionSever来提供Region的读写服务和相关的管理工作,每个Region负责一小部分Rowkey范围的数据的读写和维护,Region包含了对应的起始行到结束行的所有信息。
上图模拟了一个HBase的表是如何拆分成region,以及分配到不同的RegionServer中去。上面是1个Userinfo表,里面有7条记录,其中rowkey为0001到0002的记录被分配到了Region1上,Rowkey为0003到0004的记录被分配到了Region2上,而rowkey为0005、0006和0007的记录则被分配到了Region3上。region1和region2被master分配给了RegionServer1(RS1),Region3被master配分给了RegionServer2(RS2)。
既然读写都在RegionServer上发生,我们前面有讲到,每个RegionSever为一定数量的region服务,那么client要对某一行数据做读写的时候如何能知道具体要去访问哪个RegionServer呢?那就是接下来我们要讨论的问题。
3.1.1 .META.表
HBase有个特殊的表:.META.表
,它的位置存储在ZooKeeper中。
3.1.2 Client缓存
这里还有一个问题需要说明,那就是Client会缓存.META.的数据,用来加快访问。
当.META.的数据发生更新,如上面的例子,由于Region1的位置发生了变化,Client再次根据缓存去访问RS3的时候,会出现错误,当出现异常达到重试次数后就会去.META.所在的RS2获取最新的数据,如果.META.所在的RegionServer也变了,Client就会去ZK上获取.META.所在的RegionServer的最新地址。
3.2 写入流程
从上图可以看出氛围3步骤:
第1步:Client获取数据写入的Region所在的RegionServer
第2步:请求写Hlog
第3步:请求写MemStore
只有当写Hlog和写MemStore都成功了才算请求写入完成。MemStore后续会逐渐刷到HDFS中。Hlog存储在HDFS,当RegionServer出现异常,需要使用Hlog来恢复数据。
3.2.1 Write-Ahead-Log(WAL)
我们理解下HLog的作用。HBase中的HLog机制是WAL的一种实现,而WAL(一般翻译为预写日志)是事务机制中常见的一致性的实现方式。每个RegionServer中都会有一个HLog的实例(备注:1.x版本的可以开启MultiWAL功能,允许多个Hlog),RegionServer会将更新操作(如 Put,Delete)先记录到 WAL(也就是HLog)中,然后将其写入到Store的MemStore,最终MemStore会将数据写入到持久化的HFile中(MemStore 到达配置的内存阀值)。这样就保证了HBase的写的可靠性。对于已经刷盘的数据,其对应的Hlog会有一个过期的概念,Hlog过期后,会被监控线程移动到.oldlogs,然后会被自动删除掉。
从上图我们可以看出都个Region共享一个Hlog文件,单个Region在Hlog中是按照时间顺序存储的,但是多个Region可能并不是完全按照时间顺序。
3.2.2 MemStore刷盘
为了提高HBase的写入性能,当写请求写入MemStore后,不会立即刷盘,而是会等到一定的时候进行刷盘的操作。
- 全局内存控制:比例达到上限时刷盘;
- HBase.hregion.memstore.flush.size:大小达到上限时刷盘;
- HLog数量达到上限时刷盘,默认是32个;
- 手工触发;
- 关闭RegionServer触发;
3.3 物理模型
- 每个column family存储在HDFS上的一个单独文件中,空值不会被保存。
- Key 和 Version number在每个column family中均有一份;
- HBase为每个值维护了多级索引
- 表在行的方向上分割为多个Region;
- Region是HBase中分布式存储和负载均衡的最小单元,不同Region分布到不同RegionServer上。
- Region按大小分割的,随着数据增多,Region不断增大,当增大到一个阀值的时候,Region就会分成两个新的Region;
- Region虽然是分布式存储的最小单元,但并不是存储的最小单元。每个Region包含着多个Store对象。每个Store包含一个MemStore或若干StoreFile,StoreFile包含一个或多个HFile。MemStore存放在内存中。
4. Region的拆分
4.1 三种拆分策略
4.1.1 ConstantSizeRegionSplitPolicy
ConstantSizeRegionSplitPolicy策略是0.94版本之前的默认拆分策略,这个策略的拆分规则是:当region大小达到HBase.hregion.max.filesize(默认10G)后拆分。
这种拆分策略对于小表不太友好,按照默认的设置,如果1个表的Hfile小于10G就一直不会拆分。注意10G是压缩后的大小,如果使用了压缩的话。如果1个表一直不拆分,访问量小也不会有问题,但是如果这个表访问量比较大的话,就比较容易出现性能问题。这个时候只能手工进行拆分。还是很不方便。
4.1.2 IncreasingToUpperBoundRegionSplitPolicy
IncreasingToUpperBoundRegionSplitPolicy策略是HBase的0.94~2.0版本默认的拆分策略,这个策略相较于ConstantSizeRegionSplitPolicy策略做了一些优化,该策略的算法为:min(r^2*flushSize,maxFileSize ),最大为maxFileSize 。
从这个算是我们可以得出flushsize为128M、maxFileSize为10G的情况下,可以计算出Region的分裂情况如下:
第一次拆分大小为:min(10G,1*1*128M)=128M
第二次拆分大小为:min(10G,3*3*128M)=1152M
第三次拆分大小为:min(10G,5*5*128M)=3200M
第四次拆分大小为:min(10G,7*7*128M)=6272M
第五次拆分大小为:min(10G,9*9*128M)=10G
第五次拆分大小为:min(10G,11*11*128M)=10G
从上面的计算我们可以看到这种策略能够自适应大表和小表,但是这种策略会导致小表产生比较多的小region,对于小表还是不是很完美。
4.1.3 SteppingSplitPolicy
SteppingSplitPolicy是在HBase 2.0版本后的默认策略,,拆分规则为:If region=1 then: flush size * 2 else: MaxRegionFileSize。还是以flushsize为128M、maxFileSize为10场景为列,计算出Region的分裂情况如下:
第一次拆分大小为:2*128M=256M
第二次拆分大小为:10G
从上面的计算我们可以看出,这种策略兼顾了ConstantSizeRegionSplitPolicy策略和IncreasingToUpperBoundRegionSplitPolicy策略,对于小表也肯呢个比较好的适配。
4.2 拆分流程
在K的/HBase/region-in-transition/region-name下创建一个znode,并设置状态为SPLITTING
master通过watch节点检测到Region状态的变化,并修改内存中Region状态的变化
RegionServer在父Region的目录下创建一个名称为.splits的子目录
RegionServer关闭父Region,强制将数据刷新到磁盘,并这个Region标记为offline的状态。此时,落到这个Region的请求都会返回NotServingRegionException这个错误
RegionServer在.splits创建daughterA和daughterB,并在文件夹中创建对应的reference文件,指向父Region的Region文件
RegionServer在HDFS中创建daughterA和daughterB的Region目录,并将reference文件移动到对应的Region目录中
在.META.表中设置父Region为offline状态,不再提供服务,并将父Region的daughterA和daughterB的Region添加到.META.表中,已表名父Region被拆分成了daughterA和daughterB两个Region
RegionServer并行开启两个子Region,并正式提供对外写服务
RegionSever将daughterA和daughterB添加到.META.表中,这样就可以从.META.找到子Region,并可以对子Region进行访问了
RegionServr修改/HBase/region-in-transition/region-name的znode的状态为SPLIT
备注:为了减少对业务的影响,Region的拆分并不涉及到数据迁移的操作,而只是创建了对父Region的指向。只有在做大合并的时候,才会将数据进行迁移。
5 使用教程
对于建表,和RDBMS类似,HBase也有namespace的概念,可以指定表空间创建表,也可以直接创建表,进入default表空间。
对于数据操作,HBase支持四类主要的数据操作,分别是:
- Put:增加一行,修改一行;
- Delete:删除一行,删除指定列族,删除指定column的多个版本,删除指定column的制定版本等;
- Get:获取指定行的所有信息,获取指定行和指定列族的所有colunm,获取指定column,获取指定column的几个版本,获取指定column的指定版本等;
- Scan:获取所有行,获取指定行键范围的行,获取从某行开始的几行,获取满足过滤条件的行等。
这四个类都是org.apache.hadoop.HBase.client的子类,可以到官网API去查看详细信息,本文仅总结常用方法,力争让读者用20%的时间掌握80%的常用功能。
5.1 命名空间管理
在关系数据库系统中,命名空间namespace指的是一个表的逻辑分组,同一组中的表有类似的用途。命名空间的概念为即将到来的多租户特性打下基础:
- 配额管理(Quota Management (HBase-8410)):限制一个namespace可以使用的资源,资源包括region和table等;
- 命名空间安全管理(Namespace Security Administration (HBase-9206)):提供了另一个层面的多租户安全管理;
- Region服务器组(Region server groups (HBase-6721)):一个命名空间或一张表,可以被固定到一组regionservers上,从而保证了数据隔离性。
HBase shell:
#Create a namespace
create_namespace 'my_ns'
#create my_table in my_ns namespace
create 'my_ns:my_table', 'fam'
#drop namespace
drop_namespace 'my_ns'
#alter namespace
alter_namespace 'my_ns', {METHOD => 'set', 'PROPERTY_NAME' => 'PROPERTY_VALUE'}
有两个系统内置的预定义命名空间:
- HBase:系统命名空间,用于包含HBase的内部表
- default:所有未指定命名空间的表都自动进入该命名空间
5.2 管理表
5.2.1 创建表
Configuration conf = HBaseConfiguration.create();
HBaseAdmin admin = new HBaseAdmin(conf);
//create namespace named "my_ns"
admin.createNamespace(NamespaceDescriptor.create("my_ns").build());
//create tableDesc, with namespace name "my_ns" and table name "mytable"
HTableDescriptor tableDesc = new HTableDescriptor(TableName.valueOf("my_ns:mytable"));
tableDesc.setDurability(Durability.SYNC_WAL);
//add a column family "mycf"
HColumnDescriptor hcd = new HColumnDescriptor("mycf");
tableDesc.addFamily(hcd);
admin.createTable(tableDesc);
admin.close();
- 可以通过HTableDescriptor对象设置表的特性
- 可以通过HColumnDescriptor对象设置列族的特性
5.2.2 删除表
Configuration conf = HBaseConfiguration.create();
HBaseAdmin admin = new HBaseAdmin(conf);
String tablename = "my_ns:mytable";
if(admin.tableExists(tablename)) {
try {
admin.disableTable(tablename);
admin.deleteTable(tablename);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
admin.close();
说明:删除表前必须先disable表。
5.2.3 修改表
修改表,删除三个列族,新增一个列族:
Configuration conf = HBaseConfiguration.create();
HBaseAdmin admin = new HBaseAdmin(conf);
String tablename = "rd_ns:itable";
if(admin.tableExists(tablename)) {
try {
admin.disableTable(tablename);
//get the TableDescriptor of target table
HTableDescriptor newtd = admin.getTableDescriptor(Bytes.toBytes("rd_ns:itable"));
//remove 3 useless column families
newtd.removeFamily(Bytes.toBytes("note"));
newtd.removeFamily(Bytes.toBytes("newcf"));
newtd.removeFamily(Bytes.toBytes("sysinfo"));
//create HColumnDescriptor for new column family
HColumnDescriptor newhcd = new HColumnDescriptor("action_log");
newhcd.setMaxVersions(10);
newhcd.setKeepDeletedCells(true);
//add the new column family(HColumnDescriptor) to HTableDescriptor
newtd.addFamily(newhcd);
//modify target table struture
admin.modifyTable(Bytes.toBytes("rd_ns:itable"),newtd);
admin.enableTable(tablename);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
admin.close();
5.3 添加/修改行-Put
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "rd_ns:leetable");
Put put = new Put(Bytes.toBytes("100001"));
put.add(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("lion"));
put.add(Bytes.toBytes("info"), Bytes.toBytes("address"), Bytes.toBytes("shangdi"));
put.add(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("30"));
put.setDurability(Durability.SYNC_WAL);
table.put(put);
table.close();
- Put的构造函数都需要指定行键,如果是全新的行键,则新增一行;如果是已有的行键,则更新现有行。
- 创建Put对象及put.add过程都是在构建一行的数据,创建Put对象时相当于创建了行对象,add的过程就是往目标行里添加cell,直到table.put才将数据插入表格;
- 以上代码创建Put对象用的是构造函数1,也可用构造函数2,第二个参数是时间戳;
- Put还有别的构造函数,请查阅官网API。
5.4 删除指定列-Delete
Delete deleteColumn(byte[] family, byte[] qualifier) 删除指定列的最新版本的数据。
Delete deleteColumns(byte[] family, byte[] qualifier) 删除指定列的所有版本的数据。
Delete deleteColumn(byte[] family, byte[] qualifier, long timestamp) 删除指定列的指定版本的数据。
Delete deleteColumns(byte[] family, byte[] qualifier, long timestamp) 删除指定列的,时间戳小于等于给定时间戳的所有版本的数据。
Delete deleteFamily(byte[] family) 删除指定列族的所有列的所有版本数据。
Delete deleteFamily(byte[] family, long timestamp) 删除指定列族的所有列中时间戳小于等于指定时间戳的所有数据。
Delete deleteFamilyVersion(byte[] family, long timestamp) 删除指定列族中所有列的时间戳等于指定时间戳的版本数据。
(1)删除整行的所有列族、所有行、所有版本
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "rd_ns:leetable");
Delete delete = new Delete(Bytes.toBytes("000"));
table.delete(delete);
table.close();
(2)删除指定列的最新版本
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "rd_ns:leetable");
Delete delete = new Delete(Bytes.toBytes("100003"));
// 删除最新版本
delete.deleteColumn(Bytes.toBytes("info"), Bytes.toBytes("address"));
table.delete(delete);
table.close();
(3)删除指定列的所有版本
// 删除所有版本,有个s
delete.deleteColumns(Bytes.toBytes("info"), Bytes.toBytes("address"));
(4)删除指定列族中所有列的时间戳等于指定时间戳的版本数据
delete.deleteFamilyVersion(Bytes.toBytes("info"), 1405390959464L);
5.5 获取单行-Get
- Get addFamily (byte[] family) 指定希望获取的列族
- Get addColumn (byte[] family, byte[] qualifier) 指定希望获取的列
- Get setTimeRange (long minStamp, long maxStamp) 设置获取数据的时间戳范围
- Get setTimeStamp (long timestamp) 设置获取数据的时间戳
- Get setMaxVersions (int maxVersions) 设定获取数据的版本数
- Get setMaxVersions() 设定获取数据的所有版本
- Get setFilter (Filter filter) 为Get对象添加过滤器
- void setCacheBlocks (boolean cacheBlocks) 设置该Get获取的数据是否缓存在内存中
(1)获取行键指定行的所有列族、所有列的最新版本数据
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "rd_ns:leetable");
Get get = new Get(Bytes.toBytes("100003"));
Result r = table.get(get);
for (Cell cell : r.rawCells()) {
System.out.println(
"Rowkey : "+Bytes.toString(r.getRow())+
" Familiy:Quilifier : "+Bytes.toString(CellUtil.cloneQualifier(cell))+
" Value : "+Bytes.toString(CellUtil.cloneValue(cell))
);
}
(2)获取行键指定行中,指定列的最新版本数据
get.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"));
(3)获取行键指定的行中,指定时间戳的数据
get.setTimeStamp(1405407854374L);
(4)获取行键指定的行中,所有版本的数据
get.setMaxVersions();
5.6 获取多行-Scan
- 如果希望获取指定列族的所有列,可使用addFamily方法来添加所有希望获取的列族
- 如果希望获取指定列,使用addColumn方法来添加所有列
- 通过setTimeRange方法设定获取列的时间范围
- 通过setTimestamp方法指定具体的时间戳,只返回该时间戳的数据
- 通过setMaxVersions方法设定最大返回的版本数
- 通过setBatch方法设定返回数据的最大行数
- 通过setFilter方法为Scan对象添加过滤器
- Scan的结果数据是可以缓存在内存中的,可以通过getCaching()方法来查看当前设定的缓存条数,也可以通过setCaching(int caching)来设定缓存在内存中的行数,缓存得越多,以后查询结果越快,同时也消耗更多内存。此外,通过setCacheBlocks方法设置是否缓存Scan的结果数据块,默认为true
- 我们可以通过setMaxResultSize(long)方法来设定Scan返回的结果行数。
(1)扫描表中的所有行的最新版本数据
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "rd_ns:itable");
Scan s = new Scan();
ResultScanner rs = table.getScanner(s);
for (Result r : rs) {
for (Cell cell : r.rawCells()) {
System.out.println(
"Rowkey : "+Bytes.toString(r.getRow())+
" Familiy:Quilifier : "+Bytes.toString(CellUtil.cloneQualifier(cell))+
" Value : "+Bytes.toString(CellUtil.cloneValue(cell))+
" Time : "+cell.getTimestamp()
);
}
}
table.close();
(2)扫描指定行键范围,通过末尾加0,使得结果集包含StopRow
Scan s = new Scan();
s.setStartRow(Bytes.toBytes("100001"));
s.setStopRow(Bytes.toBytes("1000020"));
ResultScanner rs = table.getScanner(s);
(3)返回所有已经被打上删除标记但尚未被真正删除的数据
使用Scan强大的s.setRaw(true)方法,可以获得所有已经被打上删除标记但尚未被真正删除的数据:
Scan s = new Scan();
s.setStartRow(Bytes.toBytes("100003"));
s.setRaw(true);
s.setMaxVersions();