hBase之HTable踩坑

刚发布完,异常暴增,报警电话响个不停,看了下异常信息,竟然是这货:


HTable线程不安全引发的异常

看到这异常第一反应就是,完了,HTable线程不安全,我们来看看报错的源码部分,看看对谁的操作线程不安全:


发生异常的源码部分

从源码的标记可以看出,异常发生在批量写入数据提交后,writeBuffer操作移除掉已经写入成功的数据时。从writeBuffer的声明:private final ArrayList<Put> writeBuffer = new ArrayList<Put>();可以看出,这货其实就是一个常量数组,当多个线程同时操作它时,就会出现线程不安全导致的数组越界问题。那这个问题要怎么解决呢?在想解决方案之前我们还是先来简单了解下HTable。

HTable

在hBase中,HTable是其客户端和服务端通信的java api对象,主要提供对表的put/get/delete/scan等操作,它的创建很简单:

Configuration conf = HBaseConfiguration.create();
HTable hTable = new HTable(conf, "tableName");

HTable主要提供以下常用方法操作表:

  • 自动的检查row/family/qualifier是否与给定的值匹配:
public boolean checkAndPut(final byte[] row, final byte[] family, final byte[] qualifier, final byte[] value, final Put put) throws IOException
  • 获取指定行某些单元格对应的值:
public Result get(final Get get) throws IOException
  • 获取当前打开的表每个区域的结束键值:
public byte[][] getEndKeys() throws IOException
  • 获取当前给定列族的scanner实例:
public ResultScanner getScanner(final Scan scan) throws IOException
  • 获取当前表的HTableDescriptor实例:
public HTableDescriptor getTableDescriptor() throws IOException
  • 获取表名:
public byte [] getTableName()
  • 添加值:
public void put(final Put put) throws IOException
  • 删除指定单元格/行:
void delete(Delete delete) throws IOException
  • 释放所有的资源或挂起内部缓冲区中的更新:
void close() throws IOException
  • 检查Get实例所指定的值是否存在于HTable的列中:
boolean exists(Get get) throws IOException;

在使用HTable时需要注意:

  1. 创建HTable对象耗时较高,耗时较高的主要原因是hBase客户端在创建好HTable对象后会进行一些列的校验,包括表是否存在,是否有效等等;

  2. 在构造多个HTable对象时,hBase推荐多个HTable对象共享Configuration,这样,HTable之间便可共享HConnection对象,zookeeper信息以及Region地址的缓存信息;

  3. HTable线程不安全,在多线程的场景下,线程一定不能共用HTable,一定要给每一个线程都创建一个HTable。

HTable的这些问题,HTablePool可以解决,它为HBase集群提供了客户端连接池。

HTablePool

跟ThreadPoolExecutor一样:

  • HTablePool屏蔽了HTable创建过程,避免了多线程间数据并发修改问题;

  • 维护固定数量的HTable对象,能够在程序运行期间复用这些HTable资源对象,减少了不断创建HTable对象带来的性能消耗;

  • HTablePool中的HTable对象之间是公用Configuration连接的,能够可以减少网络开销。

我们来个简单的例子看看HTablePool如何使用:

Configuration conf = HBaseConfiguration.create();
HTablePool pool = new HTablePool(conf, 10);
HTable hTable = (HTable) pool.getTable("tableName");
try {
      Put put = new Put(Bytes.toBytes("rowKey"));
      put.add(FAMILY, QUALIFIER, Bytes.toBytes("content"));
      hTable.put(put);
} finally {
      //释放资源
      hTable.close();
}

后记

到这里为止,本文最开始给出的问题的解决方案其实就有了:

  1. 使用HTablePool管理HTable,多个线程不要共用HTable对象;

  2. 每个线程使用完HTable对象后需要将资源释放后再放回HTablePool复用。

注:HTablePool是hBase连接池的老用法,该类在0.94、0.95和0.96版本中已不建议使用,并且在0.98.1版本以后已移除。本文选择HTablePool方案也是因为项目使用的hBase客户端版本较低。至于为什么要废弃掉HTablePool将会在后文做详细分析~

推荐阅读更多精彩内容