HBase之HFile解析
Sumary:
Protobuf
BinarySearch
本篇主要讲HFileV2的相关内容,包括HFile的构成、解析及怎么样从HFile中快速找到相关的KeyValue.基于Hbase 0.98.1-hadoop2,本文大部分参考了官方的资源,大家可以先阅读下这篇官方文档,Reference Guide:http://hbase.apache.org/book/apes03.html。其实也就是跟我们发行包内dos/book下的其中一篇。dos下有很多有用的文章,有时间的时候建议大家还是细读一下。
研究HFile也有一些时间了,源码也大概研究了下,做了不少试验,庖丁解牛远远谈不上,但是还是很详细地分享一下HFile的方方面面,像拆零件一样,把它一件一件地拆开看看,究竟是什么东西,怎么组织在一起的。
图1
这张图也是摘自上面那篇文章,主要分四部分:Scanned block section,Non-scanned block section,Load-on-open-section,以及Trailer.
Scanned block section: 即存储数据block部分
Non-scanned block section:元数据block部分,主要存放meta信息,即BloomFilter信息。
Load-on-open-section:这部分数据在RegionServer启动时,实例化Region并创建HStore时会将所有StoreFile的Load-on-open-section加载进内存,主要存放了Root Data Index,meta Index,File Info及BooleamFilter的metadata等。除了Fields for midkey外,每部分都是一个HFileBlock.下面会详细去讲这块。
Trailer:文件尾,主要记录version版本,不同的版本Trailer的字段不一样,及Trailer的字段相关信息。
在拆解HFile过程中,我们从下而上地开始分析,HBase本身也是这样,首先要知道Version版本,才知道怎么去加载它们。在开始讲解之前,我们应先获得一份HFile数据,其实很简单,直接从hdfs上下载到本地即可,我使用的数据是我上一篇文章中做测试生成的,10W rows, 70W KeyValue,26M左右。
Trailer:
文件最后4位,即一个整型数字,为version信息,我们知道是V2.而V2的Trailer长度为212字节。除去MagicCode(BlockType) 8字节及 Version 4字节外,剩余206字节记录了整个文件的一些重要的字段信息,而这些字段信息是由protobuf组成的,下面我们尝试山寨一把,自主解析下Trailer的所有信息。
实践1:
step1: 准备一份描述Trailer的Protobuf.
Hbase的源码包下,有一个hbase-protocol sub module.它包含了HBase的所有Protobuf,包括序列化要用到的实体及RPC的定义。我们找到HFile.proto,我们只选取一小部分
新建我们自已的Protobuf文件 : HFile.proto
option java_package = "com.bdifn.hbase.hfile.proto";
option java_outer_classname = "HFileProtos";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED; message FileTrailerProto {
optional uint64 file_info_offset = ; //fileInfo起始偏移量
optional uint64 load_on_open_data_offset = ; //加载到内存区域起始偏移量
optional uint64 uncompressed_data_index_size = ; //未压缩的数据索引大小
optional uint64 total_uncompressed_bytes = ; //KeyValue未压缩的总大小
optional uint32 data_index_count = ; //Root DataIndex 的个数,如果只有1级索引的话,往往也是datablock个数
optional uint32 meta_index_count = ; //元数据索引个数
optional uint64 entry_count = ; //KeyValue总个数
optional uint32 num_data_index_levels = ; //数据索引的级别,
optional uint64 first_data_block_offset = ; //第一个数据块的偏移量
optional uint64 last_data_block_offset = ; //最后一个数据块的偏移量
optional string comparator_class_name = ; //比较器类名
optional uint32 compression_codec = ; //编码
optional bytes encryption_key = ; //加密相关
}
从proto文件可以看出,Trailer主要记录了Load-on-open-section相关的信息,应该花点时间去做些结合和对比。
step2:使用Protobuf命令生成java代码.(刚好我之前在hadoop环境中编译过源码,安装了protobuf)
protoc HFile.proto --java_out=.
将生成的java类拷到我们的项目中.
step3. 编写java代码解析Trailer.
public static void main(String[] args) throws Exception {
Configuration config = new Configuration(); // 我已经将文件拷到了f盘根目录
String pathStr = "file:///f:/0a99d83b2b0a49c0adbc371d4bfe021e";
Path path = new Path(pathStr);
FileSystem fs = FileSystem.get(URI.create(pathStr), config); FSDataInputStream input = fs.open(path); long length = input.available();
int trailerSize = ; input.seek(length - trailerSize);
byte[] trailerBytes = new byte[trailerSize];
input.read(trailerBytes); ByteBuffer trailerBuf = ByteBuffer.wrap(trailerBytes);
trailerBuf.position(trailerSize - ); int version = trailerBuf.getInt();
//3, 0, 0, 2
//最后三位是majorVersion
int majorVersion = version & 0x00ffffff;
//高位是 minorVersion
int minorVersion = version >>> ; String magicCode = Bytes.toString(Arrays.copyOfRange(trailerBytes, , )); // 除去头8个字节MagicCode ,除去尾4个字节version信息。咱就是这么暴力。
FileTrailerProto hfileProtos = FileTrailerProto.PARSER
.parseDelimitedFrom(new ByteArrayInputStream(trailerBytes, ,
trailerSize - ));
System.out.println(String.format("MagicCode:%s,majorVersion:%d,:minorVersion:%d",magicCode,majorVersion,minorVersion));
System.out.println(hfileProtos);
}
输出结果:
至此,Trailer已经完全解析完成,接下来开始下一部分:
Load-on-open-section:
RegionServer托管着0...n个Region,Region管理着一个或多个HStore,其中HStore就管理着一个MemStore及多个StoreFile.
所在RegionServer启动时,会扫描所StoreFile,加载StoreFile的相关信息到内存,而这部分内容就是Load-on-open-section,主要包括 Root数据索引,miidKyes(optional),Meta索引,File Info,及BloomFilter metadata等.
数据索引:
数据索引是分层的,可以1-3层,其中第一层,即Root level Data Index,这部分数据是处放在内存区的。一开始,文件比较小,只有single-level,rootIndex直接定位到了DataBlock。当StoreFile变大时,rootIndex越来越大,随之所耗内存增大,会以多层结构存储数据索引.当采用multi-level方式,level=2时,使用root index和leaf index chunk,即内存区的rootIndex定位到的是 leafIndex,再由leafIndex定位到Datablock。当一个文件的datablock非常多,采用的是三级索引,即rootIndex定位到intermediate index,再由intermediate index定位到leaf index,最后定位到data block.可以看看上面图1所示,各个level的index都是分布在不同的区域的。但每部分index是以HFileBlock格式存放的,后面会比较详细地讲HFileBlock,说白了,就是HFile中的一个块。
Fileds for midKey:
这部分数据是Optional的,保存了一些midKey信息,可以快速地定位到midKey,常常在HFileSplit的时候非常有用。
MetaIndex:
即meta的索引数据,和data index类似,但是meta存放的是BloomFilter的信息,关于BloomFilter由于篇幅就不深入讨论了.
FileInfo:
保存了一些文件的信息,如lastKey,avgKeylen,avgValueLen等等,一会我们将会写程序将这部分内容解析出来并打印看看是什么东西。同样,FileInfo使用了Protobuf来进行序列化。
Bloom filter metadata:
分为GENERAL_BLOOM_META及DELETE_FAMILY_BLOOM_META二种。
OK,下面开始操刀分割下Load-on-open-section的各个小块,看看究竟有什么东西。在开始分析之前,上面提到了一个HFileBlock想先看看。从上面可以看出来,其实基本每个小块都叫HFileBlock(除field for midkey),在Hbase中有一个类叫HFileBlock与之对应。从V2开始,即我们当前用的HFile版本,HFileBlock是支持checksum的,默认地使用CRC32,由此HFileBlock由header,data,checksum三部分内容组成,如下图所示。其中Header占了33个字节,字段是一样的,而每个block的组织会有些小差异.
图2
了解了HFileBlock的结构,我们下面开始正式解析内存区中的各个index的block内容。首先我们根据图2我们抽象出一个简单的HFileBlock实体。
实验2: HFileBlock的解析.及BlockReader内部类
public class MyHFileBlock {
public static class Header {
private String magicCode ;
int onDiskSizeWithoutHeader;
int unCompressBlockSize;
long prevBlockOffset;
byte checkSum;
int bytesPerChecksum;
int onDiskDataSizeWithHeader;
} private Header header;
private ByteBuffer blockBuf;
private byte [] checkSum ; ....
public static class BlockIndexReader {
public BlockIndexReader(MyHFileBlock block) {
....
}
public BlockIndexReader parseMultiLevel(int numEntries, String expectedMagicCode, int level) throws Exception {
.....
}
.......
}
}
2.编写HFileBlock遍历器,代码有点长,折叠起来吧,有兴趣可以看看,详细完整代码还是下载附件项目吧,
public class MyHFileBlockIterator { private ByteBuffer loadOnOpenBuffer; public MyHFileBlockIterator(FSDataInputStream data, long offset, int length) { try {
data.seek(offset);
byte[] loadOnOpenBytes = new byte[length];
data.read(loadOnOpenBytes);
loadOnOpenBuffer = ByteBuffer.wrap(loadOnOpenBytes);
} catch (IOException e) {
e.printStackTrace();
}
} public MyHFileBlockIterator(byte [] data) {
loadOnOpenBuffer = ByteBuffer.wrap(data);
} public MyHFileBlock nextBlock() { MyHFileBlock block = new MyHFileBlock(loadOnOpenBuffer);
Header header = block.getHeader();
int currentBlockLength = block.getHeader()
.getOnDiskDataSizeWithHeader(); int dataSize = currentBlockLength - MyHFileBlock.HARDER_SIZE; byte[] dataBlockArray = new byte[dataSize]; loadOnOpenBuffer.get(dataBlockArray); ByteBuffer dataBlock = ByteBuffer.wrap(dataBlockArray); block.setBlockBuf(dataBlock); int checkSumChunks = header.getOnDiskSizeWithoutHeader()
/ header.getBytesPerChecksum();
if (header.getOnDiskSizeWithoutHeader() % header.getBytesPerChecksum() != ) {
checkSumChunks++;
}
int checkSumBytes = checkSumChunks * ;
byte[] checkSum = new byte[checkSumBytes]; loadOnOpenBuffer.get(checkSum); block.setCheckSum(checkSum); return block;
} public boolean hasNext(){
return loadOnOpenBuffer.position() < loadOnOpenBuffer.capacity();
}
}
开始解析Root Data Index和metaIndex .在Trailer解析后,我们可以得到Load-on-open-section内容的相关信息,可以构造字节数组,将这部分字节码load进内存进行解析,在解析之前先讲下FileInfo
FileInfo的内容是以ProtoBuf放式存放的,与Trailer类似,我们先创建FileInfo.proto
option java_package = "com.bdifn.hbase.hfile.proto";
option java_outer_classname = "FileInfoProtos";
option java_generic_services = true;
option java_generate_equals_and_hash = true;
option optimize_for = SPEED; message BytesBytesPair {
required bytes first = 1;
required bytes second = 2;
} message FileInfoProto {
repeated BytesBytesPair map_entry = 1;
}
编译: protoc FileInfo.proto --java_out=.
编写测试类:
....
FileTrailerProto hfileProtos = FileTrailerProto.PARSER.parseDelimitedFrom(new ByteArrayInputStream(trailerBytes, 8,trailerBytes.length - 4));
long loadOnOpenLength = length - trailerSize - hfileProtos.getLoadOnOpenDataOffset();
MyHFileBlockIterator inter = new MyHFileBlockIterator(input,hfileProtos.getLoadOnOpenDataOffset(), (int) loadOnOpenLength);
//解析出来root data index
MyHFileBlock dataIndex = inter.nextBlock();
int dataIndexLevels = hfileProtos.getNumDataIndexLevels();
int dataIndexEntries = hfileProtos.getDataIndexCount();
//创建root data index reader
MyHFileBlock.BlockIndexReader rootDataReader = dataIndex.createBlockIndexReader().parseMultiLevel(dataIndexEntries,"IDXROOT2", dataIndexLevels);
//解析出来root meta index
MyHFileBlock metaIndex = inter.nextBlock();
.....
//获取file info
MyHFileBlock fileInfo = inter.nextBlock();
//解析读取FileInfo内容
ByteArrayInputStream in = new ByteArrayInputStream(fileInfo.getBlockBuf().array());
int pblen = ProtobufUtil.lengthOfPBMagic();
byte[] pbuf = new byte[pblen];
if (in.markSupported())
in.mark(pblen);
int read = in.read(pbuf);
FileInfoProtos.FileInfoProto fileInfoProto = FileInfoProtos.FileInfoProto.parseDelimitedFrom(in); List<BytesBytesPair> entries = fileInfoProto.getMapEntryList(); for (BytesBytesPair entry : entries) {
System.out.println(entry.getFirst().toStringUtf8() + ":"+ entry.getSecond().toStringUtf8());
}
//剩下的BloomFileter metadata block.
while (inter.hasNext()) {
MyHFileBlock block = inter.nextBlock();
System.out.println(block.getHeader());
}
以上就是解析HFile Load-on-open-section部分的各个fileblock内容,完整代码请下载附带的地址。
Scanned block section: 关于bloomfilter先不分析了。
Non-scanned block section:
这部分内容就是真正的数据块,从图1看出,这部分数据是分datablock存储的,默认地,每个datablock占64K,如果是多层的index的话,部分index block也会存放在这里,由于我的测试数据,是single-level的,所以只针对单级的index分析。
的single-level情况下,内存的rootDataIndex记录了每个datablock的偏移量,大小及startKey信息,主要是为了快速地定位到KeyValue的位置,在HFile中查找或者seek到某个KeyValue时,首先会在内存中,对rootDataIndex进行二分查找,单级的index可以直接定位DataBlock,然后通过迭代datablock定位到KeyValue所在的位置,而2-3层时,上面也略有提及,大家有时间的话,可以做多点研究这部分。
弱弱提句:在HStore中,会有cache将这些datablock缓存起来,使用LRU算法,这样会提高不少性能。
每个DataBlock同样也是一个HFileBlock,也包括header,data,checksum信息,可以用我们之前写的BlockIterator就可以搞定。下面使用代码,去遍历一个datablock看看。
实验3:
编写KeyValue遍历器
public class KeyValueIterator {
public static final int KEY_LENGTH_SIZE = 4;
public static final int VALUE_LENGTH_SIZE = 4; private byte [] data ;
private int currentOffset ; public KeyValueIterator(byte [] data) {
this.data = data;
currentOffset = 0;
} public KeyValue nextKeyValue(){
KeyValue kv = null;
int keyLen = Bytes.toInt(data,currentOffset,4);
incrementOffset(KEY_LENGTH_SIZE); int valueLen = Bytes.toInt(data,currentOffset,4);
incrementOffset(VALUE_LENGTH_SIZE); //1 is memTS
incrementOffset(keyLen,valueLen,1); int kvSize = KEY_LENGTH_SIZE + VALUE_LENGTH_SIZE + keyLen + valueLen ; kv = new KeyValue(data , currentOffset - kvSize - 1, kvSize);
return kv;
}
public void incrementOffset(int ... lengths) {
for(int length : lengths)
currentOffset = currentOffset + length;
} public boolean hasNext() {
return currentOffset < data.length;
}
}
编写测试代码:
//从rootDataReader中获取第一块的offset及数据大小
long offset = rootDataReader.getBlockOffsets()[0];
int size = rootDataReader.getBlockDataSizes()[0]; byte[] dataBlockArray = new byte[size];
input.seek(offset);
input.read(dataBlockArray);
//图方便,直接用iterator来解析出来FileBlock
MyHFileBlockIterator dataBlockIter = new MyHFileBlockIterator(dataBlockArray);
MyHFileBlock dataBlock1 = dataBlockIter.nextBlock();
//将data内容给一个keyvalue迭代器
KeyValueIterator kvIter = new KeyValueIterator(dataBlock1.getBlockBuf().array());
while (kvIter.hasNext()) {
KeyValue kv = kvIter.nextKeyValue();
//do some with keyvalue. like print the kv.
System.out.println(kv);
}
OK,基本上是这些内容了。有点抱歉一开篇讲得有点大了,其实没有方方面面都讲得很详细。meta,bloomfilter部分没有详细分析,大家有时间可以研究后,分享一下。
源码我将我测试的Hfile也附带上传了,压缩后有3M多,完整代码请下载:下载源码
HBase之HFile解析的更多相关文章
- HBase – 探索HFile索引机制
本文由 网易云发布. 作者: 范欣欣 本篇文章仅限内部分享,如需转载,请联系网易获取授权. 01 HFile索引结构解析 HFile中索引结构根据索引层级的不同分为两种:single-level和m ...
- HFile解析 基于0.96
什么是HFile HBase.BigTable以及其他分布式存储.查询系统的底层存储都采用SStable的思想,HBase的底层存储是HFile,他要解决的问题就是如果将内容存储到磁盘,以及如何高效的 ...
- Hadoop生态圈-HBase的HFile创建方式
Hadoop生态圈-HBase的HFile创建方式 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 废话不多说,直接上代码,想说的话都在代码的注释里面. 一.环境准备 list cr ...
- HBase 高级架构解析
整体框架 使用 ZooKeeper 框架协助 RegionServer(类似于HDFS的nodemanager)用户请求从 Client 到 Zookeeper 进行判断数据属于哪一个 Region ...
- hbase 查看hfile文件
emp表数据结构 hbase(main):098:0> scan 'emp' ROW COLUMN+CELL row1 column=mycf:depart, timestamp=1555846 ...
- HBase – 存储文件HFile结构解析
本文由 网易云发布. 作者:范欣欣 本篇文章仅限内部分享,如需转载,请联系网易获取授权. HFile是HBase存储数据的文件组织形式,参考BigTable的SSTable和Hadoop的TFile ...
- HBase架构深度解析
原文出处: DLevin(@雪地脚印_) 前记 公司内部使用的是MapR版本的Hadoop生态系统,因而从MapR的官网看到了这篇文文章:An In-Depth Look at the HBase A ...
- HBase工具:如何查看HBase的HFile
root@root:~/Desktop/sourceCodes/hbase-2.1.1/bin# ./hbase Usage: hbase [<options>] <command& ...
- HBase轻松入门之HBase架构图解析
2018-12-13 2018-12-20 本篇文章旨在针对初学者以我本人现阶段所掌握的知识就HBase的架构图中各模块作一个概念科普.不对文章内容的“绝对.完全正确性”负责. 1.开胃小菜 关于HB ...
随机推荐
- java 过滤器(自己的理解)
filter继承javax.servlet.* 必须实现doFilter方法 chain.doFilter(request, response);这句话必须写在doFilter方法内部(以便调用其他的 ...
- 以JPanel为基础实现一个图像框
代码: import java.awt.Graphics; import javax.swing.ImageIcon; import javax.swing.JPanel; public class ...
- Visual Studio2015 简体中文版 安装
VS2015简体中文版安装 导航 介绍 解决安装先决条件 安装 VS2015 创建桌面快捷方式 启动 VS2015 命令启动VS2015 配置 VS2015 启动完成 Visual Studio的功能 ...
- Git库搭建好之后,当要提交一个新的文件,需要做的是3个步骤
Git库搭建好之后,当要提交一个新的文件,需要做的是3个步骤 1.git add new.txt 2.git commit -m "add a new file" 3.git pu ...
- java泛型介绍
一.泛型初衷 Java集合不会知道我们需要用它来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要就具有很好的通用性.但这样做也带来两个问题: –集合对元素类型没有任何限制,这样可能引 ...
- Oracle 临时表空间 temp表空间切换
一.TEMP表空间 临时表空间主要用途是在数据库进行排序运算.管理索引.访问视图等操作时提供临时的运算空间,当运算完成之后系统会自动清理.当oracle里需要用到sort的时候,PGA中sort_ar ...
- 【Firefly API文档】—— Package Netconnect
http://bbs.gameres.com/forum.php?mod=viewthread&tid=219655 package netconnect 该包中包含的服务端与客户端通信的一些 ...
- Linux内核分析:实验八--Linux进程调度与切换
刘畅 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 概述 这篇文章主要分析Li ...
- 广告系统的smart pricing是什么
smart pricing这个词来源于google的Adwords系统,指的是系统能够根据流量质量对流量方的收入进行打折,为的是让广告主获得更高的ROI(投资回报率).将smart pricing的使 ...
- Exception sending context initialized event to listener instance of class ssm.blog.listener.InitBloggerData java.lang.NullPointerException at ssm.blog.listener.InitBloggerData.c
spring注入是分两部分执行的 首先是 先把需要注入的对象加载到spring容器 然后在把对象注入到具体需要注入的对象里面 这种就是配置和注解的注入 getbean方式其 ...