Region Server的總體架構html
本文主要介紹Region的總體架構,後續再慢慢介紹region的各部分具體實現和源碼緩存
RegionServer邏輯架構圖 數據結構
RegionServer職責架構
一、 監聽協做,經過zk來偵聽master、meta位置、集羣狀態等信息的變化,更新本地數據。併發
二、 管理region的offline、online、open、close等操做,這些操做是和hmaster配合這來作的,region的狀態有以下這些mvc
三、 rpcService服務,分發收到的讀寫請求到具體的region上執行。高併發
在1.0中rpc服務對於請求的作了種類、優先級等的區分,不一樣handler處理不一樣優先級的請求 。post
四、 有一些全局的線程去監控、發現、執行具體region的flush、compaction、split這三種region的核心操做。this
五、 LogRoller,定時將wal日誌進行切分,是一個可運行的thread ,會定時roll全部的wal,也能夠接受外部的roll請求,而後將log來分割 。spa
WAL,寫操做日誌,整個regionServer中維護着這樣一份日誌,全部的region的wal都是寫在這一份日誌中。
在wal被roll後,會向相關的region發送flush request請求。
六、 Leases,leases管理機制,全部region涉及到超時的操做都註冊到lease中,按期統一檢查移除expire並調用expireHandler。
七、 待補充
RegionServer的內部線程
其它關於RegionServer的點
每一個regionServer啓動的時候,都會分配一個startcode,和host,port,startcode統一構成一個regionserver的惟一標誌,因此在一臺機器重啓後和之前實際上是兩個不一樣rs,這樣能夠區分。
Region
region的邏輯架構圖
region組件&流程
region構成
每一個region表示table中一個數據的分區,每一個region包含多個Store,每一個Store對應一個family的讀寫操做,一個Store中包含一個memStore和多個HfileStore,寫的數據是直接寫到memestore中,而後定時刷新到存儲(hdfs)中造成一個HFile,讀的時候會綜合memstore和全部HFileStore中的數據 。
每一個region提供split、flush、compaction的策略和操做方法,可是這個觸發以及執行是由RegionServer中的線程來具體作的。
MVCC多版本控制協議
多版本控制協議的實現類,hbase中採用的是多版本控制協議的方式,來作操做的回滾、操做的原子性、操做在未完成時候的不可見等問題。
在hbase中經過MultiVersionConsistencyControl類來管理多版本的控制協議,能夠同時支持多個寫操做,如果其中的一個寫操做失敗能夠rollback相應的寫操做。
具體的操做是每個/一批操做得到一個惟一的一個mvcc版本號,先將這部分操做的數據 寫入到memstore內存中-->寫入到wal中-->其餘操做,如果其中的一步失敗可,能夠更具mvcc version id刪除已經寫到memstore中的操做(實際上,memstore能夠理解爲一個簡單的SortSet<KeyValue>,失敗的話直接刪除相應的KeyValue就OK了)。
在整個系統中MultiVersionConsistencyControl維護着一個readpoint,這個在讀的時候如果須要控制原子性(未寫完的數據不可見),則只需讀取mvcc version <=readpoint的便可。
關於readpoint的更新,當一個write操做完成的時候,會更新readpoint,由於同時能夠支持多個write的操做,可是隻有一個readpoint,因此MultiVersionConsistencyControl中存儲了一個write操做的queue, write操做在開始的時候會在queue註冊本身,當有一個write完成的時候,會將這個write的mvcc操做置成完成OK狀態,並隨之遍歷mvcc寫操做queqe,而後遍歷整個queue,更新readpoint並移除readpoint前面的write mvcc 版本 。
一個region中存在一個惟一的MultiVersionConsistencyControl,因此目前的操做原子性支持到region級別的。
KeyValue的數據結構以下:
在內存中時候會存在mvcc版本號,寫到hfile中時候,不會寫這個mvcc版本號。
HStore(針對一個family)
Store沒什麼好說的,就是針對一個family一個Store,負責這個family上的讀寫操做,一個Store中包含一個MemStore和多個HFileStore。
memStore
memStore是hbase寫數據的時候(hbase寫的數據就是一個個的KeyValue,delete寫實寫成一個KeyValue,只是type是delete在檢索的時候這個來排除相應的kv)的一個緩衝區,全部的keyValue都先寫入memstore中。
MemStore說白了就是一個SortSet<KeyValue>(下面封裝了一個ConcurrentNavigableMap<KV,KV>),全部寫入的數據直接寫入到這個Set中就好了,memstore中還維護了一個MemStoreLAB,起做用就是在添加kv的時候,將KeyValue內存複製到全局的MemStoreChunkPool(這個就是爲了一個大內存,內面的內存都是一塊一塊的,簡稱一個chunk,這個主要就是爲了不內存碎片,由於對於高併發寫入的時候,memStore的flush仍是挺頻繁的。
關於MemStore的flush操做,MemStore中維護了兩個kvSet,一個是正常的kvSet一個是snapShotKvSet,正常狀況下是數據寫入到kvSet中,snapshot是空的,flush的時候會將當前kvSet賦值給snapshotKvSet,kvSet從新new一個,切換(切換的時候是須要加memStore全局鎖的,可是這個時間很短)完成後,因爲snapShotKvSet是不變的,因此就能夠慢慢得刷新到hdfs中了,在刷寫hdfs,可是在刷新的時候須要解決兩個問題:
一、 刷的時候,snapshot中不能有變化的(由於這個時候有可能rollback啊,rollback的時候也會從snapshot中找的,若找到了也同樣刪除):這個解決就是在snapshot以前會等待當前全部的mvcc 的write請求都完成,才flush 。
二、 讀數據:讀的時候其實MemStoreScanner會檢索這兩kvSet,這裏面其實也是一個HeapScanner
MemStoreChunkPool (統一buffer)
MemStore中的統一buffer,放入MemStore的kvSet的KeyValue都是先System.copyArray到這裏的一個chunk中,在添加到kvSet中,主要是爲了減小內存碎片,這個MemStoreChunkPool也是全局惟一的單例,全部的memStore的空間都會在這上面申請。
StoreFile
一個StoreFile就是一個HFile的對照,HFile的格式見附件的一篇pdf介紹。
Put/Delete
Put和delete實際上是同樣的流程,構形成KeyValue,而後放入到memstore中,如果事務的話,則全部涉及到的rowlock一塊兒申請鎖,不是的話,則是同樣一行處理,鎖其實也是一個一個申請的 。
原子transition
hbase是支持操做的transition的,具體的操做也是經過mvcc來控制的,這一批操做是同一個mvcc version,如果成功則都成功,如果失敗則都rollback,對應在HbaseClient接口上是mutate接口上,通常都是非原子行的
Scan/Get(lsm的HeapScanner)
Hbase中get和scan最終都是會轉化爲scan的操做,只不過get只是拿到一row數據,scanner的搜索就是一個典型的lsm樹的搜索,以下
HeapScanner是爲lsm數結構的檢索而寫的,lsm數的子樹都是一個排過序的Scanner,HeapScanner就是爲了使得本身的next方法返回的KeyValue是有序的,其子節點也多是一個HeapScanner,這個過程能夠迭代的。
HeapScanner的內部實際上是把全部的Scanner放在一個PriorityQueue<KeyValueScanner> heap中,這個heap queue的比較器使用scanner的第一個元素做爲(peek出來),因此每次取出來的HeapScanner.poll出來的元素就是第一個元素最小的scanner,poll出第一個元素後, 再把這個Scanner放入到heap queue中,再取出來的時候,獲得的仍是最小的。
整個Scanner的大體流程以下:
HRegion.getScanner(Scan)--->
HRegion.getScanner(Scan , additionalScanners)(family檢查)
--->HRegion.instantiateRegionScanner(Scan,additionalScanners)
---->RegionScannerImpl(scan, additionalScanners, region)
readPt : 根據isolationLevel來肯定是否須要讀入最新未跟新完以及查詢過程當中更新的數據,也能夠選擇只讀取這個scanner建立的時候mvcc中complete的最新readPoint的 。
isScan = scan.isGetScan() ? -1 : 0;
scanners = new ArrayList<KeyValueScanner>();
joinedScanners = new ArrayList<KeyValueScanner>();
典型的lsm樹種的對個排序的scanner,通常來講都不會出現joinedScanners的狀況,都是scanners,包含每一個Store一個Scanner以及additional scanner
每一個Store經過 store.getScanner(scan, entry.getValue(), this.readPt) 來獲取一個scanner,參數是scan、qualifiler、readPoint
---->RegionScannerImpl(scan,additionalScanners,region)
storeHeap = new KeyValueHeap(scanners, region.comparator);
RegionScannerImpl主要的檢索屬性,filter中也能夠設置在哪中止的屬性,經過filterAllRemaining方法返回true
----->RegionScannerImpl.nextInternal(outResults, limit)
最終落在這個方法上
----->KeyValueHeap.next(List<Cell> result, int limit)
這個其實就是HeapScanner的檢索邏輯了,這個HeapScanner就如上圖所示,中間過程基本都是包裝一層的HeapScanner,最終會落到MemStoreScanner和StoreFileScanner上,在這兩個之上的StoreScanner會過一遍ScanQueryMatcher這個,它的做用就是處理過時、刪除、maxVersion等過濾的。
MemStoreScanner
memStore其是自己就是一個有序的Set,因此直接檢索就好了,可是若是沒有mvcc 版本的控制,其實會檢索到最新寫的KV 。
StoreFileScanner
HfileStore的Scanner邏輯會複雜一些,每次scanner都是新建一個HfileReader(這個如果放在單機普通硬盤上,太多的scanner的話,會很慢,由於每一個get其實都會轉成scanner,其實每一個scanner都會是一個硬盤尋址,可能會慢一些),這個其實就是從具體的kv開始定位,先HFile的index(每一個block的startRow、endRow)信息,而後再定位block,須要具體的block的時候,先去緩存裏面查詢,如果緩存裏面沒有,再去硬盤上讀,讀完後先放到cache中 。
storefile檢索的調用流程
StoreFileScanner.next ----- >HFileScanner(具體的實現是ScannerV2).next() ,首先在當前block的buffer中找,找不到,尋找下一個block(固然前提是先判斷有沒有對到最後一個block)------->AbstractScannerV2.readNextDataBlock------->ScannerV2.readBlock,這個方法裏面會構造block的cachekey(hfileName+offset +encode),而後如今cache中找。
BlockCache(統一管理cache)
具體cache的配置由CacheConfig來配置,使用的是LruBlockCache cache,而後根據配置決定是否須要再這個之上再加BucketCache,做爲其的耳機緩存,這個由配置"hbase.bucketcache.ioengine 等一系列參數決定,詳見CacheConfig.instantiateBlockCache()方法,這個cache是單例,全局惟一的,默認是隻有lru的cache的,沒有BucketCache做爲二級緩存,這個單機版跟蹤過,確實沒有)
一篇介紹hbase幾種cache的博客
http://www.cnblogs.com/cenyuhai/p/3707971.html
Flush
flush操做由region內部定義,可是調用以及內存管理都是由外部(region server)來管理觸發的外部有一個FlushRequester來管理這些待flush的的region,而後定時觸發flush操做 ,如上所述,這個flush操做其實定義在region內部的,只不過在另外的線程中來調用的。
flush是以region爲單位的,一個region中可能包含多個memstore,如果一個達到flush的條件後,則會總體flush這個region下的全部memstore,通常的flush觸發條件以下:
距離上次flush的時間達到limit了;
已經寫得數據量達到limit了;
已經改變的chang數目(transition num)達到limit;
wal被roll後;
數據量的觸發條件由每次寫操做前檢查,達到limit則觸發FlushRequest,其它的都是由rs中的固定線程按期檢查,調用region的shouldFlush()來判斷,針對一個Region的flush是單個且同步的,同一時間只可能有一個FlushRequest,相同直接不接收,同一時間只能有一個現成在進行flush操做,具體的flush操做在方法Region.internalFlushcache中,這個在memstore中會細講。
Compact
Compact操做也是,它的selectCompactFile(如果select的file爲空,則代表不須要compact)的邏輯都是在region中,select的具體邏輯見HStore.requestCompaction()方法中,先選擇相應的Hfile,而後在讀取這些文件併合併成一個Hfile,最終切換Store中的reader 。
選擇過程當中會判斷是須要minor compaction 或者 major compaction,而後以CompactionRequest的方式提交到regionserver中,由regionserver來調起具體compaction操做,region中會根據須要compact的Hfile的大小分爲big compaction和small compaction,而後由不一樣的線程去執行。
OffPeakHours,一個頗有意思的東西,能夠設置不作compact的時間段[startHour,endHour)
startHour配置: hbase.offpeak.start.hour
endHour配置:hbase.offpeak.end.hour
minor compaction的選擇選擇策略
minor compact的選擇策略就是儘量多選擇小的、多的HFile來作一次compact
Split
一、split是由RegionSplitPolicy提供的策略來定的,0.94之後使用的默認policy是IncreasingToUpperBoundRegionSplitPolicy,固然這個也能夠自定義配置(能夠配置hbase.regionserver.region.split.policy來配置,也能夠實現單表獨立的,寫在create table時候的metainfo中,來覆蓋這個配置)
二、關於RegionSplitPolicy的幾個方法
byte[] getSplitPoint() :默認實現是拿size最大的store的split point ,每一個store的splitpoint是本身存儲管理的。
shouldSplit() ,判斷這個region是否須要split
三、默認的實現 IncreasingToUpperBoundRegionSplitPolicy
是否split由一些參數決定
maxFileSize : tableMeta中自定義、或者hbase.hregion.max.filesize來定義,前一個優先使用
三、split斷定策略
有一個region的時候,第一次split是flush的時候,這次split後有兩個region
第二次當達到(Min(2*2*flushSize, splitSize))的時候,此次split後有三個region
第三次當達到(Min(3*3*flushSize, splitSize))的時候,此次split後有四個region啦
以此類推 ................
WAL和EditlogReplay
Editlog即wal寫的日誌,位於hdfs之上,由於寫操做都是寫在memStore中,意外宕機的話,這部分數據都消失了,因此須要些內存的時候寫一份在硬盤上,整個RegionServer的wal是惟一的一個,全部的region的editlog都是寫到一塊兒,Editlog存儲的其實就是一個個的HLog.Entry,HLog.Entry的結構以下:
WALEdit:KeyValue list
HLogKey:tableName、regionEncode、squenceId、writeTime等
每一個Entry標誌屬於哪一個region,並有squenceId,等到須要recove相應的region(crash了)的時候,會先把Editlog按照region切分,每一個region的editlog存在${region_dir}/recovered.edits中,這個region啓動的時候發現這個這文件會啓動replay的流程,詳見Region.replayRecoveredEditsIfAny方法中,其實就是判斷這個log的squenceId和hfile的squenceId(hfile寫入會寫入最大和最小squenceId)做比較,看是否已經固化到hfile中了,若沒有則將這些操做從新寫到memstore中,不然則跳過 。
對於comprocess只會執行 preWALRestore、postWALRestore操做 ,不會執行其餘的。