1. 程式人生 > >HBase之CF持久化系列(續1)

HBase之CF持久化系列(續1)

  這一節本來打算講解HRegion的初始化過程中一些比較複雜的流程。不過,考慮前面的博文做的鋪墊並不夠,因此,在這一節,我還是特意來介紹HBase的CF持久化。關於這個話題的整體流程性分析在博文《HBase資料持久化之HRegion.flushcache即CF持久化》中已經介紹過,大家最好先看一下我的上篇博文,然後再看這個系列(這個系列我預計會通過三篇博文來詳細講解)。可以說,《HBase資料持久化之HRegion.flushcache即CF持久化》是CF持久化的概述,而本節是CF持久化過程中關鍵流程的一個詳細描述。由於該系列比較複雜,因此,該系列中涉及到WAL的知識點或者其他的無關的知識點我都略過,單就介紹CF持久化的相關資訊。   由於在上篇博文中,我已經將HRegion.internalPrepareFlushCache流程講解的比較詳細,因此,該系列的三篇文章都從HRegion.internalFlushCacheAndCommit講起。   由於在博文《HBase資料持久化之HRegion.flushcache即CF持久化》中已經介紹過方法HRegion.internalFlushCacheAndCommit中的一些流程,但是某些方法講的比較粗略。因此,我在這一節從DefaultStoreFlusher.flushSnapshot方法講起,下一節從StoreFlusher.finalizeWriter開始講起,第三節我將分析HStore.validateStoreFile。至於CF持久化流程中涉及到其他知識點的,我會在CF持久化系列(雜記)中一一介紹。   在這一節,我不再貼上大量的圖,儘量使用文字來進行描述,必要的時候,我會貼圖來說明。雖然這樣說,但是,由於該系列比較複雜,因此,在本篇部落格,我可能還是會貼不少的圖。   首先,讓我們來到流程性的方法DefaultStoreFlusher.flushSnapshot。這裡有幾個關鍵的步驟。   1.呼叫內部的createScanner方法,構造了StoreScanner,這裡是本節的一個重點。   2.呼叫HStore.createWriterInTmp方法構建了一個StoreFileWriter,該方法的流程在本節中比較複雜,但其功能可以比較簡單的概括——確定CF的目錄檔案,並且在其目錄檔案上建立對應的輸入流(這裡我們只討論檔案系統為HDFS的情況)   3.呼叫內部的performFlush,將每一行資訊寫入到上面建立的StoreFileWriter.HFileWriterImpl.HFileBlock.Writer.userDataStream(大家這裡看的可能比較懵逼,沒有關係,耐心往後面看,這裡是本節的重點,在這裡有一個印象就好)。   4.呼叫內部的finalizeWriter方法,該方法將上一步中儲存的行資訊寫入到HFileWriterImpl.outputStream,包括布隆過濾器的值,blockIndex、File info、FixedFileTrailer中的資訊。   5.將上面獲取到的store.scanInfo關閉   6.將CF的目錄儲存到待返回的結果值中。   在createScanner方法內部,主要呼叫了StoreScanner構造方法,如下圖所示。這裡相對比較複雜。   1.呼叫了內部的另外一個構造方法,完成部分成員變數的初始化。   2.呼叫selectScannersFrom,會根據TLL等條件篩選出一部分scanners   3.呼叫seekScanners,這裡將根據matcher.getStartKey()中的row與family篩選出該scanner。由於其內部使用了NavigableMap型別,以傳入的matcher.getStartKey()為NavigableMap.tailMap的入參,就可以獲得大於等於該值的所有cell。該方法的主要作用就是遍歷入參scanners,並且初始化每個scanner中的關鍵性成員變數iter與current。這裡的具體流程我在接下來還會著重講解。   4.呼叫addCurrentScanners將入參scanners中的值都新增到其成員變數currentScanners中。   5.呼叫resetKVHeap初始化了成員變數heap。這裡的具體流程我也會著重講解。
  首先,讓我們分析方法seekScanners。在該方法內部值得注意的方法是呼叫了scanner.seek(scanner型別為SegmentScanner),這裡的seek方法就是用於初始化scanner的成員變數iter與current。   讓我們來到SegmentScanner.seek,如下圖所示,這裡有兩個比較重要的方法——getIterator、updateCurrent。   1.這裡的getIterator方法比較瑣碎,細講吧,有種雞肋的感覺,不細講吧,卻是一個知識點。不過,為了後面方便理解,我還是不偷懶了,為大家細細講解一番。   2.呼叫updateCurrent,獲得上面iter.next值,並賦給成員變數current。
  這裡首先介紹方法getIterator,如下圖所示。這裡呼叫了Segment.tailSet。   接下來讓我們來到Segment.tailSet。如下圖所示。這裡呼叫getCellSet方法獲得成員變數cellSet中的值,其cellSet型別為AtomicReference。然後呼叫CellSet.tailSet。   然後,我們來到CellSet.tailSet,如下圖所示。這裡我需要說明一點CellSet實現了介面java.util.NavigableSet,而且這裡的delegatee型別為NavigableMap。也就是說,這裡將delegatee中大於等於(入參inclusive為true)fromElement的值封裝為NavigableSet返回,並且作為新構建的CellSet的入參。   到這裡getIterator方法的流程就介紹完了。至此,我們也明白了,返回的iterator就是新構建的CellSet.iterator。
  接下來,讓我們來到updateCurrent。如下圖所示。看到這裡,我相信大家就明白許多了。沒錯,這裡迭代iterator,將其中符合條件的值賦給了成員變數current。   然後,呼叫方法resetKVHeap,將剛剛初始化好的scanners封裝到KeyValueHeap中,並賦給成員變數heap。   這裡,讓我們簡單看一下KeyValueHeap的建構函式,有利於我們在後面的理解。如下圖所示,這裡初始化了其成員變數heap,並且將入參中的scanners一一新增到成員變數heap中。然後呼叫方法pollRealKV,將其返回值用於初始化成員變數current。   在HStore.createWriterInTmp方法中主要構建了StoreFileWriter。首先,讓我們來到StoreFileWriter.Builder.build方法。   1.這裡首先檢視CF所在的目錄是否存在,如果不存在,則建立。   2.在CF所在目錄下指定隨機檔案,該檔名通過UUID隨機生成(注:這裡並沒有建立檔案)。   3.呼叫StoreFileWriter的構造方法。   然後,來到StoreFileWriter的構造方法:   1.其內部首先通過HFile.WriterFactory建立了HFileWriterImpl。   2.呼叫BloomFilterFactory.createGeneralBloomAtWrite建立CompoundBloomFilterWriter,並且賦給成員變數generalBloomFilterWriter   3.這裡我們分析傳入的bloomType為ROW的,可以看到,其使用剛剛建立的generalBloomFilterWriter構建了RowBloomContext   4.同樣,呼叫BloomFilterFactory.createGeneralBloomAtWrite建立CompoundBloomFilterWriter,賦給成員變數deleteFamilyBloomFilterWriter。   上面流程中,最主要的是建立了HFileWriterImpl。接下來讓我們來到HFile.WriterFactory.create。   1.呼叫HFileWriterImpl.createOutputStream建立並獲得檔案的輸出流,注意,這裡的檔案就是在StoreFileWriter.Builder.build中構建的檔案。   在這裡我們只關心檔案系統為HDFS的情況。這裡內部會呼叫FSUtils.create。如下圖所示,這裡通過反射方式呼叫了DistributedFileSystem.create方法。   2.呼叫HFileWriterImpl構造方法。注意,這裡將剛剛獲得的輸出流傳入了HFileWriterImpl的構造方法中(這裡是重點,我在後面將詳細講解)。   讓我們來到HFileWriterImpl的構造方法中,如下圖所示。   1.這裡將傳入的輸出流賦給了成員變數outputStream   2.然後將NoOpDataBlockEncoder.INSTANCE賦給成員變數blockEncoder   3.呼叫了內部方法finishInit,完成了另一部分成員變數的初始化   來到finishInit,如下圖所示,這裡連續完成了成員變數blockWriter、dataBlockIndexWriter、metaBlockIndexWriter的初始化。這裡,我要著重講一下HFileBlock.Writer建構函式。   接下來讓我們來到HFileBlock.Writer的建構函式。如下圖所示,這裡將傳入的dataBlockEncoder賦給了成員變數dataBlockEncoder。另外,分別初始化了dataBlockEncodingCtx、defaultBlockEncodingCtx。由於這裡的dataBlockEncoder實際型別為NoOpDataBlockEncoder,因此,這兩個成員變數最後呼叫的方法其實是一樣的,都呼叫了HFileBlockDefaultEncodingContext構造方法。然後初始化了成員變數baosInMemory,而這個成員變數可以說是本節的核心。   至此,我們就完成了StoreFileWriter的初始化過程。也就是本節的主線——HStore.createWriterInTmp方法呼叫完成。   然後,來到StoreFlusher.performFlush。   1.呼叫scanner.next,獲取下一條行資料,並且在其方法內部呼叫了heap.next,更新了heap.KeyValueScanner.current的值。這裡的scanner型別為StoreScanner,也正是文章一開始構造的那個StoreScanner。該方法的詳細流程我還會在後面談到。   2.接下來呼叫sink.append方法,將入參c中的cell資訊寫入到sink中。大家還記得這裡的sink嗎,他的實際型別是我在上面談到的StoreFileWriter。該方法比較複雜,也是本節中的重點之一,我將在後面講解。   3.將kvs中的值清空,以便下一次迭代。   讓我們首先來到scanner.next,也就是StoreScanner.next的詳細流程。該方法比較長,為了避免講解混亂,我這裡就只介紹方法呼叫中比較重要的兩個方法——heap.peek、heap.next。這裡的heap型別為KeyValueHeap。因此,我們來到KeyValueHeap.peek與KeyValueHeap.next方法,如下圖所示。大家可能忘記了這裡的current的實際型別,我簡單提醒一下,他的實際型別是文章開始談到的SegmentScanner。   1.呼叫SegmentScanner.peek,獲取其成員變數current的值。   2.呼叫SegmentScanner.next,其內部呼叫了方法updateCurrent(我在上面已經提到過),他的作用是更新內部成員變數current的值。   接著,讓我們來到sink.append,也就是StoreFileWriter.append。該方法是本節中最為複雜的,希望大家特別留意。讓我們來到其方法內部,如下圖所示。   1.首先呼叫appendGeneralBloomfilter,其內部呼叫了bloomContext.writeBloom(上面分析過,這裡的bloomContext型別為RowBloomContext),這裡的內容我接下來會詳細講解。   2.呼叫writer.append。這裡的呼叫將cell值寫入到HFileBlock.Writer.userDataStream(也就是上面提到的)。至於詳細的流程,我會詳細講解。   首先,讓我們關注appendGeneralBloomfilter。其內部的呼叫比較簡單,呼叫了bloomContext.writeBloom,這裡的bloomContext型別為RowBloomContext,其內部的bloomFilterWriter型別為CompoundBloomFilterWriter。在方法bloomContext.writeBloom中呼叫了bloomFilterWriter.append。因此,讓我們來到CompoundBloomFilterWriter.append,如下圖所示。   1.這裡首先呼叫enqueueReadyChunk,以確保成員變數chunk仍然有空間寫入,如果沒有,則將其封裝到ReadyChunk中,然後加入到成員佇列readyChunks中,並將chunk置空,以期後面的呼叫將其重新分配。   2.如果成員變數chunk為空,則將當前cell值copy並賦給成員變數firstKeyInChunk   3.呼叫allocateNewChunk,這裡比較重要,後面我還是簡單講一下。   4.將入參cell加入到chunk中,需要注意的是這裡的cell型別為BloomFilterChunk。關於這裡的布隆過濾器的使用,我將在後面專門拿出一章來講解。這裡就不詳細介紹了。   讓我們來到allocateNewChunk,如下圖所示。看到這裡,相信大家就很清楚了。這裡首先構建了BloomFilterChunk(在BloomFilterUtil.createBySize方法內),然後呼叫chunk.allocBloom,為chunk內部的bloom(ByteBuffer型別)分配指定的位元組。   到此,大家對於其中的bloomContext.writeBloom方法應該有了一個比較明確的瞭解。   接下來,我來為大家詳細介紹writer.append,也就是HFileWriterImpl.append。如下圖所示。   1.由於這是首次寫入,因此這裡會呼叫newBlock,這裡主要是後面blockWriter.write的呼叫做準備,具體的細節還是比較重要的,我將放在後面來講解。   2.呼叫blockWriter.write方法,將cell資訊寫入HFileBlock.userDataStream(這裡比較重要)。   3.判斷是否將入參cell賦給成員變數firstCellInBlock   4.將入參cell賦給成員變數lastCell   讓我們首先關注方法newBlock。在其內部主要呼叫了blockWriter.startWriting(BlockType.DATA),這裡的入參為BlockType.DATA,需要格外注意。讓我們來到HFileBlock.startWriting。   1.這裡的baosInMemory在Writer構造時已經實現了初始化。這裡呼叫其reset,為下一次完整的寫入做準備。   2.呼叫baosInMemory.write,這裡已經開始寫入。而HConstants.HFILEBLOCK_DUMMY_HEADER是一個空的header。在後面呼叫finishBlock中,會將這裡的內容填充(這裡的具體內容我會在後面finishBlock中分析,大家有一個概念就好)。   3.將baosInMemory封裝到userDataStream,後面都會通過userDataStream將內容簡介寫入baosInMemory。看到這裡,大家可能就會明白我在上面提到的StoreFileWriter.HFileWriterImpl.HFileBlock.Writer.userDataStream。不清楚也沒有關係,後面我還會詳細講解,這裡是本節的重點。   4.由於入參格式為BlockType.DATA,因此這裡會呼叫dataBlockEncoder.startBlockEncoding。這裡完成了dataBlockEncodingCtx.encoderState的初始化,為後面將cell寫入userDataStream做前期準備。由於我們這裡的dataBlockEncoder型別為NoOpDataBlockEncoder,因此,接下來,我們來到NoOpDataBlockEncoder.startBlockEncoding。這裡需要注意的是,第一個入參為成員變數dataBlockEncodingCtx,二個入參為封裝了baosInMemory的userDataStream。後面還會用到這兩個變數,希望大家緊記。   如下圖所示,我們來到NoOpDataBlockEncoder.startBlockEncoding。   1.這裡將入參中的out封裝到新構造的encoder   2.將封裝了剛剛構造的encoder封裝並賦給入參blkEncodingCtx.encoderState,也就是說,這裡的blkEncodingCtx.encoderState將入參中的out也封裝進去了。   到這裡,方法newBlock的呼叫就完成了,這裡主要為後面資料的寫入做了前期的準備。   接下來我們來分析blockWriter.write。   1.這裡的userDataStream的就是上面剛剛構造的,其封裝了baosInMemory   2.呼叫了dataBlockEncoder.encode,這裡的第二個入參就是上面介紹的成員變數dataBlockEncodingCtx。當然,這裡的dataBlockEncoder型別為NoOpDataBlockEncoder。   因此,接下來,讓我們來到NoOpDataBlockEncoder.encode。如下圖所示,這裡獲得入參的encodingCtx.encoderState。然後獲取其encoder,並且呼叫encoder.write。   讓我們來到NoneEncoder.write,如下圖所示。這裡我將其中的構造方法也順帶粘了過來。這裡呼叫KeyValueUtil.oswrite。這裡的out就是上面的userDataStream。   在KeyValueUtil.oswrite完成了將cell中內容寫入到out的功能。其流程還是一個知識點,我這裡還是講解一番。   我這裡只介紹入參cell型別為NoTagByteBufferChunkKeyValue。在KeyValueUtil.oswrite方法中,由於NoTagByteBufferChunkKeyValue實現了ExtendedCell介面。   因此,在KeyValueUtil.oswrite方法中,僅僅呼叫了((ExtendedCell)cell).write(out, withTags)方法。如下圖所示,這裡呼叫了ByteBufferUtils.copyBufferToStream(該方法會多次呼叫)。   接下來讓我們來到ByteBufferUtils.copyBufferToStream。這裡的入參out型別為ByteBufferWriterDataOutputStream,其實現了ByteBufferWriter介面。因此,這裡呼叫了ByteBufferWriterDataOutputStream.write方法。   讓我們來到ByteBufferWriterDataOutputStream.write。如下圖所示,這裡又呼叫了上面的方法ByteBufferUtils.copyBufferToStream,不過這裡的out型別與上面的out的型別不同了,這裡的out型別為ByteArrayOutputStream。也就是上面提到的userDataStream封裝的baosInMemory。也就是說,這裡然後呼叫了ByteArrayOutputStream.write。讓我們進一步分析。   來到ByteArrayOutputStream.write。   1.這裡檢查成員變數buf是否足以容納extra長度的位元組,如果不滿足,則從新分配、拷貝,如果已經滿足,則不再進行任何操作。這裡的詳細邏輯我就不詳述了。   2.呼叫ByteBufferUtils.copyFromBufferToArray將入參b中的內容拷貝到成員變數buf中。   到此,我們就分析完了blockWriter.write的詳細流程。   也就是說,這裡就將cell中的內容寫到了HFileBlock.userDataStream,也就是HFileBlock.baosInMemory中。從整體流程上來講,我們就完成了StoreFlusher.performFlush。   本節就分析到這裡。下一節我將從StoreFlusher.finalizeWriter開始分析。