HBase內(nèi)存管理之MemStore的實(shí)現(xiàn)原理和優(yōu)化
Java工程中內(nèi)存管理總是一個(gè)繞不過(guò)去的知識(shí)模塊,無(wú)論HBase、Flink還是Spark等,如果使用的JVM堆比較大同時(shí)對(duì)讀寫(xiě)延遲等性能有較高要求,一般都會(huì)選擇自己管理內(nèi)存,而且一般都會(huì)選擇使用部分堆外內(nèi)存。
HBase系統(tǒng)中有兩塊大的內(nèi)存管理模塊,一塊是MemStore ,一塊是BlockCache,這兩塊內(nèi)存的管理在HBase的版本迭代過(guò)程中不斷進(jìn)行過(guò)各種優(yōu)化,接下來(lái)筆者結(jié)合自己的理解,將這兩個(gè)模塊的內(nèi)存管理迭代過(guò)程通過(guò)幾篇文章梳理一遍,相信很多優(yōu)化方案在各個(gè)系統(tǒng)中都有,舉一反三,個(gè)人覺(jué)得對(duì)內(nèi)核開(kāi)發(fā)有很大的學(xué)習(xí)意義。本篇文章重點(diǎn)集中介紹MemStore內(nèi)存管理優(yōu)化。
基于跳表實(shí)現(xiàn)的MemStore基礎(chǔ)模型
實(shí)現(xiàn)MemStore模型的數(shù)據(jù)結(jié)構(gòu)是SkipList(跳表),跳表可以實(shí)現(xiàn)高效的查詢(xún)\插入\刪除操作,這些操作的期望復(fù)雜度都是O(logN)。另外,因?yàn)樘肀举|(zhì)上是由鏈表構(gòu)成,所以理解和實(shí)現(xiàn)都更加簡(jiǎn)單。這是很多KV數(shù)據(jù)庫(kù)(Redis、LevelDB等)使用跳表實(shí)現(xiàn)有序數(shù)據(jù)集合的兩個(gè)主要原因。跳表數(shù)據(jù)結(jié)構(gòu)不再贅述,網(wǎng)上有比較多的介紹,可以參考。
JDK原生自帶的跳表實(shí)現(xiàn)目前只有ConcurrentSkipListMap(簡(jiǎn)稱(chēng)CSLM,注意:ConcurrentSkipListSet是基于ConcurrentSkipListMap實(shí)現(xiàn)的)。ConcurrentSkipListMap是JDK Map的一種實(shí)現(xiàn),所以本質(zhì)上是一種Map,不過(guò)這個(gè)Map中的元素是有序的。這個(gè)有序的保證就是通過(guò)跳表實(shí)現(xiàn)的。ConcurrentSkipListMap的結(jié)構(gòu)如下圖所示:

基于ConcurrentSkipListMap這樣的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),按照最簡(jiǎn)單的思路來(lái)看,如果寫(xiě)入一個(gè)KeyValue到MemStore中,肯定是如下的寫(xiě)入步驟:
- 在JVM堆中為KeyValue對(duì)象申請(qǐng)一塊內(nèi)存區(qū)域。
- 調(diào)用ConcurrentSkipListMap的put(K key, V value)方法將這個(gè)KeyValue對(duì)象作為參數(shù)傳入。

圖1 基于跳表實(shí)現(xiàn)的最基礎(chǔ)MemStore模型
對(duì)吧,這樣的話(huà),實(shí)現(xiàn)非常簡(jiǎn)單。根據(jù)Key查詢(xún)可以利用跳表的有序性。但是這樣的內(nèi)存存儲(chǔ)模型會(huì)有很多問(wèn)題(具體見(jiàn)下節(jié)),本文就基于這個(gè)最原始的模型開(kāi)始優(yōu)化之旅。
再看圖1這個(gè)存儲(chǔ)模型,可以發(fā)現(xiàn)MemStore從內(nèi)存管理上來(lái)說(shuō)主要由兩個(gè)部分組成,一個(gè)是原生KeyValue的內(nèi)存管理,見(jiàn)上圖下半部分;一個(gè)是ConcurrentSkipListMap的內(nèi)存管理,見(jiàn)上圖上半部分。接下來(lái)筆者分別就這兩個(gè)部分的內(nèi)存管理優(yōu)化,分成兩個(gè)小節(jié)進(jìn)行深入介紹。
MemStore中原生KeyValue對(duì)象內(nèi)存存儲(chǔ)優(yōu)化
對(duì)于HBase這樣基于LSM實(shí)現(xiàn)的MemStore來(lái)說(shuō),上述實(shí)現(xiàn)方案每寫(xiě)入一個(gè)KeyValue,在沒(méi)有寫(xiě)入ConcurrentSkipList之前就需要申請(qǐng)一個(gè)內(nèi)存對(duì)象,可以想見(jiàn),對(duì)于很多寫(xiě)入吞吐量幾萬(wàn)每秒的業(yè)務(wù)來(lái)說(shuō),每秒就會(huì)有幾萬(wàn)個(gè)內(nèi)存對(duì)象產(chǎn)生,這些對(duì)象會(huì)在內(nèi)存中存在很長(zhǎng)一段時(shí)間,對(duì)應(yīng)的會(huì)晉升到老生代,一旦執(zhí)行了flush操作,老生代的這些對(duì)象會(huì)被GC回收掉。這樣的內(nèi)存玩法,會(huì)導(dǎo)致JVM的GC壓力非常大。GC壓力主要來(lái)源于:這些內(nèi)存對(duì)象一旦晉升到老生代,執(zhí)行完Major GC后會(huì)存在大量的非常小的內(nèi)存碎片,這些內(nèi)存碎片會(huì)引起頻繁的Full GC,而且每次Full GC的時(shí)間會(huì)異常的長(zhǎng)。
- MemStore引入MemStoreLAB
針對(duì)上面的問(wèn)題,MemStore借鑒TLAB(Thread Local Allocation Buffer)機(jī)制,實(shí)現(xiàn)了MemStoreLAB,簡(jiǎn)稱(chēng)MSLAB?;贛SLAB實(shí)現(xiàn)寫(xiě)入的核心流程如下:
- 一個(gè)KeyValue寫(xiě)入之后不再單獨(dú)為KeyValue申請(qǐng)內(nèi)存,而是提前申請(qǐng)好一個(gè)2M大小的內(nèi)存區(qū)域(Chunk)。
- 將寫(xiě)入的KeyValue順序復(fù)制到申請(qǐng)的Chunk中,一旦Chunk寫(xiě)滿(mǎn),再申請(qǐng)下一個(gè)Chunk。
- 將KeyValue復(fù)制到Chunk中后,生成一個(gè)Cell對(duì)象(這個(gè)Cell對(duì)象在源碼中為ByteBufferChunkKeyValue),這個(gè)Cell對(duì)象指向Chunk中的KeyValue內(nèi)存區(qū)域。
- 將這個(gè)Cell對(duì)象作為Key和Value寫(xiě)入ConcurrentSkipListMap中。
- 原生的KeyValue對(duì)象寫(xiě)入到Chunk之后就沒(méi)有再被引用,所以很快就會(huì)被Young GC回收掉。
基于MSLAB的MemStore可以表征為下圖:

圖2 基于MSLAB實(shí)現(xiàn)的MemStore示意圖
對(duì)比圖1和圖2,引入MSLAB之后MemStore實(shí)現(xiàn)稍顯復(fù)雜,后者引入了兩個(gè)長(zhǎng)壽內(nèi)存對(duì)象,一個(gè)是2M的Chunk對(duì)象,一個(gè)是指向KV內(nèi)存區(qū)域的Cell對(duì)象,這兩種內(nèi)存對(duì)象都會(huì)晉升到老生代。這里分別針對(duì)這兩個(gè)內(nèi)存對(duì)象進(jìn)行解讀:
- 引入2M大小的Chunk對(duì)象之后,數(shù)據(jù)寫(xiě)入就不再需要為每個(gè)KeyValue申請(qǐng)一個(gè)內(nèi)存對(duì)象,這樣可以大大降低內(nèi)存碎片的產(chǎn)生。
- 那Cell對(duì)象不會(huì)引起內(nèi)存碎片?這個(gè)筆者查閱了很多資料都沒(méi)有找到相關(guān)的說(shuō)明,個(gè)人理解是因?yàn)镃ell相對(duì)原生KeyValue來(lái)說(shuō)占用內(nèi)存小的多,可以一定程度上可以忽略。Cell與KeyValue對(duì)象分別占用內(nèi)存大小如下所示:
ByteBufferChunkKeyValue類(lèi)(Cell對(duì)象)的字段如下:
- protected final ByteBuffer buf;
- protected final int offset;
- protected final int length;
- private long seqId = 0;
對(duì)象大小可以表示為對(duì)象頭和各個(gè)字段的大小總和,其中對(duì)象頭占16Byte,Reference類(lèi)型占8Byte,int類(lèi)型占4Byte,long類(lèi)型占8Byte,Cell對(duì)象大小可以表示為:
- ClassSize.OBJECT
- + ClassSize.REFERENCE
- + (2 * Bytes.SIZEOF_INT)
- + Bytes.SIZEOF_LONG
- = 16 + 8 + 2 * 4 + 8 = 40 Byte。
原生KeyValue對(duì)象大小為:
- ClassSize.OBJECT(16)
- + Key Length(4)
- + Value Length(4)
- + Row Length(2)
- + FAMILY LENGTH(1)
- + TIMESTAMP_TYPE LENGTH(9)
- + length(row) + length(column family)
- + length(column qualifier)
- + length(value)
- =
- 36
- + length(row) + length(column family)
- + length(column qualifier) + length(value)
按照一個(gè)KV中l(wèi)ength(row) + length(column family) + length(column qualifier) + length(value)總計(jì)84Byte算,原生KeyValue對(duì)象大小為120Byte,為Cell對(duì)象的3倍。
引入MSLAB后一定程度上降低了老生代內(nèi)存碎片的產(chǎn)生,進(jìn)而降低了Promotion Failure類(lèi)型的Full GC產(chǎn)生。那還有沒(méi)有進(jìn)一步的優(yōu)化空間呢?針對(duì)Chunk,還有兩個(gè)大的優(yōu)化思路:
- MemStore引入ChunkPool
MSLAB機(jī)制中KeyValue寫(xiě)入Chunk,如果Chunk寫(xiě)滿(mǎn)了會(huì)在JVM堆內(nèi)存申請(qǐng)一個(gè)新的Chunk。引入ChunkPool后,申請(qǐng)Chunk都從ChunkPool中申請(qǐng),如果ChunkPool中沒(méi)有可用的空閑Chunk,才會(huì)從JVM堆內(nèi)存中申請(qǐng)新Chunk。如果一個(gè)MemStore執(zhí)行flush操作后,這個(gè)MemStore對(duì)應(yīng)的所有Chunk都可以被回收,回收后重新進(jìn)入池子中,以備下次使用?;驹砣鐖D3、圖4所示:

圖3 基于ChunkPool實(shí)現(xiàn)的Chunk管理模型
每個(gè)RegionServer會(huì)有一個(gè)全局的Chunk管理器,負(fù)責(zé)Chunk的生成、回收等。MemStore申請(qǐng)Chunk對(duì)象會(huì)發(fā)送請(qǐng)求讓Chunk管理器創(chuàng)建新Chunk,Chunk管理器會(huì)檢查當(dāng)前是否有空閑Chunk,如果有空閑Chunk,就會(huì)將這個(gè)Chunk對(duì)象分配給MemStore,否則從JVM堆上重新申請(qǐng)。每個(gè)MemStore僅持有Chunk內(nèi)存區(qū)域的引用,如圖3中MemStoreLAB的小格子。
下圖是MemStore執(zhí)行Flush之后,對(duì)應(yīng)的所有Chunk對(duì)象中KV落盤(pán)形成HFile,這部分Chunk就可以被Chunk管理器回收到空閑池子。

圖4 MemStore Flush過(guò)程中Chunk回收過(guò)程
使用ChunkPool的好處是什么呢?因?yàn)镃hunk可以回收再使用,這就一定程度上降低了Chunk對(duì)象申請(qǐng)的頻率,有利于Young GC。
- MemStore Offheap實(shí)現(xiàn)
除過(guò)ChunkPool之外,HBase 2.x版本針對(duì)Chunk對(duì)象優(yōu)化的另一個(gè)思路是將Chunk使用的這部分內(nèi)存堆外化。關(guān)于堆外內(nèi)存的細(xì)節(jié)內(nèi)容,筆者將會(huì)在下篇文章重點(diǎn)分析,這篇文章只做個(gè)簡(jiǎn)單介紹。
Chunk堆外化實(shí)現(xiàn)比較簡(jiǎn)單,在創(chuàng)建新Chunk時(shí)根據(jù)用戶(hù)配置選擇是否使用堆外內(nèi)存,如果使用堆外內(nèi)存,就使用JDK提供的ByteBuffer.allocateDirect方法在堆外申請(qǐng)?zhí)囟ù笮〉膬?nèi)存區(qū)域,否則使用ByteBuffer.allocate方法在堆內(nèi)申請(qǐng)。如果不做配置,默認(rèn)使用堆內(nèi)內(nèi)存,用戶(hù)可以設(shè)置hbase.regionserver.offheap.global.memstore.size這個(gè)值為大于0的值開(kāi)啟堆外,表示RegionServer中所有MemStore可以使用的堆外內(nèi)存總大小。
- 原生KeyValue對(duì)象內(nèi)存存儲(chǔ)優(yōu)化總結(jié)
基于原生KeyValue直接寫(xiě)入ConcurrentSkipListMap方案,HBase在之后的版本中不斷優(yōu)化,針對(duì)原生KeyValue內(nèi)存管理部分分別采用MemStoreLAB機(jī)制、ChunkPool機(jī)制以及Chunk Offheap機(jī)制三種策略,對(duì)GC性能進(jìn)行持續(xù)優(yōu)化。
第一節(jié)我們提到MemStore內(nèi)存管理分為原生KeyValue內(nèi)存管理和ConcurrentSkipListMap內(nèi)存管理兩個(gè)部分,第二節(jié)重點(diǎn)介紹了HBase針對(duì)原生KeyValue內(nèi)存管理所采用的3種優(yōu)化方案。接下來(lái)第三節(jié)首先介紹JDK原生ConcurrentSkipListMap在內(nèi)存管理方面的主要問(wèn)題,以及HBase在2.x版本以及3.x版本針對(duì)ConcurrentSkipListMap內(nèi)存管理問(wèn)題進(jìn)行優(yōu)化的兩個(gè)方案。
ConcurrentSkipListMap數(shù)據(jù)結(jié)構(gòu)存在的問(wèn)題以及優(yōu)化方案
一個(gè)KV在MemStore中的旅程
經(jīng)過(guò)上面知識(shí)的鋪墊我們知道,一個(gè)KV寫(xiě)入MemStore會(huì)經(jīng)過(guò)如下幾個(gè)核心步驟:
- 在Chunk中申請(qǐng)一段與KV相同大小的內(nèi)存空間將KV拷貝進(jìn)去。
- 生成一個(gè)Cell對(duì)象,該對(duì)象包含指向Chunk中對(duì)應(yīng)數(shù)據(jù)塊的指針、offsize以及l(fā)ength。
- 將這個(gè)Cell對(duì)象分別作為Key和Value插入到CSLM表示的跳表Map中。
有了CSLM這樣的跳表之后,查詢(xún)就可以在O(N)時(shí)間復(fù)雜度內(nèi)完成。但是,JDK實(shí)現(xiàn)的CSLM跳表在內(nèi)存使用方面有些粗糙,導(dǎo)致內(nèi)存中產(chǎn)生了大量意義不大的Java對(duì)象,這些Java對(duì)象的頻繁產(chǎn)生一方面導(dǎo)致內(nèi)存效率使用比較低,另一方面會(huì)引起比較嚴(yán)重的Java GC。為什么JDK實(shí)現(xiàn)的CSLM跳表會(huì)有這樣的問(wèn)題?接著往下看。
- MemStore ConcurrentSkipListMap數(shù)據(jù)結(jié)構(gòu)存在的問(wèn)題
原生ConcurrentSkipListMap邏輯示意圖如下圖所示:

圖5 CSLM示意圖
JDK自帶的CSLM每個(gè)節(jié)點(diǎn)都是一個(gè)對(duì)象,其中最底層節(jié)點(diǎn)是Node對(duì)象,其他上層節(jié)點(diǎn)是Index對(duì)象。Node對(duì)象和Index對(duì)象的核心字段可以參考CSLM源碼實(shí)現(xiàn):

其中Node對(duì)象由一個(gè)key、一個(gè)value以及指向下一個(gè)Node節(jié)點(diǎn)的引用組成。Index對(duì)象由一個(gè)Node節(jié)點(diǎn)引用、向下和向右的引用組成。
根據(jù)上述代碼可以知道:
- 每個(gè)Node對(duì)象有3個(gè)引用變量,分別指向Key(Cell對(duì)象)、Value(Cell對(duì)象)以及Next Node。
- 每個(gè)Index對(duì)象有3個(gè)引用變量,分別指向代表的Node節(jié)點(diǎn),下層Index節(jié)點(diǎn)以及右側(cè)Index節(jié)點(diǎn)。
假設(shè)業(yè)務(wù)寫(xiě)入50M規(guī)模的KV,那寫(xiě)入到MemStore后,除了正常存儲(chǔ)KV數(shù)據(jù)占用的Chunk對(duì)象外,CSLM占用的對(duì)象和內(nèi)存分別有多少呢?
對(duì)象數(shù):50M個(gè)Node對(duì)象,假如跳表中l(wèi)evel N層的Index節(jié)點(diǎn)個(gè)數(shù)是50M/2^(N+2),那么總共會(huì)有50M/4個(gè)Index對(duì)象。整個(gè)CSLM一共有62.5M個(gè)對(duì)象。
內(nèi)存占用情況(均認(rèn)為JVM設(shè)置大于32G,未開(kāi)啟壓縮指針):
(1)Node對(duì)象
- 50M * (ClassSize.OBJECT + ClassSize.REFERENCE + ClassSize.REFERENCE + ClassSize.REFERENCE) =50M * (16 + 8 + 8 + 8) = 2000M
(2)Index對(duì)象
- 12.5M * (ClassSize.OBJECT + ClassSize.REFERENCE + ClassSize.REFERENCE + ClassSize.REFERENCE) =12.5M * (16 + 8 + 8 + 8) = 500M
總內(nèi)存占用2500M。假設(shè)業(yè)務(wù)寫(xiě)入的KV為50Byte,那總的數(shù)據(jù)量為2500M。為了存儲(chǔ)這2500M大小的數(shù)據(jù),MemStore又產(chǎn)生了額外的2500M內(nèi)存。
- CompactingMemStore如何優(yōu)化這個(gè)困境
CompactingMemStore的核心工作原理如圖所示:

圖6 CompactingMemStore核心工作原理示意圖
- 一個(gè)Cell寫(xiě)入到Region后會(huì)先寫(xiě)入MutableSegment中。MutableSegment可以認(rèn)為就是一個(gè)小的MemStore,MutableSegment包含一個(gè)MSLAB存儲(chǔ)Chunk,同時(shí)包含一個(gè)ConcurrentSkipListMap。
- 默認(rèn)情況下一旦MutableSegment的大小超過(guò)2M,就會(huì)執(zhí)行In-memory Flush操作,將MutableSegment變?yōu)镮mmutableSegment,并重新生成一個(gè)新的MutableSegment接收寫(xiě)入。ImmutableSegment有多個(gè)實(shí)現(xiàn)類(lèi),In-memory Flush生成的ImmutableSegment為CSLMImmutableSegment,可以預(yù)見(jiàn)這個(gè)ImmutableSegment在數(shù)據(jù)結(jié)構(gòu)上也是使用CSLM。
- 每次執(zhí)行完In-memory Flush之后,RegionServer都會(huì)啟動(dòng)一個(gè)異步線(xiàn)程執(zhí)行In-memory Compaction。In-memory Compaction的本質(zhì)是將CSLMImmutableSegment變?yōu)镃ellArrayImmutableSegment或者CellChunkImmutableSegment,這才是CompactingMemStore最核心的地方。那什么是CellArrayImmutableSegment/CellChunkImmutableSegment呢?為什么要做這樣的轉(zhuǎn)換?接著往下看。
- In-memory Compaction機(jī)制
現(xiàn)在我們需要回過(guò)頭來(lái)想想這兩個(gè)問(wèn)題:
- 為什么要將一個(gè)大的MemStore切分成這么多小的Segment?這么設(shè)計(jì)的初衷是為In-memory Compaction做準(zhǔn)備,只有將MemStore分為MutableSegment和ImmutableSegment,才可能基于ImmutableSegment進(jìn)行內(nèi)存優(yōu)化。
- 如何對(duì)ImmutableSegment進(jìn)行內(nèi)存優(yōu)化?答案是將CSLMImmutableSegment變?yōu)镃ellArrayImmutableSegment或者CellChunkImmutableSegment。通過(guò)上文的介紹我們知道,CSLM這種數(shù)據(jù)結(jié)構(gòu)對(duì)內(nèi)存并不友好,因?yàn)镮mmutableSegment本身已經(jīng)不再接收任何更新刪除寫(xiě)入操作,只允許讀操作,這樣的話(huà)CSLM就可以轉(zhuǎn)換為對(duì)內(nèi)存更加友好的Array或者其他的數(shù)據(jù)結(jié)構(gòu)。這個(gè)轉(zhuǎn)換就是In-memory Compaction。
理清楚上面兩個(gè)問(wèn)題,我們?cè)賮?lái)看看In-memory Compaction的主要流程。如果參與Compaction的Segment只有一個(gè),我們稱(chēng)之為Flatten,非常形象,就是將CSLM拉平為Array或者Chunk,示意圖如下圖圖7所示。

圖7 In-Memory Compaction之Flatten示意圖
緊接著就有兩個(gè)問(wèn)題:
- CSLMImmutableSegment是如何拉平成CellArrayImmutableSegment?
- CellArrayImmutableSegment和CellChunkImmutableSegment分別是什么樣的數(shù)據(jù)結(jié)構(gòu)?
CSLMImmutableSegment拉平成CellArrayImmutableSegment比較容易理解,順序遍歷CSLMImmutableSegment讀取出對(duì)應(yīng)的Cell,順序?qū)懭胍粋€(gè)申請(qǐng)好的數(shù)組即可。所以直觀(guān)上看,CSLMImmutableSegment和CellArrayImmutableSegment相比就是將CSLM變成了一個(gè)數(shù)組。
那CellArrayImmutableSegment能不能進(jìn)一步優(yōu)化呢?是不是可以將Array[Cell]這樣一個(gè)在內(nèi)存中不完全連續(xù)的對(duì)象轉(zhuǎn)變成一塊完全連續(xù)內(nèi)容空間的對(duì)象,這種優(yōu)化方式也比較自然,借鑒Chunk思路申請(qǐng)一塊2M的大內(nèi)存空間,遍歷數(shù)組中的Cell對(duì)象,將其順序拷貝到這個(gè)Chunk(這種Chunk稱(chēng)為Index Chunk,區(qū)別與存儲(chǔ)KV數(shù)據(jù)的Data Chunk)中,就變成了CellChunkImmutableSegment,將內(nèi)存由不連續(xù)變?yōu)檫B續(xù)的一大好處就是變換后連續(xù)的內(nèi)存區(qū)域可以在堆外管理,默認(rèn)情況下In-memory Compaction會(huì)直接將CSLMImmutableSegment拉平成CellChunkImmutableSegment。
上述過(guò)程是只有一個(gè)Segment參與Compaction的流程。如果參與Compaction的Segment個(gè)數(shù)超過(guò)1個(gè),會(huì)有兩種Compaction的形式:Merge和Compact。先說(shuō)Merge,Merge的處理流程和Flatten基本一致,如In-Memory Compaction之Merge示意圖所示,左側(cè)兩個(gè)Segment進(jìn)行合并形成右側(cè)一個(gè)大的CellChunkImmutableSegment,合并過(guò)程就是順序遍歷左邊兩個(gè)Segment,取出對(duì)應(yīng)的Cell,然后順序?qū)懭胗疫匔ellChunkImmutableSegment中。

圖8 In-Memory Compaction之Merge示意圖
Compaction與Merge的處理流程基本相同,不同的是,Compaction在合并的過(guò)程中順序遍歷左邊兩個(gè)Segment,讀取對(duì)應(yīng)Cell之后會(huì)檢測(cè)是否有多個(gè)版本的Cell,如果存在超過(guò)設(shè)置版本數(shù)的Cell,就將老版本的Cell刪掉。因?yàn)榇嬖谠糑V的變更,所以新生成的Data Chunk會(huì)進(jìn)行重建,這是和Merge最大的不同。如In-Memory Compaction之Compaction示意圖所示,右側(cè)新生成的CellChunkImmutableSegment的Data Chunk是經(jīng)過(guò)重建過(guò)的。

圖9 In-Memory Compaction之Compaction示意圖
CompactingMemStore通過(guò)將CSLM數(shù)據(jù)結(jié)構(gòu)變成Array或者Chunk,優(yōu)化了CSLM數(shù)據(jù)結(jié)構(gòu)本身內(nèi)存利用效率低的問(wèn)題,提升GC效率。另外,提升內(nèi)存利用率可以使MemStore中存儲(chǔ)下更多的KV數(shù)據(jù),進(jìn)而減少Flush和Compaction發(fā)生的頻率,提升整個(gè)HBase集群的性能。
CCSMap又是如何優(yōu)化CSLM?
CCSMap全稱(chēng)CompactedConcurrentSkipListMap,是阿里巴巴內(nèi)部版本為了優(yōu)化CSLM數(shù)據(jù)結(jié)構(gòu)內(nèi)存利用效率低所實(shí)現(xiàn)的一個(gè)新的數(shù)據(jù)結(jié)構(gòu)。CCSMap數(shù)據(jù)結(jié)構(gòu)的基本理念是將原生的ConcurrentSkipListMap進(jìn)行壓縮,壓縮的直觀(guān)效果如下圖所示:

圖10 CCSMap數(shù)據(jù)結(jié)構(gòu)邏輯示意圖
上圖中上面結(jié)構(gòu)是原生的CSLM數(shù)據(jù)結(jié)構(gòu),下面是CCSMap數(shù)據(jù)結(jié)構(gòu),很明顯,主要是將Index對(duì)象壓縮到了Node對(duì)象上,數(shù)據(jù)寫(xiě)入/讀取流程和CSLM基本上一致。壓縮后可以抹掉Index對(duì)象,但是這樣的優(yōu)化顯然不是全部。接下來(lái)的優(yōu)化才是重點(diǎn)。
CCSMap數(shù)據(jù)結(jié)構(gòu)可以認(rèn)為只有一個(gè)Node對(duì)象(Index可以理解為Node對(duì)象的一個(gè)字段),既然只有一種對(duì)象,是否可以借鑒MSLAB的思路將Node對(duì)象順序存儲(chǔ)到固定大小的Chunk中,這樣做的好處顯而易見(jiàn):整個(gè)Chunk可以以大塊申請(qǐng),同時(shí)可以在堆外申請(qǐng)。CCSMap就是基于這個(gè)思路進(jìn)行的物理存儲(chǔ)設(shè)計(jì),筆者根據(jù)相關(guān)Jira上給出的資料以及閱讀源代碼畫(huà)出來(lái)的一個(gè)物理存儲(chǔ)示意圖:

圖11 CCSMap數(shù)據(jù)結(jié)構(gòu)物理實(shí)現(xiàn)示意圖
這里有個(gè)認(rèn)知的轉(zhuǎn)換,在CSLM數(shù)據(jù)結(jié)構(gòu)下,KV就是KV,但是在CCSMap數(shù)據(jù)結(jié)構(gòu)下,KV需要包裝成Node對(duì)象,Node對(duì)象的核心字段如下:

Node對(duì)象除了正常的KV Data之外,還有幾個(gè)比較重要的字段,meta字段主要存儲(chǔ)level,dataLen存儲(chǔ)數(shù)據(jù)大小,nextNode存儲(chǔ)當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn),levelIndex是一個(gè)數(shù)組,表示這個(gè)Node在各個(gè)Level上Index指向的Node(NodeId)。這樣一個(gè)Node對(duì)象就可以完全表征邏輯示意圖中Node節(jié)點(diǎn)。
當(dāng)然在具體實(shí)現(xiàn)中如何根據(jù)Index存儲(chǔ)的一個(gè)long類(lèi)型的NodeId在CCSMap中找到對(duì)應(yīng)的Node,這里面有個(gè)小小的技巧,就是這個(gè)long類(lèi)型的NodeId前4Byte表示ChunkId,后4Byte表示對(duì)應(yīng)Node在指定Chunk上的偏移量,這樣就可以根據(jù)NodeId輕松讀取到這個(gè)Node對(duì)應(yīng)的內(nèi)存空間。
根據(jù)物理存儲(chǔ)示意圖,KV數(shù)據(jù)寫(xiě)入的時(shí)候會(huì)首先包裝成一個(gè)Node對(duì)象,包裝的過(guò)程主要是生成level字段,然后根據(jù)跳表規(guī)則不斷查找,確定對(duì)應(yīng)的nextNode和levelIndex[]兩個(gè)字段。Node對(duì)象封裝好之后就可以順序持久化到Chunk中。
CCSMap相比CSLM可以節(jié)省多少對(duì)象?多少內(nèi)存?
- 減少多少對(duì)象?CCSMap將ConcurrentSkipListMap和KV對(duì)象一起放到Chunk中去了,所以沒(méi)有任何對(duì)象開(kāi)銷(xiāo),這點(diǎn)比原生的CSLM優(yōu)秀了非常多。
- 減少了多少內(nèi)存?還是假設(shè)業(yè)務(wù)寫(xiě)入50M規(guī)模的KV,CCSMap中Node對(duì)象中l(wèi)ong[] levelIndex會(huì)占用12.5M * 8Byte = 100M,另外,dataLen占用4Byte,nextNode占用8Byte,meta占用4Byte,總共占用50 * 16Byte = 800M。所以總計(jì)占用900M。相比CSLM的2500M,降低了64%。
MemStore內(nèi)存進(jìn)化總結(jié)
基于上述長(zhǎng)篇大論,我們知道MemStore的內(nèi)存主要分為兩部分,其中一部分是KV存儲(chǔ)本身,一部分是CSLM。文中第二節(jié)重點(diǎn)介紹了KV存儲(chǔ)本身的幾個(gè)優(yōu)化思路,包括MSLAB、ChunkPool以及Chunk Offheap等,第三節(jié)分別重點(diǎn)介紹了使用CompactingMemStore和CCSMap兩種機(jī)制對(duì)CSLM數(shù)據(jù)結(jié)構(gòu)進(jìn)行優(yōu)化的原理。其實(shí),優(yōu)化來(lái)優(yōu)化去,最核心的落腳點(diǎn)還是能不能將對(duì)象順序持久化到連續(xù)一段內(nèi)存(Chunk)上,抓住這個(gè)最終落腳點(diǎn)非常重要。
最后說(shuō)點(diǎn)題外話(huà),前一段時(shí)間JDK13發(fā)布,進(jìn)一步增強(qiáng)了ZGC特性。ZGC應(yīng)該是Java歷史上最大的改進(jìn)之一,這點(diǎn)應(yīng)該沒(méi)有任何疑問(wèn),TB級(jí)別的堆可以保證GC時(shí)間低于10ms,實(shí)際場(chǎng)景中128G內(nèi)存最大GC時(shí)間才1.68ms。如果真是這樣的GC性能,可能很多現(xiàn)在我們做的各種內(nèi)存管理優(yōu)化在很多年之后都不再是問(wèn)題。