Bulk Loading
向hbase写入数据常用两种方式:
- 客户端 API写入
- Mapreduce任务TableOutputFormat格式输出
然而,对于超大量的数据写入,这两种方式都不合适,会非常占用内存和耗时而且JVM GC和Hbase compaction都会急剧增加。为此,Hbase提供了另一种很好的解决方式——Bulk Loading,顾名思义就是批量导入,它通过预生成Hbase存储格式文件Hfile然后直接拷贝到对应RegionServer的方式减少导入时间和集群性能损耗。
包括如下几步:
- 从数据库或其他源抽取待写入数据,写入HBase所在HDFS集群
- 数据预生成为HFile文件,注意生成的HFile文件和导入HDFS的文件差不多大,主要保证集群空间足够大
- 导入数据到HBase
后两步又分为不同场景处理:
- 对于Hbase列不变的表,可以使用预定义工具importtsv来完成整个过程
- 但是对于列一直在动态变化的表,必须自己写MR程序来完成整个过程
Importtsv
使用importtsv工具生成HFile必须保证导入HDFS的文件内容格式为tsv,也就是每行内容是使用相同分隔符分割的同字段数的内容,第一字段对应row key,后面每列对应hbase表的一字段(Qualifier),类似如下:
row1^v11^v12^v13
row2^v21^v22^v23
row3^v31^v32^v33
生成HFile
首先,如下生成HFile,参数表示从HDFS处$input读入数据到表$table中,数据以^分割,对应的数据表字段名为依次为c1、c2、c3,生成的HFile保存在$output处。
${HADOOP_HOME}/bin/hadoop jar ${HBASE_HOME}/lib/hbase-mapreduce-2.0.4.4.jar importtsv \
-Dimporttsv.columns=HBASE_ROW_KEY,b:c1,b:c2,b:c3 \
-Dimporttsv.separator=^ \
-Dimporttsv.bulk.output=$output \
$table $input \
>> $log_file 2>&1
导入HBase
然后,如下将生成的HFile导入Hbase 表$table中。
${HADOOP_HOME}/bin/hadoop jar ${HBASE_HOME}/lib/hbase-mapreduce-2.0.4.4.jar completebulkload $output $table >> log/${table}-load.log 2>&1
自定义MR
使用importtsv固然很好,但是如果导入的数据列名是动态变化的,比如如下每行数据对应的hbase 字段名和个数都不一样,这种情况就没法用importtsv来处理,需要自己写MR来处理生成HFile和加载到HBase。
row1 (k1,v11) (k2,v12)
row2 (k2,v22) (k3,v23) (k4,v24)
row3 (k3,v31)
生成HFile
这种情况需要自行考虑MR的输入,因为具体每列数据对应的字段名到底是什么,是没办法像importtsv一样提前指定的,因此必须在输入数据中包含此信息,比如如下,用|分别分割行键、字段名列表、对应值列表。
row1|k1^k2|v11^v22
row2|k2^k3^k4|v22^c23^v24
row3|k3|v31
- 设定格式
这里为了生成HFile格式,MR的输入输出和中间map结果设置如下,最终生成文件格式为HFileOutputFormat2。
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(HFileOutputFormat2.class);
job.setMapOutputKeyClass(ImmutableBytesWritable.class);
job.setMapOutputValueClass(Put.class);
- 设定 Mapper
任务的Mapper部分主要完成读入并拆分数据,生成指定格式中间文件即可,如下,按照之前说的数据输入,先按照|拆分出三部分,按照rowKey,colKey和colVal填入put中写入文件即可,这里的列族COL_FAMILY是强制指定的。
public void map(LongWritable key, Text value, Mapper<LongWritable, Text, ImmutableBytesWritable, Put>.Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] list = line.split("\\|", -1);
long length = list.length;
//每行数据输入格式为rowkey|c1^c2^c3|v1^v2^v3
if( length != 3 || list[1].isEmpty() || list[2].isEmpty()) {
return;
}
String Rowkey=list[0];
String[] colKey = list[1].split("\\^", -1);
String[] colVal = list[2].split("\\^", -1);
if (colKey.length!=colVal.length){
return;
}
long colLen = colKey.length;
//拼装rowkey和put
ImmutableBytesWritable PutRowkey=new ImmutableBytesWritable(Rowkey.getBytes());
Put put=new Put(Rowkey.getBytes());
for (int i= 0; i<colLen; i++){
if (!colVal[i].isEmpty()){
put.addColumn(COL_FAMILY.getBytes(), colKey[i].getBytes(), colVal[i].getBytes());
}
}
context.write(PutRowkey, put);
}
- 设定Reducer
任务的Reducer不用自己指定,如下HFileOutputFormat2.configureIncrementalLoad会根据hbase对应表信息设置reducer和对应的reducer 个数。
//配置MapReduce作业,以执行增量加载到给定表中
Configuration hbaseConf=HBaseConfiguration.create();
hbaseConf.addResource(new Path("/home/wenzhou/hbase/conf/hbase-site.xml"));
Connection conn=ConnectionFactory.createConnection(hbaseConf);
Table table=conn.getTable(TableName.valueOf(tname));
Admin admin=conn.getAdmin();
HFileOutputFormat2.configureIncrementalLoad(job, table, conn.getRegionLocator(TableName.valueOf(tname)));
导入HBase
确认上述数据生成成功后,如下即可加载到hbase表中。
//生成的HFile Bulkload导入
LoadIncrementalHFiles loader = new LoadIncrementalHFiles(hbaseConf);
loader.doBulkLoad(new Path(outputReal), admin, table, conn.getRegionLocator(TableName.valueOf(tname)));
注意事项
- 实际reducer个数和表的region个数相同,大量数据首次导入必须预分区,否则默认只有一个redion对应一个reducer,很容易Mem Overflow且数据倾斜厉害导致耗时很长
源码下载
对应源码下载链接,可直接用于生产环境
开发参考文档
原创,转载请注明来自