hbase源码系列(九)StoreFile存储格式
從這一章開始要講Region Server這塊的了,但是在講Region Server這塊之前得講一下StoreFile,否則后面的不好講下去,這塊是基礎,Region Sever上面的操作,大部分都是基于它來進行的。
HFile概述
HFile是HBase中實際存數據的文件,為HBase提供高效快速的數據訪問。它是基于Hadoop的TFile,模仿Google Bigtable 架構中的SSTable格式。文件格式如下:
文件是變長的,唯一固定的塊是File Info和Trailer,如圖所示,Trailer有指向其它塊的指針,這些指針也寫在了文件里,Index塊記錄了data和meta塊的偏移量,meta塊是可選的。
下面我們從原來上來一個一個的看它們到底是啥樣的,先從入口看起,那就是StoreFile.Writer的append方法,先看怎么寫入的,然后它就怎么讀了,不知道怎么使用這個類的,可以看看我寫的這篇文章《非mapreduce生成Hfile,然后導入hbase當中》。
往HFile追加KeyValue?
不扯這些了,看一下StoreFile里面的append方法。
public void append(final KeyValue kv) throws IOException {//如果是新的rowkey的value,就追加到Bloomfilter里面去 appendGeneralBloomfilter(kv);//如果是DeleteFamily、DeleteFamilyVersion類型的kv appendDeleteFamilyBloomFilter(kv);writer.append(kv);//記錄最新的put的時間戳,更新時間戳范圍 trackTimestamps(kv);}在用writer進行append之前先把kv寫到generalBloomFilterWriter里面,但是我們發現generalBloomFilterWriter是HFile.Writer里面的InlineBlockWriter。
generalBloomFilterWriter = BloomFilterFactory.createGeneralBloomAtWrite(conf, cacheConf, bloomType,(int) Math.min(maxKeys, Integer.MAX_VALUE), writer); //在createGeneralBloomAtWriter方法發現了以下代碼 ...... CompoundBloomFilterWriter bloomWriter = new CompoundBloomFilterWriter(getBloomBlockSize(conf),err, Hash.getHashType(conf), maxFold, cacheConf.shouldCacheBloomsOnWrite(),bloomType == BloomType.ROWCOL ? KeyValue.COMPARATOR : KeyValue.RAW_COMPARATOR);writer.addInlineBlockWriter(bloomWriter);我們接下來看HFileWriterV2的append方法吧。
public void append(final KeyValue kv) throws IOException {append(kv.getMvccVersion(), kv.getBuffer(), kv.getKeyOffset(), kv.getKeyLength(),kv.getBuffer(), kv.getValueOffset(), kv.getValueLength());this.maxMemstoreTS = Math.max(this.maxMemstoreTS, kv.getMvccVersion()); }為什么貼這段代碼,注意這個參數maxMemstoreTS,它取kv的mvcc來比較,mvcc是用來實現MemStore的原子性操作的,在MemStore flush的時候同一批次的mvcc都是一樣的,失敗的時候,把mvcc相同的全部干掉,這里提一下,以后應該還會說到,繼續追殺append方法。方法比較長,大家展開看看。
private void append(final long memstoreTS, final byte[] key, final int koffset, final int klength,final byte[] value, final int voffset, final int vlength)throws IOException {boolean dupKey = checkKey(key, koffset, klength);checkValue(value, voffset, vlength);if (!dupKey) {//在寫每一個新的KeyValue之間,都要檢查,到了BlockSize就重新寫一個HFileBlock checkBlockBoundary();}//如果當前的fsBlockWriter的狀態不對,就重新寫一個新塊if (!fsBlockWriter.isWriting())newBlock();// 把值寫入到ouputStream當中,怎么寫入的自己看啊 {DataOutputStream out = fsBlockWriter.getUserDataStream();out.writeInt(klength);totalKeyLength += klength;out.writeInt(vlength);totalValueLength += vlength;out.write(key, koffset, klength);out.write(value, voffset, vlength);if (this.includeMemstoreTS) {WritableUtils.writeVLong(out, memstoreTS);}}// 記錄每個塊的第一個key 和 上次寫的keyif (firstKeyInBlock == null) {firstKeyInBlock = new byte[klength];System.arraycopy(key, koffset, firstKeyInBlock, 0, klength);}lastKeyBuffer = key;lastKeyOffset = koffset;lastKeyLength = klength;entryCount++;} View Code從上面我們可以看到來,HFile寫入的時候,是分一個塊一個塊的寫入的,每個Block塊64KB左右,這樣有利于數據的隨機訪問,不利于連續訪問,連續訪問需求大的,可以把Block塊的大小設置得大一點。好,我們繼續看checkBlockBoundary方法。
private void checkBlockBoundary() throws IOException {if (fsBlockWriter.blockSizeWritten() < blockSize)return;finishBlock();writeInlineBlocks(false);newBlock();}簡單交代一下
1、結束一個block的時候,把block的所有數據寫入到hdfs的流當中,記錄一些信息到DataBlockIndex(塊的第一個key和上一個塊的key的中間值,塊的大小,塊的起始位置)。
2、writeInlineBlocks(false)給了一個false,是否要關閉,所以現在什么都沒干,它要等到最后才會輸出的。
3、newBlock方法就是重置輸出流,做好準備,讀寫下一個塊。
Close的時候
?close的時候就有得忙咯,從之前的圖上面來看,它在最后的時候是最忙的,因為它要寫入一大堆索引信息、附屬信息啥的。
public void close() throws IOException {boolean hasGeneralBloom = this.closeGeneralBloomFilter();boolean hasDeleteFamilyBloom = this.closeDeleteFamilyBloomFilter();writer.close(); }在調用writer的close方法之前,close了兩個BloomFilter,把BloomFilter的類型寫進FileInfo里面去,把BloomWriter添加到Writer里面。下面進入正題吧,放大招了,我折疊吧。。。
public void close() throws IOException {if (outputStream == null) {return;}// 經過編碼壓縮的,把編碼壓縮方式寫進FileInfo里面blockEncoder.saveMetadata(this);//結束塊 finishBlock();//輸出DataBlockIndex索引的非root層信息writeInlineBlocks(true);FixedFileTrailer trailer = new FixedFileTrailer(2,HFileReaderV2.MAX_MINOR_VERSION);// 如果有meta塊的存在的話if (!metaNames.isEmpty()) {for (int i = 0; i < metaNames.size(); ++i) {long offset = outputStream.getPos();// 輸出meta的內容,它是meta的名字的集合,按照名字排序DataOutputStream dos = fsBlockWriter.startWriting(BlockType.META);metaData.get(i).write(dos);fsBlockWriter.writeHeaderAndData(outputStream);totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();// 把meta塊的信息加到meta塊的索引里 metaBlockIndexWriter.addEntry(metaNames.get(i), offset,fsBlockWriter.getOnDiskSizeWithHeader());}}//下面這部分是打開文件的時候就加載的部分,是前面部分的索引//HFileBlockIndex的根層次的索引long rootIndexOffset = dataBlockIndexWriter.writeIndexBlocks(outputStream);trailer.setLoadOnOpenOffset(rootIndexOffset);//Meta塊的索引 metaBlockIndexWriter.writeSingleLevelIndex(fsBlockWriter.startWriting(BlockType.ROOT_INDEX), "meta");fsBlockWriter.writeHeaderAndData(outputStream);totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();//如果需要寫入Memstore的最大時間戳到FileInfo里面if (this.includeMemstoreTS) {appendFileInfo(MAX_MEMSTORE_TS_KEY, Bytes.toBytes(maxMemstoreTS));appendFileInfo(KEY_VALUE_VERSION, Bytes.toBytes(KEY_VALUE_VER_WITH_MEMSTORE));}//把FileInfo的起始位置寫入trailer,然后輸出 writeFileInfo(trailer, fsBlockWriter.startWriting(BlockType.FILE_INFO));fsBlockWriter.writeHeaderAndData(outputStream);totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();// 輸出GENERAL_BLOOM_META、DELETE_FAMILY_BLOOM_META類型的BloomFilter的信息for (BlockWritable w : additionalLoadOnOpenData){fsBlockWriter.writeBlock(w, outputStream);totalUncompressedBytes += fsBlockWriter.getUncompressedSizeWithHeader();}//HFileBlockIndex的二級實體的層次 trailer.setNumDataIndexLevels(dataBlockIndexWriter.getNumLevels());//壓縮前的HFileBlockIndex的大小 trailer.setUncompressedDataIndexSize(dataBlockIndexWriter.getTotalUncompressedSize());//第一個HFileBlock的起始位置 trailer.setFirstDataBlockOffset(firstDataBlockOffset);//最后一個HFileBlock的起始位置 trailer.setLastDataBlockOffset(lastDataBlockOffset);//比較器的類型 trailer.setComparatorClass(comparator.getClass());//HFileBlockIndex的根實體的數量,應該是和HFileBlock的數量是一樣的//它每次都把HFileBlock的第一個key加進去 trailer.setDataIndexCount(dataBlockIndexWriter.getNumRootEntries());//把Trailer的信息寫入硬盤,關閉輸出流 finishClose(trailer);fsBlockWriter.release();} View Code和圖片上寫的有些出入。
1、輸出HFileBlocks
2、輸出HFileBlockIndex的二級索引(我叫它二級索引,我也不知道對不對,HFileBlockIndex那塊我有點兒忘了,等我再重新調試的時候再看看吧)
3、如果有的話,輸出MetaBlock
下面的部分是打開文件的時候就加載的
4、輸出HFileBlockIndex的根索引
5、如果有的話,輸出MetaBlockIndex的根索引(它比較小,所以只有一層)
6、輸出文件信息(FileInfo)
7、輸出文件尾巴(Trailer)
?Open的時候
這部分打算講一下實例化Reader的時候,根據不同類型的文件是怎么實例化Reader的,在StoreFile里面搜索open方法。
this.reader = fileInfo.open(this.fs, this.cacheConf, dataBlockEncoder.getEncodingInCache());// 加載文件信息到map里面去,后面部分就不展開講了 metadataMap = Collections.unmodifiableMap(this.reader.loadFileInfo());我們進入F3進入fileInfo.open這個方法里面去。
FSDataInputStreamWrapper in;FileStatus status;if (this.link != null) {// HFileLinkin = new FSDataInputStreamWrapper(fs, this.link);status = this.link.getFileStatus(fs);} else if (this.reference != null) {// HFile Reference 反向計算出來引用所指向的位置的HFile位置Path referencePath = getReferredToFile(this.getPath());in = new FSDataInputStreamWrapper(fs, referencePath);status = fs.getFileStatus(referencePath);} else {in = new FSDataInputStreamWrapper(fs, this.getPath());status = fileStatus;}long length = status.getLen();if (this.reference != null) {hdfsBlocksDistribution = computeRefFileHDFSBlockDistribution(fs, reference, status);//如果是引用的話,創建一個一半的readerreturn new HalfStoreFileReader(fs, this.getPath(), in, length, cacheConf, reference, dataBlockEncoding);} else {hdfsBlocksDistribution = FSUtils.computeHDFSBlocksDistribution(fs, status, 0, length);return new StoreFile.Reader(fs, this.getPath(), in, length, cacheConf, dataBlockEncoding);}它一上來就判斷它是不是HFileLink是否為空了,這是啥情況?找了一下,原來在StoreFile的構造函數的時候,就開始判斷了。
this.fileStatus = fileStatus;Path p = fileStatus.getPath();if (HFileLink.isHFileLink(p)) {// HFileLink 被判斷出來它是HFilethis.reference = null;this.link = new HFileLink(conf, p);} else if (isReference(p)) {this.reference = Reference.read(fs, p);//關聯的地址也可能是一個HFileLink,snapshot的時候介紹了Path referencePath = getReferredToFile(p);if (HFileLink.isHFileLink(referencePath)) {// HFileLink Reference 如果它是一個HFileLink型的this.link = new HFileLink(conf, referencePath);} else {// 只是引用this.link = null;}} else if (isHFile(p)) {// HFilethis.reference = null;this.link = null;} else {throw new IOException("path=" + p + " doesn't look like a valid StoreFile");} View Code它有4種情況:
1、HFileLink
2、既是HFileLink又是Reference文件
3、只是Reference文件
4、HFile
?說HFileLink吧,我們看看它的構造函數
public HFileLink(final Path rootDir, final Path archiveDir, final Path path) {Path hfilePath = getRelativeTablePath(path);this.tempPath = new Path(new Path(rootDir, HConstants.HBASE_TEMP_DIRECTORY), hfilePath);this.originPath = new Path(rootDir, hfilePath);this.archivePath = new Path(archiveDir, hfilePath);setLocations(originPath, tempPath, archivePath); }尼瑪,它計算了三個地址,原始位置,archive中的位置,臨時目錄的位置,按照順序添加到一個locations數組里面。。接著看FSDataInputStreamWrapper吧,下面是三段代碼
this.stream = (link != null) ? link.open(hfs) : hfs.open(path); //走的link.open(hfs) new FSDataInputStream(new FileLinkInputStream(fs, this)); //注意tryOpen方法 public FileLinkInputStream(final FileSystem fs, final FileLink fileLink, int bufferSize)throws IOException {this.bufferSize = bufferSize;this.fileLink = fileLink;this.fs = fs;this.in = tryOpen(); }tryOpen的方法,會按順序打開多個locations列表。。
for (Path path: fileLink.getLocations()) {if (path.equals(currentPath)) continue;try {in = fs.open(path, bufferSize);in.seek(pos);assert(in.getPos() == pos) : "Link unable to seek to the right position=" + pos;if (LOG.isTraceEnabled()) {if (currentPath != null) {LOG.debug("link open path=" + path);} else {LOG.trace("link switch from path=" + currentPath + " to path=" + path);}}currentPath = path;return(in);} catch (FileNotFoundException e) {// Try another file location } } View Code恩,這回終于知道它是怎么出來的了,原來是嘗試打開了三次,直到找到正確的位置。
StoreFile的文件格式到這里就結束了,有點兒遺憾的是HFileBlockIndex沒給大家講清楚。
?
補充:經網友"東岸往事"的提醒,有一個地方寫錯了,在結束一個塊之后,會把它所有的BloomFilter全部輸出,HFileBlockIndex的話,如果滿了默認的128*1024個就輸出二級索引。
具體的的內容在后面說查詢的時候會說,下面先交代一下:
通過看繼承InlineBlockWriter的類,發現了以下信息
1、BlockIndexWriter 不是關閉的情況下,沒有超過默認值128*1024是不會輸出的,每128*1024個HFileBlock 1個二級索引。
HFileBlockIndex包括2層,如果是MetaBlock的HFileBlock是1層。
二級索引 curInlineChunk 在結束了一個塊之后添加一個索引的key(上一個塊的firstKey和這個塊的firstKey的中間值)。
byte[] indexKey = comparator.calcIndexKey(lastKeyOfPreviousBlock, firstKeyInBlock);curInlineChunk.add(firstKey, blockOffset, blockDataSize);
一級索引 rootChunk 輸出一次二級索引之后添加每個HFileBlock的第一個key,這樣子其實二級索引里面是包括是一級索引的所有key的。
firstKey = curInlineChunk.getBlockKey(0); rootChunk.add(firstKey, offset, onDiskSize, totalNumEntries);2、CompoundBloomFilterWriter也就是Bloom Filter,在數據不為空的時候,就會輸出。
?
對于HFileV2的正確的圖,應該是下面這個,但是上面的那個圖看起來好看一點,就保留了。
?
?
轉載于:https://www.cnblogs.com/cenyuhai/p/3722644.html
總結
以上是生活随笔為你收集整理的hbase源码系列(九)StoreFile存储格式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【数据结构】数组和广义表
- 下一篇: String和Date、Timestam