天天看點

hbase源碼系列(十三)緩存機制MemStore與Block Cache

這一章講hbase的緩存機制,這裡面涉及的内容也是比較多,呵呵,我了解中的緩存是儲存在記憶體中的特定的便于檢索的資料結構就是緩存。

之前在講put的時候,put是被添加到store裡面,這個store是個接口,實作是在hstore裡面,memstore其實是它底下的小子。

那它和region server、region是什麼關系?

region server下面有若幹個region,每個region下面有若幹的列族,每個列族對應着一個hstore。

hstore裡面有三個很重要的類,在這章的内容都會提到。

memstore是存儲着兩個有序的kv集合,kv進來先寫到裡面,超過閥值之後就會寫入硬碟。

cacheconf是針對hfileblock的緩存,專門用來緩存快,預設是在讀的時候緩存塊,也可以修改列族的參數,讓它在寫的時候也緩存,這個在資料模型定義的時候提到過。

storeengine是storefile的管理器,它管理着這個列族對應的所有storefiles。

memstore比較有意思,我們先看它的add方法,這個是入口。

先把kv放到maybeclonewithallocator裡面複制出來一個新的kv,然後再走internaladd的方法,為啥要這麼搞呢?

先看maybeclonewithallocator,我們慢慢看,沒關系。

<b>allocator是何許人也,它是一個memstorelab,它是幹啥的呀,這個讓人很糾結呀?</b>

<b>下面看看getormakechunk看看是啥情況,挺疑惑的東西。</b>

<b>chunk是一個持有一個byte[]數組的資料結構,屬性如下。</b>

不管怎麼樣吧,把多個小的kv寫到一個連續的數組裡面可能是好點好處吧,下面講一下它的相關參數吧。

那我們繼續講講這個memstorechunkpool吧,它預設是不被開啟的,因為它的參數hbase.hregion.memstore.chunkpool.maxsize預設是0 (隻允許輸入0-&gt;1的數值),它是通過堆記憶體的最大值*比例來計算得出來的結果。

它可以承受的最大的chunk的數量是這麼計算的 maxcount = memstore記憶體限制 * chunkpool.maxsize / chunksize。

memstore的記憶體最大最小值分别是0.35 --&gt; 0.4,這個在我之前的部落格裡面也有。

還有這個參數hbase.hregion.memstore.chunkpool.initialsize需要設定,預設又是0,輸入0-&gt;1的數值,maxcount乘以它就設定初始的chunk大小。

沒試過開啟這個pool效果是否會好,它是依附在memstore裡面的,它設定過大了,最直接的影響就是,另外兩個集合的空間就小了。

配置設定完chunk之後,幹的是這個函數,就是添加到一個有序集合當中kvset。

memstore裡面有兩個有序的集合,kvset和snapshot,keyvalueskiplistset的内部實作是concurrentnavigablemap。

它們的排序規則上一章已經說過了,排過序的在搜尋的時候友善查找,這裡為什麼還有一個snapshot呢?snapshot是一個和它一樣的東西,我們都知道memstore是要flush到檔案生成storefile的,那我不能寫檔案的時候讓别人都沒法讀了吧,那怎麼辦,先把它拷貝到snapshot當中,這個時間很短,複制完了就可以通路kvset,實際flush的之後,我們flush掉snapshot當中的kv就可以啦。

這裡我們主要關注的是lrublockcache和bucketcache,至于他們的使用,請參照上面的部落格設定,這裡不再介紹哦。

cacheconfig是一個hstore一個,屬性是根據列族定制的,比如是否常駐記憶體,但是它記憶體用來緩存塊的blockcache是region server全局共享的的globalblockcache,在new一個cacheconfig的時候,它會調用instantiateblockcache方法傳回一個blockcache緩存block的,如果已經存在globalblockcache,就直接傳回,沒有才會重新執行個體化一個globalblockcache。

這裡還分堆上記憶體和直接配置設定的記憶體,堆上的記憶體的參數hfile.block.cache.size預設是0.25。

直接配置設定的記憶體,要通過設定jvm參數-xx:maxdirectmemorysize來設定,設定了這個之後我們還需要設定hbase.offheapcache.percentage(預設是0)來設定占直接配置設定記憶體的比例。

offheapcachesize =offheapcache.percentage * directmemorysize

這裡我們還真不能設定它,因為如果設定了它的話,它會把new一個doublecache出來,它是lrublockcache和slabcache的合體,之前我提到的那篇文章裡面說到slabcache是一個隻能存固定大小的block大小的cache,比較垃圾。

如果offheapcachesize &lt;= 0,就走下面的邏輯,這裡我就簡單陳述一下了,代碼沒啥可貼的。

lrublockcache和bucketcache的合作方式有兩種,一種是bucketcache作為二級緩存使用,比如ssd,一種是在記憶體當中,它倆各占比列0.1和0.9,還是建議上ssd做二級緩存,其實也不貴。

不管如何,blockcache這塊的總大小是固定的,是由這個參數決定hfile.block.cache.size,預設它是0.25,是以lrublockcache最大也就是0.25的最大堆記憶體。

在lrublockcache當中還分了三種優先級的緩存塊,分别是single、multi、memory,比列分别是0.25、0.5、0.25,當快要滿的時候,要把塊剔除出記憶體的時候,就要周遊所有的塊了,然後計算他們的分别占的比例,剔除的代碼還挺有意思。

搞了一個優先級隊列,先從single的開刀、single不行了,再拿multi開刀,最後是memory。bytestofree是之前計算好的,要釋放的大小=目前值-最小值。

在我們設定列族參數的時候,有一個inmemory的參數,如果設定了它就是memory,如果沒設定,就是single,single類型的一旦被通路過之後,立馬變成高富帥的multi,但是沒有希望變成memory。

這裡之前百度的一個哥麼問我,meta表的塊會不會一直被儲存在memory當中呢,這塊的代碼寫得讓人有點兒郁悶的,它是按照列族的參數設定的,但是我怎麼去找meta表的列族設定啊,啊被我找到了,在代碼裡面寫着的。

可以看出來meta表的塊隻有8k,常駐記憶體,不使用bloomfilter,允許叢集間複制。

再吐槽一下hbase這個lru算法吧,做得挺粗糙的,它記錄了每個block塊的通路次數,但是它并沒有按照這個來排序,就是簡單的依賴哈希值來排序。

tips:江湖傳言一個regionserver上有一個blockcache和n個memstore,它們的大小之和不能大于等于heapsize * 0.8,否則hbase不能正常啟動,想想也是,hbase是記憶體大戶,記憶體稍有不夠就挂掉,大家要小心設定這個緩存的參數。

原來這塊的圖在上面的那篇文章已經提到了,我就不再重複了,之前沒看的請一定要看,那邊有很詳細的圖解,我這裡隻是講點我了解的實作。

我們可以從兩個方法裡面看lrublockcache和bucketcache的關系,一個是getblock,一個是evictblock,先看evictblock。

在把block剔除出記憶體之後,就把塊加到victimhandler裡面,這個victimhandler就是bucketcache,在cacheconfig執行個體化lrublockcache之後就用setvictimcache方法傳進去的。

看完這個我們再看getblock。

 先從map中取,如果找不到就從victimhandler中取得。

從上面兩個方法,我們可以看出來bucketcache是lrublockcache的二級緩存,它不要了才會存到bucketcache當中,取得時候也是,找不到了才想起人家來。

好,我們現在進入到bucketcache裡面看看,它裡面有幾個重要的屬性。

這裡怎麼又來了兩個,一個記憶體的,一個後備隊裡的,這個是有差別的ramqueueentry當中直接儲存了塊的buffer資料,bucketentry隻是儲存了起始位置和長度。

下面我們看看這個流程吧,還是老規矩,先看入口,再看出口,入口在哪裡,前面的代碼中提到了,入口在cacheblockwithwait方法。

可以看得出來在這個方法當中,先把塊寫入到ramcache當中,然後再插入到一個随機的寫入隊列,寫入線程有3個,每個寫入線程持有一個寫入隊列,線程的數量由參數hbase.bucketcache.writer.threads控制。

我們看看這個writerthread的run方法吧。

<b>那我們要關注的就是dodrain的方法了,在這個方法裡面,它主要幹了4件事情。</b>

1、把ramcache當中的實體給剔除出來轉換成bucketentry,并切入到ioengine。

2、ioengine同步,ioengine包括3種(file,offheap,heap),第一種就是寫入ssd,用的是filechannel,後兩種是寫入到一個bytebufferarray

3、把bucketentry添加到backingmap

4、如果空間不足的話,調用freespace清理空間,清理空間的方法和lrublockcache的方法類似。

這裡面的bucket它也不是一個具體的東西,它裡面記住的也是起始位置,使用了多少次的這些參數,是以說它是一個邏輯上的,而不是實體上的配置設定的一塊随機的位址。

我們是不是可以這麼了解:就是當我們不需要某個塊的時候我們不用去實體的删除它,隻需要不斷的重用它裡面的空間就可以了,而不需要管怎麼删除、釋放等相關内容。

bucketsizeinfo是負責管理這些bucket的,它管理着3個隊列,同時它可以動态根據需求,new一些新的不同大小的bucket出來,也可以把現有的bucket變更它的大小,bucket的大小最小是5k,最大是513k。

sizeindex是啥意思?是在bucketsizeinfo的數組裡面的位置,它的大小都是有固定的值的,不能多也不能少,這裡就不詳細介紹了。我們直接看writetocache這個方法吧,好驗證一下之前的想法。

這裡我們看這一句就可以了ioengine.write(slicebuf, offset);  在寫入ioengine的時候是要傳這個offset的,也正好驗證了我之前的想法,是以bucketallocator.allocateblock的配置設定管理這塊就很關鍵了。

關于怎麼配置設定這塊,還是留個能人講吧,我是講不好了。

繼續閱讀