【转载】HBase 数据库检索性能优化策略

转自:http://www.ibm.com/developerworks/cn/java/j-lo-HBase/index.htmlhtml

高性能 HBase 数据库java

本文首先介绍了 HBase 数据库基本原理及专用术语,而后介绍了 HBase 数据库发布的操做 API 及部分示例,重点介绍了 Scan 方法的操做方式,接着介绍了检索 HBase 数据库时的优化方案,最后经过一个示例总结了实际项目中遇到的检索速度慢的解决方案。程序员

HBase 数据表介绍

HBase 数据库是一个基于分布式的、面向列的、主要用于非结构化数据存储用途的开源数据库。其设计思路来源于 Google 的非开源数据库”BigTable”。数据库

HDFS 为 HBase 提供底层存储支持,MapReduce 为其提供计算能力,ZooKeeper 为其提供协调服务和 failover(失效转移的备份操做)机制。Pig 和 Hive 为 HBase 提供了高层语言支持,使其能够进行数据统计(可实现多表 join 等),Sqoop 则为其提供 RDBMS 数据导入功能。apache

HBase 不能支持 where 条件、Order by 查询,只支持按照主键 Rowkey 和主键的 range 来查询,可是能够经过 HBase 提供的 API 进行条件过滤。编程

HBase 的 Rowkey 是数据行的惟一标识,必须经过它进行数据行访问,目前有三种方式,单行键访问、行键范围访问、全表扫描访问。数据按行键的方式排序存储,依次按位比较,数值较大的排列在后,例如 int 方式的排序:1,10,100,11,12,2,20…,906,…。数组

ColumnFamily 是“列族”,属于 schema 表,在建表时定义,每一个列属于一个列族,列名用列族做为前缀“ColumnFamily:qualifier”,访问控制、磁盘和内存的使用统计都是在列族层面进行的。缓存

Cell 是经过行和列肯定的一个存储单元,值以字节码存储,没有类型。sass

Timestamp 是区分不一样版本 Cell 的索引,64 位整型。不一样版本的数据按照时间戳倒序排列,最新的数据版本排在最前面。服务器

Hbase 在行方向上水平划分红 N 个 Region,每一个表一开始只有一个 Region,数据量增多,Region 自动分裂为两个,不一样 Region 分布在不一样 Server 上,但同一个不会拆分到不一样 Server。

Region 按 ColumnFamily 划分红 Store,Store 为最小存储单元,用于保存一个列族的数据,每一个 Store 包括内存中的 memstore 和持久化到 disk 上的 HFile。

图 1 是 HBase 数据表的示例,数据分布在多台节点机器上面。

图 1. HBase 数据表示例
图 1. HBase 数据表示例
 

HBase 调用 API 示例

相似于操做关系型数据库的 JDBC 库,HBase client 包自己提供了大量能够供操做的 API,帮助用户快速操做 HBase 数据库。提供了诸如建立数据表、删除数据表、增长字段、存入数据、读取数据等等接口。清单 1 提供了一个做者封装的工具类,包括操做数据表、读取数据、存入数据、导出数据等方法。

清单 1.HBase API 操做工具类代码
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.util.Bytes;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class HBaseUtil {
private Configuration conf = null;
private HBaseAdmin admin = null;

protected HBaseUtil(Configuration conf) throws IOException {
 this.conf = conf;
 this.admin = new HBaseAdmin(conf);
}

public boolean existsTable(String table)
 throws IOException {
 return admin.tableExists(table);
}

public void createTable(String table, byte[][] splitKeys, String... colfams)
 throws IOException {
HTableDescriptor desc = new HTableDescriptor(table);
for (String cf : colfams) {
HColumnDescriptor coldef = new HColumnDescriptor(cf);
desc.addFamily(coldef);
 }
if (splitKeys != null) {
admin.createTable(desc, splitKeys);
} else {
admin.createTable(desc);
 }
}

public void disableTable(String table) throws IOException {
admin.disableTable(table);
}

public void dropTable(String table) throws IOException {
 if (existsTable(table)) {
 disableTable(table);
 admin.deleteTable(table);
 }
}
 
public void fillTable(String table, int startRow, int endRow, int numCols,
 int pad, boolean setTimestamp, boolean random,
 String... colfams) throws IOException {
 HTable tbl = new HTable(conf, table);
 for (int row = startRow; row <= endRow; row++) {
 for (int col = 0; col < numCols; col++) {
 Put put = new Put(Bytes.toBytes("row-"));
 for (String cf : colfams) {
 String colName = "col-";
 String val = "val-";
 if (setTimestamp) {
 put.add(Bytes.toBytes(cf), Bytes.toBytes(colName),
 col, Bytes.toBytes(val));
 } else {
 put.add(Bytes.toBytes(cf), Bytes.toBytes(colName),
 Bytes.toBytes(val));
 }
 }
 tbl.put(put);
 }
 }
 tbl.close();
 }

public void put(String table, String row, String fam, String qual,
 String val) throws IOException {
 HTable tbl = new HTable(conf, table);
 Put put = new Put(Bytes.toBytes(row));
 put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), Bytes.toBytes(val));
 tbl.put(put);
 tbl.close();
 }

 public void put(String table, String row, String fam, String qual, long ts,
 String val) throws IOException {
 HTable tbl = new HTable(conf, table);
 Put put = new Put(Bytes.toBytes(row));
 put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), ts, Bytes.toBytes(val));
 tbl.put(put);
 tbl.close();
 }

 public void put(String table, String[] rows, String[] fams, String[] quals,
 long[] ts, String[] vals) throws IOException {
 HTable tbl = new HTable(conf, table);
 for (String row : rows) {
 Put put = new Put(Bytes.toBytes(row));
 for (String fam : fams) {
 int v = 0;
 for (String qual : quals) {
 String val = vals[v < vals.length ? v : vals.length];
 long t = ts[v < ts.length ? v : ts.length - 1];
 put.add(Bytes.toBytes(fam), Bytes.toBytes(qual), t,
 Bytes.toBytes(val));
 v++;
 }
 }
 tbl.put(put);
 }
 tbl.close();
 }

 public void dump(String table, String[] rows, String[] fams, String[] quals)
 throws IOException {
 HTable tbl = new HTable(conf, table);
 List<Get> gets = new ArrayList<Get>();
 for (String row : rows) {
 Get get = new Get(Bytes.toBytes(row));
 get.setMaxVersions();
 if (fams != null) {
 for (String fam : fams) {
 for (String qual : quals) {
 get.addColumn(Bytes.toBytes(fam), Bytes.toBytes(qual));
 }
 }
 }
 gets.add(get);
 }
 Result[] results = tbl.get(gets);
 for (Result result : results) {
 for (KeyValue kv : result.raw()) {
 System.out.println("KV: " + kv +
 ", Value: " + Bytes.toString(kv.getValue()));
 }
 }
 }
 
 private static void scan(int caching, int batch) throws IOException {
 HTable table = null;
 final int[] counters = {0, 0};

 Scan scan = new Scan();
 scan.setCaching(caching); // co ScanCacheBatchExample-1-Set Set caching and batch parameters.
 scan.setBatch(batch);
 ResultScanner scanner = table.getScanner(scan);
 for (Result result : scanner) {
 counters[1]++; // co ScanCacheBatchExample-2-Count Count the number of Results available.
 }
 scanner.close();
 System.out.println("Caching: " + caching + ", Batch: " + batch +
 ", Results: " + counters[1] + ", RPCs: " + counters[0]);
 }
}

操做表的 API 都有 HBaseAdmin 提供,特别讲解一下 Scan 的操做部署。

HBase 的表数据分为多个层次,HRegion->HStore->[HFile,HFile,...,MemStore]。

在 HBase 中,一张表能够有多个 Column Family,在一次 Scan 的流程中,每一个 Column Family(Store) 的数据读取由一个 StoreScanner 对象负责。每一个 Store 的数据由一个内存中的 MemStore 和磁盘上的 HFile 文件组成,对应的 StoreScanner 对象使用一个 MemStoreScanner 和 N 个 StoreFileScanner 来进行实际的数据读取。

所以,读取一行的数据须要如下步骤:

1. 按照顺序读取出每一个 Store

2. 对于每一个 Store,合并 Store 下面的相关的 HFile 和内存中的 MemStore

这两步都是经过堆来完成。RegionScanner 的读取经过下面的多个 StoreScanner 组成的堆完成,使用 RegionScanner 的成员变量 KeyValueHeap storeHeap 表示。一个 StoreScanner 一个堆,堆中的元素就是底下包含的 HFile 和 MemStore 对应的 StoreFileScanner 和 MemStoreScanner。堆的优点是建堆效率高,能够动态分配内存大小,没必要事先肯定生存周期。

接着调用 seekScanners() 对这些 StoreFileScanner 和 MemStoreScanner 分别进行 seek。seek 是针对 KeyValue 的,seek 的语义是 seek 到指定 KeyValue,若是指定 KeyValue 不存在,则 seek 到指定 KeyValue 的下一个。

Scan类经常使用方法说明

scan.addFamily()/scan.addColumn():指定须要的 Family 或 Column,若是没有调用任何 addFamily 或 Column,会返回全部的 Columns;

scan.setMaxVersions():指定最大的版本个数。若是不带任何参数调用 setMaxVersions,表示取全部的版本。若是不掉用 setMaxVersions,只会取到最新的版本.;

scan.setTimeRange():指定最大的时间戳和最小的时间戳,只有在此范围内的 Cell 才能被获取;

scan.setTimeStamp():指定时间戳;

scan.setFilter():指定 Filter 来过滤掉不须要的信息;

scan.setStartRow():指定开始的行。若是不调用,则从表头开始;

scan.setStopRow():指定结束的行(不含此行);

scan. setCaching():每次从服务器端读取的行数(影响 RPC);

scan.setBatch():指定最多返回的 Cell 数目。用于防止一行中有过多的数据,致使 OutofMemory 错误,默认无限制。

 

HBase 数据表优化

HBase 是一个高可靠性、高性能、面向列、可伸缩的分布式数据库,可是当并发量太高或者已有数据量很大时,读写性能会降低。咱们能够采用以下方式逐步提高 HBase 的检索速度。

预先分区

默认状况下,在建立 HBase 表的时候会自动建立一个 Region 分区,当导入数据的时候,全部的 HBase 客户端都向这一个 Region 写数据,直到这个 Region 足够大了才进行切分。一种能够加快批量写入速度的方法是经过预先建立一些空的 Regions,这样当数据写入 HBase 时,会按照 Region 分区状况,在集群内作数据的负载均衡。

Rowkey 优化

HBase 中 Rowkey 是按照字典序存储,所以,设计 Rowkey 时,要充分利用排序特色,将常常一块儿读取的数据存储到一块,将最近可能会被访问的数据放在一块。

此外,Rowkey 如果递增的生成,建议不要使用正序直接写入 Rowkey,而是采用 reverse 的方式反转 Rowkey,使得 Rowkey 大体均衡分布,这样设计有个好处是能将 RegionServer 的负载均衡,不然容易产生全部新数据都在一个 RegionServer 上堆积的现象,这一点还能够结合 table 的预切分一块儿设计。

减小ColumnFamily 数量

不要在一张表里定义太多的 ColumnFamily。目前 Hbase 并不能很好的处理超过 2~3 个 ColumnFamily 的表。由于某个 ColumnFamily 在 flush 的时候,它邻近的 ColumnFamily 也会因关联效应被触发 flush,最终致使系统产生更多的 I/O。

缓存策略 (setCaching)

建立表的时候,能够经过 HColumnDescriptor.setInMemory(true) 将表放到 RegionServer 的缓存中,保证在读取的时候被 cache 命中。

设置存储生命期

建立表的时候,能够经过 HColumnDescriptor.setTimeToLive(int timeToLive) 设置表中数据的存储生命期,过时数据将自动被删除。

硬盘配置

每台 RegionServer 管理 10~1000 个 Regions,每一个 Region 在 1~2G,则每台 Server 最少要 10G,最大要 1000*2G=2TB,考虑 3 备份,则要 6TB。方案一是用 3 块 2TB 硬盘,二是用 12 块 500G 硬盘,带宽足够时,后者能提供更大的吞吐率,更细粒度的冗余备份,更快速的单盘故障恢复。

分配合适的内存给 RegionServer 服务

在不影响其余服务的状况下,越大越好。例如在 HBase 的 conf 目录下的 hbase-env.sh 的最后添加 export HBASE_REGIONSERVER_OPTS="-Xmx16000m $HBASE_REGIONSERVER_OPTS”

其中 16000m 为分配给 RegionServer 的内存大小。

写数据的备份数

备份数与读性能成正比,与写性能成反比,且备份数影响高可用性。有两种配置方式,一种是将 hdfs-site.xml 拷贝到 hbase 的 conf 目录下,而后在其中添加或修改配置项 dfs.replication 的值为要设置的备份数,这种修改对全部的 HBase 用户表都生效,另一种方式,是改写 HBase 代码,让 HBase 支持针对列族设置备份数,在建立表时,设置列族备份数,默认为 3,此种备份数只对设置的列族生效。

WAL(预写日志)

可设置开关,表示 HBase 在写数据前用不用先写日志,默认是打开,关掉会提升性能,可是若是系统出现故障 (负责插入的 RegionServer 挂掉),数据可能会丢失。配置 WAL 在调用 Java API 写入时,设置 Put 实例的 WAL,调用 Put.setWriteToWAL(boolean)。

批量写

HBase 的 Put 支持单条插入,也支持批量插入,通常来讲批量写更快,节省来回的网络开销。在客户端调用 Java API 时,先将批量的 Put 放入一个 Put 列表,而后调用 HTable 的 Put(Put 列表) 函数来批量写。

客户端一次从服务器拉取的数量

经过配置一次拉去的较大的数据量能够减小客户端获取数据的时间,可是它会占用客户端内存。有三个地方可进行配置:

1)在 HBase 的 conf 配置文件中进行配置 hbase.client.scanner.caching;

2)经过调用 HTable.setScannerCaching(int scannerCaching) 进行配置;

3)经过调用 Scan.setCaching(int caching) 进行配置。三者的优先级愈来愈高。

RegionServer 的请求处理 IO 线程数

较少的 IO 线程适用于处理单次请求内存消耗较高的 Big Put 场景 (大容量单次 Put 或设置了较大 cache 的 Scan,均属于 Big Put) 或 ReigonServer 的内存比较紧张的场景。

较多的 IO 线程,适用于单次请求内存消耗低,TPS 要求 (每秒事务处理量 (TransactionPerSecond)) 很是高的场景。设置该值的时候,以监控内存为主要参考。

在 hbase-site.xml 配置文件中配置项为 hbase.regionserver.handler.count。

Region 大小设置

配置项为 hbase.hregion.max.filesize,所属配置文件为 hbase-site.xml.,默认大小 256M。

在当前 ReigonServer 上单个 Reigon 的最大存储空间,单个 Region 超过该值时,这个 Region 会被自动 split 成更小的 Region。小 Region 对 split 和 compaction 友好,由于拆分 Region 或 compact 小 Region 里的 StoreFile 速度很快,内存占用低。缺点是 split 和 compaction 会很频繁,特别是数量较多的小 Region 不停地 split, compaction,会致使集群响应时间波动很大,Region 数量太多不只给管理上带来麻烦,甚至会引起一些 Hbase 的 bug。通常 512M 如下的都算小 Region。大 Region 则不太适合常常 split 和 compaction,由于作一次 compact 和 split 会产生较长时间的停顿,对应用的读写性能冲击很是大。

此外,大 Region 意味着较大的 StoreFile,compaction 时对内存也是一个挑战。若是你的应用场景中,某个时间点的访问量较低,那么在此时作 compact 和 split,既能顺利完成 split 和 compaction,又能保证绝大多数时间平稳的读写性能。compaction 是没法避免的,split 能够从自动调整为手动。只要经过将这个参数值调大到某个很难达到的值,好比 100G,就能够间接禁用自动 split(RegionServer 不会对未到达 100G 的 Region 作 split)。再配合 RegionSplitter 这个工具,在须要 split 时,手动 split。手动 split 在灵活性和稳定性上比起自动 split 要高不少,并且管理成本增长很少,比较推荐 online 实时系统使用。内存方面,小 Region 在设置 memstore 的大小值上比较灵活,大 Region 则过大太小都不行,过大会致使 flush 时 app 的 IO wait 增高,太小则因 StoreFile 过多影响读性能。

HBase 配置

建议 HBase 的服务器内存至少 32G,表 1 是经过实践检验获得的分配给各角色的内存建议值。

表 1. HBase 相关服务配置信息
模块 服务种类 内存需求
HDFS HDFS NameNode 16GB
HDFS DataNode 2GB
HBase HMaster 2GB
HRegionServer 16GB
ZooKeeper ZooKeeper 4GB

HBase 的单个 Region 大小建议设置大一些,推荐 2G,RegionServer 处理少许的大 Region 比大量的小 Region 更快。对于不重要的数据,在建立表时将其放在单独的列族内,而且设置其列族备份数为 2(默认是这样既保证了双备份,又能够节约空间,提升写性能,代价是高可用性比备份数为 3 的稍差,且读性能不如默认备份数的时候。

实际案例

项目要求能够删除存储在 HBase 数据表中的数据,数据在 HBase 中的 Rowkey 由任务 ID(数据由任务产生) 加上 16 位随机数组成,任务信息由单独一张表维护。图 2 所示是数据删除流程图。

图 2. 数据删除流程图
图 2. 数据删除流程图

最初的设计是在删除任务的同时按照任务 ID 删除该任务存储在 HBase 中的相应数据。可是 HBase 数据较多时会致使删除耗时较长,同时因为磁盘 I/O 较高,会致使数据读取、写入超时。

查看 HBase 日志发现删除数据时,HBase 在作 Major Compaction 操做。Major Compaction 操做的目的是合并文件,并清除删除、过时、多余版本的数据。Major Compaction 时 HBase 将合并 Region 中 StoreFile,该动做若是持续长时间会致使整个 Region 都不可读,最终致使全部基于这些 Region 的查询超时。

若是想要解决 Major Compaction 问题,须要查看它的源代码。经过查看 HBase 源码发现 RegionServer 在启动时候,有个 CompactionChecker 线程在按期检测是否须要作 Compact。源代码如图 3 所示。

图 3. CompactionChecker 线程代码图
图 3. CompactionChecker 线程代码图

isMajorCompaction 中会根据 hbase.hregion.majorcompaction 参数来判断是否作 Major Compact。若是 hbase.hregion.majorcompaction 为 0,则返回 false。修改配置文件 hbase.hregion.majorcompaction 为 0,禁止 HBase 的按期 Major Compaction 机制,经过自定义的定时机制 (在凌晨 HBase 业务不繁忙时) 执行 Major 操做,这个定时能够是经过 Linux cron 定时启动脚本,也能够经过 Java 的 timer schedule,在实际项目中使用 Quartz 来启动,启动的时间配置在配置文件中给出,能够方便的修改 Major Compact 启动的时间。经过这种修改后,咱们发如今删除数据后仍会有 Compact 操做。这样流程进入 needsCompaction = true 的分支。查看 needsCompaction 判断条件为 (storefiles.size() - filesCompacting.size()) > minFilesToCompact 触发。同时当需紧缩的文件数等于 Store 的全部文件数,Minor Compact 自动升级为 Major Compact。可是 Compact 操做不能禁止,由于这样会致使数据一直存在,最终影响查询效率。

基于以上分析,咱们必须从新考虑删除数据的流程。对用户来讲,用户只要在检索时对于删除的任务不进行检索便可。那么只须要删除该条任务记录,对于该任务相关联的数据不须要立马进行删除。当系统空闲时候再去定时删除 HBase 数据表中的数据,并对 Region 作 Major Compact,清理已经删除的数据。经过对任务删除流程的修改,达到项目的需求,同时这种修改也不须要修改 HBase 的配置。

图 4. 数据删除流程对比图
图 4. 数据删除流程对比图

检索、查询、删除 HBase 数据表中的数据自己存在大量的关联性,须要查看 HBase 数据表的源代码才能肯定致使检索性能瓶颈的根本缘由及最终解决方案。

 

结束语

HBase 数据库的使用及检索优化方式均与传统关系型数据库存在较多不一样,本文从数据表的基本定义方式出发,经过 HBase 自身提供的 API 访问方式入手,举例说明优化方式及注意事项,最后经过实例来验证优化方案可行性。检索性能自己是数据表设计、程序设计、逻辑设计等的结合产物,须要程序员深刻理解后才能作出正确的优化方案。

参考资料

学习

  • 参考 developerWorks 中国关于 HBase 知识 检索页面,查看 IBM 开发者论坛公布的关于 HBase 的相关文章。
  • 查看文章“浅谈 HBase”,做者对于 HBase 数据表做了基础解释。
  • 查看书籍《HBase Definition》,做者为 HBase 创始人,对 HBase 数据库进行权威解答。
  • 查看博客“HBase杂谈”,做者有较多的实际经验。
  • developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。

讨论

  • 加入 developerWorks 中文社区,查看开发人员推进的博客、论坛、组和维基,并与其余 developerWorks 用户交流。
相关文章
相关标签/搜索