分享

hbase源码系列(九)StoreFile存储格式

本帖最后由 坎蒂丝_Swan 于 2014-12-29 13:01 编辑

问题导读
1.打开文件的时候会加载哪些内容?
2.编码压缩方式是如何写进FileInfo里面的?






从这一章开始要讲Region Server这块的了,但是在讲Region Server这块之前得讲一下StoreFile,否则后面的不好讲下去,这块是基础,Region Sever上面的操作,大部分都是基于它来进行的。

HFile概述
HFile是HBase中实际存数据的文件,为HBase提供高效快速的数据访问。它是基于Hadoop的TFile,模仿Google Bigtable 架构中的SSTable格式。文件格式如下:

122127481096275.png

文件是变长的,唯一固定的块是File Info和Trailer,如图所示,Trailer有指向其它块的指针,这些指针也写在了文件里,Index块记录了data和meta块的偏移量,meta块是可选的。
下面我们从原来上来一个一个的看它们到底是啥样的,先从入口看起,那就是StoreFile.Writer的append方法,先看怎么写入的,然后它就怎么读了。

往HFile追加KeyValue
不扯这些了,看一下StoreFile里面的append方法。

  1. public void append(final KeyValue kv) throws IOException {
  2.       //如果是新的rowkey的value,就追加到Bloomfilter里面去
  3.       appendGeneralBloomfilter(kv);
  4.       //如果是DeleteFamily、DeleteFamilyVersion类型的kv
  5.       appendDeleteFamilyBloomFilter(kv);
  6.       writer.append(kv);
  7.       //记录最新的put的时间戳,更新时间戳范围
  8.       trackTimestamps(kv);
  9.     }
复制代码

在用writer进行append之前先把kv写到generalBloomFilterWriter里面,但是我们发现generalBloomFilterWriter是HFile.Writer里面的InlineBlockWriter。
  1. generalBloomFilterWriter = BloomFilterFactory.createGeneralBloomAtWrite(
  2.           conf, cacheConf, bloomType,
  3.           (int) Math.min(maxKeys, Integer.MAX_VALUE), writer);
  4. //在createGeneralBloomAtWriter方法发现了以下代码
  5. ......
  6. CompoundBloomFilterWriter bloomWriter = new CompoundBloomFilterWriter(getBloomBlockSize(conf),
  7.         err, Hash.getHashType(conf), maxFold, cacheConf.shouldCacheBloomsOnWrite(),
  8.         bloomType == BloomType.ROWCOL ? KeyValue.COMPARATOR : KeyValue.RAW_COMPARATOR);
  9.     writer.addInlineBlockWriter(bloomWriter);
复制代码

我们接下来看HFileWriterV2的append方法吧。
  1. public void append(final KeyValue kv) throws IOException {
  2.     append(kv.getMvccVersion(), kv.getBuffer(), kv.getKeyOffset(), kv.getKeyLength(),
  3.         kv.getBuffer(), kv.getValueOffset(), kv.getValueLength());
  4.     this.maxMemstoreTS = Math.max(this.maxMemstoreTS, kv.getMvccVersion());
  5. }
复制代码

为什么贴这段代码,注意这个参数maxMemstoreTS,它取kv的mvcc来比较,mvcc是用来实现MemStore的原子性操作的,在MemStore flush的时候同一批次的mvcc都是一样的,失败的时候,把mvcc相同的全部干掉,这里提一下,以后应该还会说到,继续追杀append方法。方法比较长,大家展开看看。
  1. private void append(final long memstoreTS, final byte[] key, final int koffset, final int klength,
  2.       final byte[] value, final int voffset, final int vlength)
  3.       throws IOException {
  4.     boolean dupKey = checkKey(key, koffset, klength);
  5.     checkValue(value, voffset, vlength);
  6.     if (!dupKey) {
  7.       //在写每一个新的KeyValue之间,都要检查,到了BlockSize就重新写一个HFileBlock
  8.       checkBlockBoundary();
  9.     }
  10.     //如果当前的fsBlockWriter的状态不对,就重新写一个新块
  11.     if (!fsBlockWriter.isWriting())
  12.       newBlock();
  13.     // 把值写入到ouputStream当中,怎么写入的自己看啊
  14.     {
  15.       DataOutputStream out = fsBlockWriter.getUserDataStream();
  16.       out.writeInt(klength);
  17.       totalKeyLength += klength;
  18.       out.writeInt(vlength);
  19.       totalValueLength += vlength;
  20.       out.write(key, koffset, klength);
  21.       out.write(value, voffset, vlength);
  22.       if (this.includeMemstoreTS) {
  23.         WritableUtils.writeVLong(out, memstoreTS);
  24.       }
  25.     }
  26.     // 记录每个块的第一个key 和 上次写的key
  27.     if (firstKeyInBlock == null) {
  28.       firstKeyInBlock = new byte[klength];
  29.       System.arraycopy(key, koffset, firstKeyInBlock, 0, klength);
  30.     }
  31.     lastKeyBuffer = key;
  32.     lastKeyOffset = koffset;
  33.     lastKeyLength = klength;
  34.     entryCount++;
  35.   }
复制代码

从上面我们可以看到来,HFile写入的时候,是分一个块一个块的写入的,每个Block块64KB左右,这样有利于数据的随机访问,不利于连续访问,连续访问需求大的,可以把Block块的大小设置得大一点。好,我们继续看checkBlockBoundary方法。
  1. private void checkBlockBoundary() throws IOException {
  2.     if (fsBlockWriter.blockSizeWritten() < blockSize)
  3.       return;
  4.     finishBlock();
  5.     writeInlineBlocks(false);
  6.     newBlock();
  7.   }
复制代码


简单交代一下
1、结束一个block的时候,把block的所有数据写入到hdfs的流当中,记录一些信息到DataBlockIndex(块的第一个key和上一个块的key的中间值,块的大小,块的起始位置)。
2、writeInlineBlocks(false)给了一个false,是否要关闭,所以现在什么都没干,它要等到最后才会输出的。
3、newBlock方法就是重置输出流,做好准备,读写下一个块。

Close的时候

close的时候就有得忙咯,从之前的图上面来看,它在最后的时候是最忙的,因为它要写入一大堆索引信息、附属信息啥的。

  1. public void close() throws IOException {
  2.       boolean hasGeneralBloom = this.closeGeneralBloomFilter();
  3.       boolean hasDeleteFamilyBloom = this.closeDeleteFamilyBloomFilter();
  4.       writer.close();
  5. }
复制代码

在调用writer的close方法之前,close了两个BloomFilter,把BloomFilter的类型写进FileInfo里面去,把BloomWriter添加到Writer里面。下面进入正题吧,放大招了。
  1. public void close() throws IOException {
  2.     if (outputStream == null) {
  3.       return;
  4.     }
  5.     // 经过编码压缩的,把编码压缩方式写进FileInfo里面
  6.     blockEncoder.saveMetadata(this);
  7.     //结束块
  8.     finishBlock();
  9.     //输出DataBlockIndex索引的非root层信息
  10.     writeInlineBlocks(true);
  11.     FixedFileTrailer trailer = new FixedFileTrailer(2,HFileReaderV2.MAX_MINOR_VERSION);
  12.     // 如果有meta块的存在的话
  13.     if (!metaNames.isEmpty()) {
  14.       for (int i = 0; i < metaNames.size(); ++i) {
  15.         long offset = outputStream.getPos();
  16.         // 输出meta的内容,它是meta的名字的集合,按照名字排序
  17.         DataOutputStream dos = fsBlockWriter.startWriting(BlockType.META);
  18.         metaData.get(i).write(dos);
  19.         fsBlockWriter.writeHeaderAndData(outputStream);
  20.         totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();
  21.         // 把meta块的信息加到meta块的索引里
  22.         metaBlockIndexWriter.addEntry(metaNames.get(i), offset,
  23.             fsBlockWriter.getOnDiskSizeWithHeader());
  24.       }
  25.     }
  26.     //下面这部分是打开文件的时候就加载的部分,是前面部分的索引
  27.     //HFileBlockIndex的根层次的索引
  28.     long rootIndexOffset = dataBlockIndexWriter.writeIndexBlocks(outputStream);
  29.     trailer.setLoadOnOpenOffset(rootIndexOffset);
  30.     //Meta块的索引
  31.     metaBlockIndexWriter.writeSingleLevelIndex(fsBlockWriter.startWriting(
  32.         BlockType.ROOT_INDEX), "meta");
  33.     fsBlockWriter.writeHeaderAndData(outputStream);
  34.     totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();
  35.     //如果需要写入Memstore的最大时间戳到FileInfo里面
  36.     if (this.includeMemstoreTS) {
  37.       appendFileInfo(MAX_MEMSTORE_TS_KEY, Bytes.toBytes(maxMemstoreTS));
  38.       appendFileInfo(KEY_VALUE_VERSION, Bytes.toBytes(KEY_VALUE_VER_WITH_MEMSTORE));
  39.     }
  40.     //把FileInfo的起始位置写入trailer,然后输出
  41.     writeFileInfo(trailer, fsBlockWriter.startWriting(BlockType.FILE_INFO));
  42.     fsBlockWriter.writeHeaderAndData(outputStream);
  43.     totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();
  44.     // 输出GENERAL_BLOOM_META、DELETE_FAMILY_BLOOM_META类型的BloomFilter的信息
  45.     for (BlockWritable w : additionalLoadOnOpenData){
  46.       fsBlockWriter.writeBlock(w, outputStream);
  47.       totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();
  48.     }
  49.     //HFileBlockIndex的二级实体的层次
  50.     trailer.setNumDataIndexLevels(dataBlockIndexWriter.getNumLevels());
  51.     //压缩前的HFileBlockIndex的大小
  52.     trailer.setUncompressedDataIndexSize(
  53.         dataBlockIndexWriter.getTotalUncompressedSize());
  54.     //第一个HFileBlock的起始位置
  55.     trailer.setFirstDataBlockOffset(firstDataBlockOffset);
  56.     //最后一个HFileBlock的起始位置
  57.     trailer.setLastDataBlockOffset(lastDataBlockOffset);
  58.     //比较器的类型
  59.     trailer.setComparatorClass(comparator.getClass());
  60.     //HFileBlockIndex的根实体的数量,应该是和HFileBlock的数量是一样的
  61.     //它每次都把HFileBlock的第一个key加进去
  62.     trailer.setDataIndexCount(dataBlockIndexWriter.getNumRootEntries());
  63.     //把Trailer的信息写入硬盘,关闭输出流
  64.     finishClose(trailer);
  65.     fsBlockWriter.release();
  66.   }
复制代码


和图片上写的有些出入。
1、输出HFileBlocks
2、输出HFileBlockIndex的二级索引(我叫它二级索引,我也不知道对不对,HFileBlockIndex那块我有点儿忘了,等我再重新调试的时候再看看吧)
3、如果有的话,输出MetaBlock

下面的部分是打开文件的时候就加载的
4、输出HFileBlockIndex的根索引
5、如果有的话,输出MetaBlockIndex的根索引(它比较小,所以只有一层)
6、输出文件信息(FileInfo)
7、输出文件尾巴(Trailer)

Open的时候

这部分打算讲一下实例化Reader的时候,根据不同类型的文件是怎么实例化Reader的,在StoreFile里面搜索open方法。

  1. this.reader = fileInfo.open(this.fs, this.cacheConf, dataBlockEncoder.getEncodingInCache());
  2. // 加载文件信息到map里面去,后面部分就不展开讲了
  3. metadataMap = Collections.unmodifiableMap(this.reader.loadFileInfo());
复制代码

我们进入F3进入fileInfo.open这个方法里面去。
  1. FSDataInputStreamWrapper in;
  2.     FileStatus status;
  3.     if (this.link != null) {
  4.       // HFileLink
  5.       in = new FSDataInputStreamWrapper(fs, this.link);
  6.       status = this.link.getFileStatus(fs);
  7.     } else if (this.reference != null) {
  8.       // HFile Reference 反向计算出来引用所指向的位置的HFile位置
  9.       Path referencePath = getReferredToFile(this.getPath());
  10.       in = new FSDataInputStreamWrapper(fs, referencePath);
  11.       status = fs.getFileStatus(referencePath);
  12.     } else {
  13.       in = new FSDataInputStreamWrapper(fs, this.getPath());
  14.       status = fileStatus;
  15.     }
  16.     long length = status.getLen();
  17.     if (this.reference != null) {
  18.       hdfsBlocksDistribution = computeRefFileHDFSBlockDistribution(fs, reference, status);
  19.       //如果是引用的话,创建一个一半的reader
  20.       return new HalfStoreFileReader(
  21.           fs, this.getPath(), in, length, cacheConf, reference, dataBlockEncoding);
  22.     } else {
  23.       hdfsBlocksDistribution = FSUtils.computeHDFSBlocksDistribution(fs, status, 0, length);
  24.       return new StoreFile.Reader(fs, this.getPath(), in, length, cacheConf, dataBlockEncoding);
  25.     }
复制代码

它一上来就判断它是不是HFileLink是否为空了,这是啥情况?找了一下,原来在StoreFile的构造函数的时候,就开始判断了。
  1. this.fileStatus = fileStatus;
  2.     Path p = fileStatus.getPath();
  3.     if (HFileLink.isHFileLink(p)) {
  4.       // HFileLink 被判断出来它是HFile
  5.       this.reference = null;
  6.       this.link = new HFileLink(conf, p);
  7.     } else if (isReference(p)) {
  8.       this.reference = Reference.read(fs, p);
  9.       //关联的地址也可能是一个HFileLink,snapshot的时候介绍了
  10.       Path referencePath = getReferredToFile(p);
  11.       if (HFileLink.isHFileLink(referencePath)) {
  12.         // HFileLink Reference 如果它是一个HFileLink型的
  13.         this.link = new HFileLink(conf, referencePath);
  14.       } else {
  15.         // 只是引用
  16.         this.link = null;
  17.       }
  18.     } else if (isHFile(p)) {
  19.       // HFile
  20.       this.reference = null;
  21.       this.link = null;
  22.     } else {
  23.       throw new IOException("path=" + p + " doesn't look like a valid StoreFile");
  24.     }
复制代码


它有4种情况:
1、HFileLink
2、既是HFileLink又是Reference文件
3、只是Reference文件
4、HFile

说HFileLink吧,我们看看它的构造函数

  1. public HFileLink(final Path rootDir, final Path archiveDir, final Path path) {
  2.     Path hfilePath = getRelativeTablePath(path);
  3.     this.tempPath = new Path(new Path(rootDir, HConstants.HBASE_TEMP_DIRECTORY), hfilePath);
  4.     this.originPath = new Path(rootDir, hfilePath);
  5.     this.archivePath = new Path(archiveDir, hfilePath);
  6.     setLocations(originPath, tempPath, archivePath);
  7. }
复制代码

尼玛,它计算了三个地址,原始位置,archive中的位置,临时目录的位置,按照顺序添加到一个locations数组里面。。接着看FSDataInputStreamWrapper吧,下面是三段代码
  1. this.stream = (link != null) ? link.open(hfs) : hfs.open(path);
  2. //走的link.open(hfs)
  3. new FSDataInputStream(new FileLinkInputStream(fs, this));
  4. //注意tryOpen方法
  5. public FileLinkInputStream(final FileSystem fs, final FileLink fileLink, int bufferSize)
  6.         throws IOException {
  7.       this.bufferSize = bufferSize;
  8.       this.fileLink = fileLink;
  9.       this.fs = fs;
  10.       this.in = tryOpen();
  11. }
复制代码

tryOpen的方法,会按顺序打开多个locations列表。。
  1. for (Path path: fileLink.getLocations()) {
  2.         if (path.equals(currentPath)) continue;
  3.         try {
  4.           in = fs.open(path, bufferSize);
  5.           in.seek(pos);
  6.           assert(in.getPos() == pos) : "Link unable to seek to the right position=" + pos;
  7.           if (LOG.isTraceEnabled()) {
  8.             if (currentPath != null) {
  9.               LOG.debug("link open path=" + path);
  10.             } else {
  11.               LOG.trace("link switch from path=" + currentPath + " to path=" + path);
  12.             }
  13.           }
  14.           currentPath = path;
  15.           return(in);
  16.         } catch (FileNotFoundException e) {
  17.           // Try another file location
  18.         }
  19. }
复制代码


恩,这回终于知道它是怎么出来的了,原来是尝试打开了三次,直到找到正确的位置。
StoreFile的文件格式到这里就结束了,有点儿遗憾的是HFileBlockIndex没给大家讲清楚。

补充:经网"的提醒,有一个地方写错了,在结束一个块之后,会把它所有的BloomFilter全部输出,HFileBlockIndex的话,如果满了默认的128*1024个就输出二级索引。

具体的的内容在后面说查询的时候会说,下面先交代一下:
通过看继承InlineBlockWriter的类,发现了以下信息
1、BlockIndexWriter 不是关闭的情况下,没有超过默认值128*1024是不会输出的,每128*1024个HFileBlock 1个二级索引。
HFileBlockIndex包括2层,如果是MetaBlock的HFileBlock是1层。
二级索引 curInlineChunk 在结束了一个块之后添加一个索引的key(上一个块的firstKey和这个块的firstKey的中间值)。

  1. byte[] indexKey = comparator.calcIndexKey(lastKeyOfPreviousBlock, firstKeyInBlock);
  2. curInlineChunk.add(firstKey, blockOffset, blockDataSize);
复制代码

一级索引 rootChunk 输出一次二级索引之后添加每个HFileBlock的第一个key,这样子其实二级索引里面是包括是一级索引的所有key的。
  1. firstKey = curInlineChunk.getBlockKey(0);
  2. rootChunk.add(firstKey, offset, onDiskSize, totalNumEntries);
复制代码


2、CompoundBloomFilterWriter也就是Bloom Filter,在数据不为空的时候,就会输出。
对于HFileV2的正确的图,应该是下面这个,但是上面的那个图看起来好看一点,就保留了。

160155267814449.png




hbase源码系列(二)HTable 探秘hbase源码系列(三)Client如何找到正确的Region Server
hbase源码系列(四)数据模型-表定义和列族定义的具体含义
hbase源码系列(五)Trie单词查找树
hbase源码系列(六)HMaster启动过程
hbase源码系列(七)Snapshot的过程
hbase源码系列(八)从Snapshot恢复表
hbase源码系列(九)StoreFile存储格式
hbase源码系列(十)HLog与日志恢复
hbase源码系列(十一)Put、Delete在服务端是如何处理?
hbase源码系列(十二)Get、Scan在服务端是如何处理?
hbase源码系列(十三)缓存机制MemStore与Block Cache
hbase源码之如何查询出来下一个KeyValue
hbase源码Compact和Split

欢迎加入about云群90371779322273151432264021 ,云计算爱好者群,亦可关注about云腾讯认证空间||关注本站微信

没找到任何评论,期待你打破沉寂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条