更新时间:2023-09-14 GMT+08:00
分享

规则

Configuration实例的创建

该类应该通过调用HBaseConfiguration的Create()方法来实例化。否则,将无法正确加载HBase中的相关配置项。

正确示例:

//该部分,应该是在类成员变量的声明区域声明
private Configuration hbaseConfig = null;
//最好在类的构造函数中,或者初始化方法中实例化该类
hbaseConfig = HBaseConfiguration.create();

错误示例:

hbaseConfig = new Configuration();

共享Configuration实例

HBase客户端代码通过创建一个与Zookeeper之间的HConnection,来获取与一个HBase集群进行交互的权限。一个Zookeeper的HConnection连接,对应着一个Configuration实例,已经创建的HConnection实例,会被缓存起来。也就是说,如果客户端需要与HBase集群进行交互的时候,会传递一个Configuration实例过去,HBase Client部分通过已缓存的HConnection实例,来判断属于这个Configuration实例的HConnection实例是否存在,如果不存在,就会创建一个新的,如果存在,就会直接返回相应的实例。

因此,如果频繁创建Configuration实例,会导致创建很多不必要的HConnection实例,很容易达到Zookeeper的连接数上限。

建议在整个客户端代码范围内,都共用同一个Configuration对象实例。

HTable实例的创建

HTable类有多种构造函数,如:

  1. public HTable(final String tableName)
  2. public HTable(final byte [] tableName)
  3. public HTable(Configuration conf, final byte [] tableName)
  4. public HTable(Configuration conf, final String tableName)
  5. public HTable(final byte[] tableName, final HConnection connection,

    final ExecutorService pool)

建议采用第5种构造函数。之所以不建议使用前面的4种,是因为:前两种方法实例化一个HTable时,没有指定Configuration实例,那么,在实例化的时候,就会自动创建一个Configuration实例。如果需要实例化过多的HTable实例,那么,就可能会出现很多不必要的HConnection(关于这一点,前面部分已经有讲述)。因此,而对于第3、4种构造方法,每个实例都可能会创建一个新的线程池,也可能会创建新的连接,导致性能偏低。

正确示例:

private HTable table = null;
public initTable(Configuration config, byte[] tableName)
{
// sharedConn和pool都已经是事先实例化好的。建议在一个进程中共享相同的connection和pool。
// 初始化HConnection的方法:
// HConnection sharedConn =
// HConnectionManager.createConnection(this.config);
table = new HTable(config, tableName, sharedConn, pool);
}

错误示例:

private HTable table = null;
public initTable(String tableName)
{
table = new HTable(tableName);
}
public initTable(byte[] tableName)
{
table = new HTable(tableName);
}

不允许多个线程在同一时间共用同一个HTable实例

HTable是一个非线程安全类,因此,同一个HTable实例,不应该被多个线程同时使用,否则可能会带来并发问题。

HTable实例缓存

如果一个HTable实例可能会被长时间且被同一个线程固定且频繁的用到,例如,通过一个线程不断的往一个表内写入数据,那么这个HTable在实例化后,就需要缓存下来,而不是每一次插入操作,都要实例化一个HTable对象(尽管提倡实例缓存,但也不是在一个线程中一直沿用一个实例,个别场景下依然需要重构,可参见下一条规则)。

正确示例:

注意该实例中提供的以Map形式缓存HTable实例的方法,未必通用。这与多线程多HTable实例的设计方案有关。如果确定一个HTable实例仅可能会被用于一个线程,而且该线程也仅有一个HTable实例的话,就无须使用Map。这里提供的思路仅供参考。

//该Map中以TableName为Key值,缓存所有已经实例化的HTable
private Map<String, HTable> demoTables = new HashMap<String, HTable>();
//所有的HTable实例,都将共享这个Configuration实例
private Configuration demoConf = null;
/**
* <初始化一个HTable类>
* <功能详细描述>
* @param tableName
* @return
* @throws IOException
* @see [类、类#方法、类#成员]
*/
private HTable initNewTable(String tableName) throws IOException
{
return new HTable(demoConf, tableName);
}
/**
* <获取HTable实例>
* <功能详细描述>
* @see [类、类#方法、类#成员]
*/
private HTable getTable(String tableName)
{
if (demoTables.containsKey(tableName))
{
return demoTables.get(tableName);
} else {
HTable table = null;
try
{
table = initNewTable(tableName);
demoTables.put(tableName, table);
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
return table;
}
}
/**
* <写数据>
* <这里未涉及到多线程多HTable实例在设计模式上的优化.这里只所以采用同步方法,
* 考虑到同一个HTable是非线程安全的。通常,建议一个HTable实例,在同一
* 时间只能被用在一个写数据的线程中>
* @param dataList
* @param tableName
* @see [类、类#方法、类#成员]
*/
public void putData(List<Put> dataList, String tableName)
{
HTable table = getTable(tableName);
//关于这里的同步:如果在采用的设计方案中,不存在多线程共用同一个HTable实例
//的可能的话,就无须同步了。这里需要注意的一点,就是HTable实例是非线程安全的
synchronized (table)
{
try
{
table.put(dataList);
table.notifyAll();
}
catch (IOException e)
{
                // 在捕获到IOE时,需要将缓存的实例重构。
try {
     // 关闭之前的Connection.
       table.close();
                  // 重新创建这个实例.
                  table = new HTable(this.config, "jeason");
} catch (IOException e1) {
// TODO
}
}
}
}

错误示例:

public void putDataIncorrect(List<Put> dataList, String tableName)
{
HTable table = null;
try
{
//每次写数据,都创建一个HTable实例
table = new HTable(demoConf, tableName);
table.put(dataList);
}
catch (IOException e1)
{
// TODO Auto-generated catch block
e1.printStackTrace();
}
finally
{
table.close();
}
}

HTable实例写数据的异常处理

尽管在前一条规则中提到了提倡HTable实例的重构,但是,并非提倡一个线程自始至终要沿用同一个HTable实例,当捕获到IOException时,依然需要重构HTable实例。示例代码可参考上一个规则的示例。

另外,勿轻易调用如下两个方法:

  • Configuration#clear:

    这个方法,会清理掉所有的已经加载的属性,那么,对于已经在使用这个Configuration的类或线程而言,可能会带来潜在的问题(例如,假如HTable还在使用这个Configuration,那么,调用这个方法后,HTable中的这个Configuration的所有的参数,都被清理掉了),也就是说:只要还有对象或者线程在使用这个Configuration,就不应该调用这个clear方法,除非,所有的类或线程,都已经确定不用这个Configuration了。那么,这个操作,可以在所有的线程要退出的时候来做,而不是每一次。

    因此,不要每次实例化一个HTable就调用此方法, 只有当所有线程都要结束时再调用。

  • HConnectionManager#deleteAllConnections:

    这个可能会导致现有的正在使用的连接被从连接集合中清理掉,同时,因为在HTable中保存了原有连接的引用,可能会导致这个连接无法关闭,进而可能会造成泄漏。因此,这个方法不建议使用。

写入失败的数据要做相应的处理

在写数据的过程中,如果进程异常或一些其它的短暂的异常,可能会导致一些写入操作失败。因此,对于操作的数据,需要将其记录下来。在集群恢复正常后,重新将其写入到HBase数据表中。

另外,有一点需要注意:HBase Client返回写入失败的数据,是不会自动重试的,仅会告诉接口调用者哪些数据写入失败了。对于写入失败的数据,一定要做一些安全的处理,例如可以考虑将这些失败的数据,暂时写在文件中,或者,直接缓存在内存中。

正确示例:

private List<Row> errorList = new ArrayList<Row>();
/**
* <采用PutList的模式插入数据>
* <如果不是多线程调用该方法,可不采用同步>
* @param put 一条数据记录
* @throws IOException
* @see [类、类#方法、类#成员]
*/
public synchronized void putData(Put put)
{
// 暂时将数据缓存在该List中
dataList.add(put);
// 当dataList的大小达到PUT_LIST_SIZE之后,就执行一次Put操作
if (dataList.size() >= PUT_LIST_SIZE)
{
try
{
demoTable.put(dataList);
}
catch (IOException e)
{
// 如果是RetriesExhaustedWithDetailsException类型的异常,
// 说明这些数据中有部分是写入失败的这通常都是因为
// HBase集群的进程异常引起,有时也会因为有大量
// 的Region正在被转移,导致尝试一定的次数后失败
if (e instanceof RetriesExhaustedWithDetailsException)
{
RetriesExhaustedWithDetailsException ree = 
  (RetriesExhaustedWithDetailsException)e;
int failures = ree.getNumExceptions();
for (int i = 0; i < failures; i++)
{
errorList.add(ree.getRow(i));
}
}
}
dataList.clear();
}
}

资源释放

关于ResultScanner和HTable实例,在用完之后,需要调用它们的Close方法,将资源释放掉。Close方法,要放在finally块中,来确保一定会被调用到。

正确示例:

ResultScanner scanner = null;
try
{
scanner = demoTable.getScanner(s);
//Do Something here.
}
finally
{
scanner.close();
}

错误示例:

  1. 在代码中未调用scanner.close()方法释放相关资源。
  2. scanner.close()方法未放置在finally块中。
    ResultScanner scanner = null;
    scanner = demoTable.getScanner(s);
    //Do Something here.
    scanner.close();

Scan时的容错处理

Scan时不排除会遇到异常,例如,租约过期。在遇到异常时,建议Scan应该有重试的操作。

事实上,重试在各类异常的容错处理中,都是一种优秀的实践,这一点,可以应用在各类与HBase操作相关的接口方法的容错处理过程中。

不用HBaseAdmin时,要及时关闭,HBaseAdmin实例不应常驻内存

HBaseAdmin的示例应尽量遵循 “用时创建,用完关闭”的原则。不应该长时间缓存同一个HBaseAdmin实例。

暂时不建议使用HTablePool获取HTable实例

因为当前的HTablePool实现中可能会带来泄露。创建HTable实例的方法,参考不允许多个线程在同一时间共用同一个HTable实例

多线程安全登录方式

如果有多线程进行login的操作,当应用程序第一次登录成功后,所有线程再次登录时应该使用relogin的方式。

login的代码样例:

  private Boolean login(Configuration conf){
    boolean flag = false;
    UserGroupInformation.setConfiguration(conf);
    
    try {
      UserGroupInformation.loginUserFromKeytab(conf.get(PRINCIPAL), conf.get(KEYTAB));
      System.out.println("UserGroupInformation.isLoginKeytabBased(): " +UserGroupInformation.isLoginKeytabBased());
      flag = true;
    } catch (IOException e) {
      e.printStackTrace();
    }
    return flag;
    
  }

relogin的代码样例:

public Boolean relogin(){
        boolean flag = false;
        try {
            
          UserGroupInformation.getLoginUser().reloginFromKeytab();
          System.out.println("UserGroupInformation.isLoginKeytabBased(): " +UserGroupInformation.isLoginKeytabBased());
          flag = true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return flag;
    }

分享:

    相关文档

    相关产品