本文分为三个部分:
首先描述hbase建表,读写数据的过程;
然后详细介绍一下这些过程中所使用的API,并给出实例;
最后给出一些在使用HBase客户端时的一些注意事项和建议。
1. 数据的读写流程
1.1 创建表
创建表是Master的任务,首先需要获取master的地址, master启动的时候会把地址告诉zk。因此客户端首先会访问zk获取master地址;client和master通信,然后由master来创建表(包括表的列簇,是否cache,设置存储的最大版本数,是否压缩等)。
1.2 查找地址:
client从Zk上获取Root表的地址,而后找到meta表的地址,并找到新创建的table的regionserver所在,然后client和regionserver通信。
1.3 读写、删除数据
client与regionserver通信,读写、删除数据,hbase是以流式存储的,写入和删除以将数据打上不同的标志append,真正的数据删除操作会在compact时候发生。
2. Client Api介绍
2.1 配置
HBaseConfiguration是每一个hbase client都会使用到的对象,它代表的是HBase配置信息。
它有两种构造方式: public HBaseConfiguration() public HBaseConfiguration(final Configuration c)
默认的构造方式会尝试从hbase-default.xml和hbase-site.xml中读取配置。如果classpath没有这两个文件,就需要你自己设置配置。如函数setconf()功能就是读取配置信息:
Example 2.1:
public static void setconf (){
Configuration HBASE_CONFIG = new Configuration(); HBASE_CONFIG.set(“hbase.zookeeper.quorum”, “zkServer”); HBASE_CONFIG.set(“hbase.zookeeper.property.clientPort”, “2181″); HBaseConfiguration cfg = new HBaseConfiguration(HBASE_CONFIG);
}
2.2 创建表
创建表是通过HBaseAdmin对象来操作的。HBaseAdmin负责表的信息处理,提供createTable方法: public void createTable(HTableDescriptor desc)
默认情况下Hbase创建Table会新建一个region。执行批量导入时所有的client会写入这个region,直到这个region足够大,以至于分裂。
一个有效的提高批量导入的性能的方式,是预创建region。 createTable(HTableDescriptor desc, byte [][] splitKeys)预先分配空的Region, Region的个数、startkey、endkey由splitkeys数组决定,Region数不宜过多否则可能降低性能。
HTableDescriptor 代表的是表的schema, 提供的方法主要有
setMaxFileSize,指定最大的region size
setMemStoreFlushSize 指定memstore flush到HDFS上的文件大小,默认是64M
增加family通过 addFamily方法,至少需要一个列簇,但是也不要太多, Hbase目前不能良好的处理超过2-3个CF的表。因为某个CF在flush发生时,它邻近的CF也会因关联效应被触发flush,最终导致系统产生很多IO。
public void addFamily(final HColumnDescriptor family)
HColumnDescriptor 代表的是column的schema,提供的方法主要有
setTimeToLive:指定最大的TTL,单位是ms,过期数据会被自动删除。
setInMemory:指定是否放在内存中,对小表有用,可用于提高效率。默认关闭
setBloomFilter:指定是否使用BloomFilter,可提高随机查询效率。默认关闭
setCompressionType:设定数据压缩类型。默认无压缩。
setScope(scope):集群的Replication,默认为flase
setBlocksize(blocksize); block的大小默认是64kb,block小适合随机读,但是可能导Index过大而使内存oom, block大利于顺序读。
setMaxVersions:指定数据最大保存的版本个数。默认为3。版本数最多为Integer.MAX_VALUE, 但是版本数过多可能导致compact时out of memory。
setBlockCacheEnabled:是否可以cache, 默认设置为true,将最近读取的数据所在的Block放入内存中,标记为single,若下次读命中则将其标记为multi
一个简单的例子,创建了拥有2个family的表:
Example 2.2:
Public static void createTable(String tablename, HBaseAdmin admin) throws IOException{
if (admin.tableExists(tablename)) {
System.out.println("table existed delete it or use another tablename");
} else {
HTableDescriptor tableDesc = new HTableDescriptor(tablename);
HTableDescriptor tableDesc = new HTableDescriptor(tablename);
HColumnDescriptor column = new HColumnDescriptor("cf1".getBytes()); column.setInMemory(true); column.setMaxVersions(100);
tableDesc.addFamily(column)
tableDesc.addFamily(new HColumnDescriptor("cf2".getBytes()));
admin.createTable(tableDesc);
System.out.println("create table ok.");
}
}
Family数目不宜过多;如果对于某些热点数据,而且数据不是很大的话可以将其放入inMemory中,可以提高读性能;对于随机读有要求可以开启BlooFilter,提高效率;另Blocksize的大小则因情况而异,小的适合随机读但是对内存压力大,大的Blocksize适合顺序读,随机读性能一般;此外版本数不可设置过多,BlockCached最好置为true,存放于缓存提高读性能。
2.3 删除表
删除表也是通过HBaseAdmin来操作,删除表之前首先要disable表。这是一个非常耗时的操作,所以不建议频繁删除表。
disableTable和deleteTable分别用来disable和delete表。
Example 2.3:
Public static void deleteTable(String tablename, HBaseAdmin admin){
if (hAdmin.tableExists(tableName)) {
hAdmin.disableTable(tableName);
hAdmin.deleteTable(tableName);
}
}
2.2与2.3主要是通过Master操作,那么master的主要功能就是:
1. 为Regionserver分配region如create table;
2. 负责region server的负载均衡,当Regionserver的region不均衡时很可能导致性能下降,此时master会定时进行负载均衡,分配region到合适的地方,从而达到负载均衡的目的。客户端提供了balancer()接口手动balance;
3. 发现失效的region server并重新分配其上的region,当RegionServer down掉以后master监听到该事件(通过zookeeper),然后master重新分配regionserver上面的region,整个过程对客户端是透明的;
4. 垃圾文件回收,如.oldlogs等;
5. 处理schema更新请求,详见2.8。
2.4 查询数据
查询分为单条随机查询和批量查询。
单条查询是通过rowkey在table中查询某一行的数据。HTable提供了get方法来完成单条查询。
批量查询是通过制定一段rowkey的范围来查询。HTable提供了个getScanner方法来完成批量查询。 public Result get(final Get get) public ResultScanner getScanner(final Scan scan)
Get对象包含了一个Get查询需要的信息。它的构造方法有两种: public Get(byte [] row) public Get(byte [] row, RowLock rowLock)
Rowlock是为了保证读写的原子性,你可以传递一个已经存在Rowlock,否则HBase会自动生成一个新的rowlock。
Scan对象提供了默认构造函数,一般使用默认构造函数。
Get/Scan的常用方法有:
addFamily/addColumn:指定需要的family或者column,如果没有调用任何addFamily或者Column,会返回所有的columns.
setMaxVersions:指定最大的版本个数。如果不带任何参数调用setMaxVersions,表示取所有的版本。如果不调用setMaxVersions,只会取到最新的版本。
setTimeRange:指定最大的时间戳和最小的时间戳,只有在此范围内的cell才能被获取。
setTimeStamp:指定时间戳。
setFilter:指定Filter来过滤掉不需要的信息
Example //通过row获取data
public static void getbyrow(HTable table, String row) throws IOException {
Get get=new Get(row.getBytes());
Result r = table.get(get);
print(r);
}
}
public static void print(Result r){
For(KeyValue kv: r.raw()){
System.out.print("rowkey : " + new String(kv.getRow()) + " "); System.out.println(new String(kv.getFamily()) + ":"+ new String(kv.getQualifier()) + " = " + new String(kv.getValue()));
}
}
//指定row、family、column来获取data, 也可以指定读取指定的版本或是指定的版本数
public static void getbycolumn(String tablename, String row, String family,String column, long time, int num) throws IOException {
HTable table = new HTable(hbaseConfig, tablename);
Get get = new Get(row.getBytes()); get.addColumn(family.getBytes(), column.getBytes()); // 指定版本 if(num > 1){
get.setTimestamp(time);
}
//返回多版本 if (time != 0){
get.setMaxVersions(num);
}
Result r=table.get(get); print(r);
}
Scan特有的方法:
setStartRow:指定开始的行。如果不调用,则从表头开始。
setStopRow:指定结束的行(不含此行)。
setBatch:指定最多返回的Cell数目。用于防止一行中有过多的数据,导致OutofMemory错误。
ResultScanner是Result的一个容器,每次调用ResultScanner的next方法,会返回Result. public Result next() throws IOException;
public Result [] next(int nbRows) throws IOException;
Result代表是一行的数据。常用方法有:
getRow:返回rowkey
raw:返回所有的key value数组。
getValue:按照column来获取cell的值
Example: public static void getAllData(HTable table) throws IOException {
Scan s = new Scan();
s.setMaxVersions();
// 设置scan的版本数,若不设置,则默认返回最新的一个版本ResultScanner
rs = table.getScanner(s);
for (Result r : rs)
{ print(r); }
}
2.5 插入数据
HTable通过put方法来插入数据。
可以传递单个Put对象或者Listput对象来分别实现单条插入和批量插入 public void put(final Put put) throws IOException public void put(final List<Put> puts) throws IOException
Put提供了3种构造方式: public Put(byte [] row) public Put(byte [] row, RowLock rowLock) public Put(Put putToCopy)
Put常用的方法有:
add:增加一个Cell
setTimeStamp:指定所有cell默认的timestamp,如果一个Cell没有指定timestamp,就会用到这个值。如果没有调用,HBase会将当前时间作为未指定timestamp的cell的timestamp.
setWriteToWAL: WAL是Write Ahead Log的缩写,指的是HBase在插入操作前是否写Log。默认是打开,关掉会提高性能,但是如果系统出现故障(负责插入的Region Server挂掉),数据可能会丢失。
另外HTable也有两个方法也会影响插入的性能
setAutoFlash:
AutoFlush指的是在每次调用HBase的Put操作,是否提交到HBase Server。默认是true,每次会提交。如果此时是单条插入,就会有更多的IO,从而降低性能。进行大量Put时,HTable的setAutoFlush最好设置为flase。否则每执行一个Put就需要和RegionServer发送一个请求。如果autoFlush = false,会等到写缓冲填满才会发起请求。显式的发起请求,可以调用flushCommits。HTable的close操作也会发起flushCommits
setWriteBufferSize:
Write Buffer Size在AutoFlush为false的时候起作用,默认是2MB,也就是当插入数据超过2MB,就会自动提交到Server
此外HTable不是线程安全的,因此当多线程插入数据的时候推荐使用HTablePool
Example 2.5:
public static void addData(String tablename, HTable table) throws IOException {
ArrayList<Put> newput = new ArrayList<Put>();
Put newput1 = new Put("r1".getBytes());
newput1.add("cf1".getBytes(), "v1".getBytes(), 1, "1".getBytes()); newput1.add("cf1".getBytes(), "v1".getBytes(), 100, "0".getBytes());
newput1.add("cf1".getBytes(), "v1".getBytes(), "7".getBytes()); newput1.add("cf2".getBytes(), "v2".getBytes(), 2, "1".getBytes()); newput1.add("cf2".getBytes(), "v2".getBytes(), 300, "2".getBytes()); newput1.add("cf2".getBytes(), "v2".getBytes(), 300, "2".getBytes()); newput1.add("cf2".getBytes(), "v2".getBytes(), "5".getBytes()); Put newput2 = new Put("r2".getBytes());
newput2.add("cf1".getBytes(), "v3".getBytes(), 4, "4".getBytes()); newput2.add("cf1".getBytes(), "v3".getBytes(), 6, "6".getBytes()); newput2.add("cf1".getBytes(), "v1".getBytes(), 8, "8".getBytes()); newput2.add("cf1".getBytes(), "v1".getBytes(), 10, "10".getBytes());
newput.add(newput1); newput.add(newput2);
table.put(newput);
table.flushCommits();
System.out.println("add data ok.");
}
2.6 删除数据
HTable 通过delete方法来删除数据。
public void delete(final Delete delete)
Delete构造方法有:
public Delete(byte [] row) public Delete(byte [] row, long timestamp, RowLock rowLock)
public Delete(final Delete d)
Delete常用方法有
deleteFamily/deleteColumns:指定要删除的family或者column的数据。如果不调用任何这样的方法,将会删除整行。
public void setTimestamp(long timestamp): 指明删除的版本。若没有指明,默认情况下是删除比当前时间早的版本。如果某个Cell的timestamp高于当前时间,这个Cell将不会被删除,仍然可以查出来。
删除操作的实现是创建一个删除标记。例如,我们想要删除一个版本,或者默认是currentTimeMillis。就意味着“删除比这个版本更早的所有版本”。Hbase在compact之前不会去改那些数据,数据不会立即从文件中删除。他使用删除标记来屏蔽掉这些值。若你知道的版本比数据中的版本晚,就意味着这一行中的所有数据都会被删除。
Example 2.6:
//按row来删除data
public static void deletebyRow(HTable , String row) throws IOException {
Delete delete = new Delete(row.getBytes());
table.delete(delete);
}
//指定family或是指定column或指定版本的column删除
public static void deleteColumn(HTable table,String row, String family ,String column, long version) throws IOException {
Delete delete = new Delete(row.getBytes());
// 删除列簇 if (column == null)
delete.deleteFamily(family.getBytes());
else if (version == 0) // 删除列
delete.deleteColumn(family.getBytes(), column.getBytes());
else
// 删除指定版本列
delete.deleteColumn(family.getBytes(), column.getBytes(), version); table.delete(delete);
}
2.4,、2.5和2.6是对数据的读写过程,整个过程中,Client首先会从zk上获取root表地址,然后和RegionServer交互得到meta表的地址,从meta表中获取region的信息,并把地址cache在客户端。整个过程不与master交互,故master的状态与数据的读写没有关系。client访问hbase上数据的过程并不需要master参与(寻址访问zookeeper和region server,数据读写访问regione server),master仅仅维护table和region的元数据信息,负载很低。 Region server维护Master分配给它的region,处理对这些region的IO请求,Region server负责切分在运行过程中变得过大的region。因此在读写的过程中,若想要达到分布式的效果,应当尽量的把IO请求分散到每一个Regionserver。
2.7 compact表
HBaseAdmin提供compact方法来手动合并小文件
public void compact(final byte [] tableNameOrRegionName)
public void majorCompact(final byte [] tableNameOrRegionName)
majorCompact会对所有的文件进行Compact,而compact会选取合适的进行compact
2.8 others
HBase客户端还提供了很多其他的Api,如:
Modefy表的cf结构 public void modifyColumn(final byte [] tableName, final byte []columnName,HColumnDescriptor descriptor)
删除/增加表中的一个cf public void addColumn(final byte [] tableName, HColumnDescriptor column) public void deleteColumn(final byte [] tableName, HColumnDescriptor column)
不过目前在modify/delete/add之前需要disable table
3. Some advice
3.1 rowkey的设计
row key是用来检索记录的主键。访问hbase Table中的行有以下几种方式:通过单个row key访问、通过row key的range、全表扫描。row key可以是任意字符串(最大长度是 64KB,实际应用中长度一般为 10-100bytes),保存为字节数组。 存储时,数据按照Row key的字典序(byte order)存储。读取的时候是以一个Block为单位读取的。因此设计key时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。 因为hbase是按rowKey连续存储的,因此如写入数据时rowKey是连续的,那么就会造成写的压力会集中在单台region server上,因此应用在设计rowKey时,要尽可能的保证写入是分散的,当然,这可能会对有连续读需求的场景产生一定的冲击。 HBase文档里面提到对于如何快速地定位最近版本的数据时,将时间戳reverse,作为key的一部分(如将Long.MAX_VALUE – timestamp作为key的末尾)从而可以提高读性能。
3.2 Some suggestion
HTable的setAutoFlush设置为flase,可以提高写性能,不过缺点就是客户端宕机的时候这部分缓存的数据可能未写入而丢失。
建表时family不宜过多,因为某个CF在flush发生时,它邻近的CF也会因关联效应被触发flush,最终导致系统产生很多IO。
版本数不宜过多可能导致compact时out of memory。
因为读数据时会从storefile寻找row,如果sotrefile数目过多,会影响读的性能。而Compact的时候会把一些storefiles文件读到内存里面,写入一个新文件从而减少Region的storefile数目。在Compact过程中对IO消耗是比较大的,尤其是major compact的时候会对所有的storefile进行compact。
Split是对Region进行切分HBase。通过将region切分在许多机器上实现分布式。也就是说,你如果有16GB的数据,只分了2个region, 你却有20台机器,有18台就浪费了。然如果Region太小了可能导致Region数目过多,同样也会对性能有影响。
Split过程中会让region短暂的下线,在这个过程中会阻塞读和写,因此频繁的split是不建议的,默认region大小达到256M时会发出split信号,而这极有可能会使split概率很大。目前可以推荐的一种做法是把split的阈值调高,以防止server自动进行split,仅当需要split的时候采用手工的方式进行切割。
对读的速度影响比较大的因素主要是:请求次数的分布均衡、StoreFile数量、BloomFilter是否打开、Cache大小以及命中率。
对写的速度影响比较大的因素主要是:请求次数的分布均衡、是否出现Blocking Update或Delaying flush、HLog数量、DataNode数量、Split File Size。