正文

dumper-dumper error

xinfeng335
文章最后更新時(shí)間2025年03月14日,若文章內(nèi)容或圖片失效,請(qǐng)留言反饋!

  網(wǎng)易視頻云是網(wǎng)易公司旗下的視頻云服務(wù)產(chǎn)品dumper,以Paas服務(wù)模式,向開發(fā)者提供音視頻編解碼SDK和開放API,助力APP接入音視頻功能。

dumper-dumper error
(圖片來源網(wǎng)絡(luò),侵刪)

  現(xiàn)在,網(wǎng)易視頻云的技術(shù)專家給大家分享一篇技術(shù)性文章:HBase BlockCache系列 - 性能對(duì)比測(cè)試報(bào)告.

  HBase BlockCache系列文章到了終結(jié)篇,幾個(gè)主角的是是非非也該有個(gè)了斷了,在SlabCache被早早地淘汰之后,站在華山之巔的也就僅剩LRU君(LRUBlockCache)和CBC君(CombinedBlockCache)。誰贏誰輸,dumper我說了不算,你說了也不算,那就來讓數(shù)據(jù)說話。這篇文章主要對(duì)比LRU君和CBC君(offheap模式)分別在四種場(chǎng)景下幾種指標(biāo)(GC、Throughput、Latency、CPU、IO等)的表現(xiàn)情況。四種場(chǎng)景分別是緩存全部命中、少大部分緩存命中、少量緩存命中、緩存基本未命中。

  需要注意的是,本文的所有數(shù)據(jù)都來自社區(qū)文檔,在這里分享也只是給大家一個(gè)參考,更加詳細(xì)的測(cè)試數(shù)據(jù)可以閱讀文章《Comparing BlockCache Deploys》和 HBASE-11323 附件報(bào)告。

  說明:本文所有圖都以時(shí)間為橫坐標(biāo),縱坐標(biāo)為對(duì)應(yīng)指標(biāo)。每張圖都會(huì)分別顯示LRU君和CBC君的四種場(chǎng)景數(shù)據(jù),總計(jì)八種場(chǎng)景,下面數(shù)據(jù)表示LRU君的四種場(chǎng)景分布在時(shí)間段21:36:39~22:36:40,CBC君的四種場(chǎng)景分布在時(shí)間段23:02:16~00:02:17,看圖的時(shí)候需要特別注意。

  LRU君:

  Tue Jul 22 21:36:39 PDT 2014 run size=32, clients=25 ; lrubc time=1200 緩存全部命中

  Tue Jul 22 21:56:39 PDT 2014 run size=72, clients=25 ; lrubc time=1200 大量緩存命中

  Tue Jul 22 22:16:40 PDT 2014 run size=144, clients=25 ; lrubc time=1200 少量緩存命中

  Tue Jul 22 22:36:40 PDT 2014 run size=1000, clients=25 ; lrubc time=1200 緩存基本未命中

  CBC君:

  Tue Jul 22 23:02:16 PDT 2014 run size=32, clients=25 ; bucket time=1200 緩存全部命中

  Tue Jul 22 23:22:16 PDT 2014 run size=72, clients=25 ; bucket time=1200 大量緩存命中

  Tue Jul 22 23:42:17 PDT 2014 run size=144, clients=25 ; bucket time=1200 少量緩存命中

  Wed Jul 23 00:02:17 PDT 2014 run size=1000, clients=25 ; bucket time=1200 緩存基本未命中

  GC

  GC指標(biāo)是HBase運(yùn)維最關(guān)心的指標(biāo),出現(xiàn)一次長(zhǎng)時(shí)間的GC就會(huì)導(dǎo)致這段時(shí)間內(nèi)業(yè)務(wù)方的所有讀寫請(qǐng)求失敗,如果業(yè)務(wù)方?jīng)]有很好的容錯(cuò),就會(huì)出現(xiàn)丟數(shù)據(jù)的情況出現(xiàn)。根據(jù)下圖可知,只有在‘緩存全部命中’的場(chǎng)景下,LRU君總GC時(shí)間25ms比CBC君的75ms短;其他三種場(chǎng)景下,LRU君表現(xiàn)都沒有CBC君好,總GC時(shí)間基本均是CBC君的3倍左右。

  

  Thoughput

  吞吐量可能是所有HBase用戶初次使用最關(guān)心的問題,這基本反映了HBase的讀寫性能。下圖是隨機(jī)讀測(cè)試的吞吐量曲線,在‘緩存全部命中’以及‘大量緩存命中’這兩種場(chǎng)景下,LRU君可謂是完勝CBC君,特別是在‘緩存全部命中’的場(chǎng)景下,LRU君的吞吐量甚至是CBC君的兩倍;而在‘少量緩存命中’以及‘緩存基本未命中’這兩種場(chǎng)景下,兩者的表現(xiàn)基本相當(dāng);

  

  Latency

  讀寫延遲是另一個(gè)用戶很關(guān)心的指標(biāo),下圖表示在所有四種情況下LRU君和CBC君都在伯仲之間,LRU君略勝一籌。

  

  IO

  接下來兩張圖是資源使用圖,運(yùn)維同學(xué)可能會(huì)比較關(guān)心。從IO使用情況來看,兩者在四種場(chǎng)景下也基本相同。

  

  CPU

  再來看看CPU使用情況,在‘緩存全部命中’以及‘大量緩存命中’這兩種場(chǎng)景下,LRU君依然完勝CBC君,特別是在‘緩存全部命中’的場(chǎng)景下,CBC君差不多做了兩倍于LRU君的工作;而在‘少量緩存命中’以及‘緩存基本未命中’這兩種場(chǎng)景下,兩者的表現(xiàn)基本相當(dāng);

  

  結(jié)論

  看完了所有比較重要的指標(biāo)對(duì)比數(shù)據(jù),我們可以得出以下兩點(diǎn):

  1. 在’緩存全部命中’場(chǎng)景下,LRU君可謂完勝CBC君。因此如果總數(shù)據(jù)量相比JVM內(nèi)存容量很小的時(shí)候,選擇LRU君;

  2. 在所有其他存在緩存未命中情況的場(chǎng)景下, LRU君的GC性能幾乎只有CBC君的1/3,而吞吐量、讀寫延遲、IO、CPU等指標(biāo)兩者基本相當(dāng),因此建議選擇CBC。

  至此,HBase BlockCache系列文章就結(jié)束了,如果大家還有什么交流討論或者補(bǔ)充的,可以留言或者發(fā)郵件至libisthanks@gmail.

  Categories:HBase, 性能Tags:blockcache

  HBase BlockCache系列 - 探求BlockCache實(shí)現(xiàn)機(jī)制

  April 26th, 2016范欣欣 閱讀(0)Comments off

  HBase BlockCache系列第一篇文章《走進(jìn)BlockCache》從全局視角對(duì)HBase中緩存、Memstore等作了簡(jiǎn)要概述,并重點(diǎn)介紹了幾種BlockCache方案及其演進(jìn)過程,對(duì)此還不了解的可以點(diǎn)這里。本文在上文的基礎(chǔ)上深入BlockCache內(nèi)部,對(duì)各種BlockCache方案具體工作原理進(jìn)行詳細(xì)分析。Note:因?yàn)镾labCache方案在0.98版本已經(jīng)不被建議使用,因此本文不針對(duì)該方案進(jìn)行講解;至于LRU方案和Bucket方案,因?yàn)楹笳吒訌?fù)雜,本文也會(huì)花更多篇幅詳細(xì)介紹該方案的實(shí)現(xiàn)細(xì)節(jié)。

  LRUBlockCache

  LRUBlockCache是HBase目前默認(rèn)的BlockCache機(jī)制,實(shí)現(xiàn)機(jī)制比較簡(jiǎn)單。它使用一個(gè)ConcurrentHashMap管理BlockKey到Block的映射關(guān)系,緩存Block只需要將BlockKey和對(duì)應(yīng)的Block放入該HashMap中,查詢緩存就根據(jù)BlockKey從HashMap中獲取即可。同時(shí)該方案采用嚴(yán)格的LRU淘汰算法,當(dāng)Block Cache總量達(dá)到一定閾值之后就會(huì)啟動(dòng)淘汰機(jī)制,最近最少使用的Block會(huì)被置換出來。在具體的實(shí)現(xiàn)細(xì)節(jié)方面,需要關(guān)注三點(diǎn):

  1. 緩存分層策略

  HBase在LRU緩存基礎(chǔ)上,采用了緩存分層設(shè)計(jì),將整個(gè)BlockCache分為三個(gè)部分:single-access、mutil-access和inMemory。需要特別注意的是,HBase系統(tǒng)元數(shù)據(jù)存放在InMemory區(qū),因此設(shè)置數(shù)據(jù)屬性InMemory = true需要非常謹(jǐn)慎,確保此列族數(shù)據(jù)量很小且訪問頻繁,否則有可能會(huì)將hbase.meta元數(shù)據(jù)擠出內(nèi)存,嚴(yán)重影響所有業(yè)務(wù)性能。

  2. LRU淘汰算法實(shí)現(xiàn)

  系統(tǒng)在每次cache block時(shí)將BlockKey和Block放入HashMap后都會(huì)檢查BlockCache總量是否達(dá)到閾值,如果達(dá)到閾值,就會(huì)喚醒淘汰線程對(duì)Map中的Block進(jìn)行淘汰。系統(tǒng)設(shè)置三個(gè)MinMaxPriorityQueue隊(duì)列,分別對(duì)應(yīng)上述三個(gè)分層,每個(gè)隊(duì)列中的元素按照最近最少被使用排列,系統(tǒng)會(huì)優(yōu)先poll出最近最少使用的元素,將其對(duì)應(yīng)的內(nèi)存釋放??梢?,三個(gè)分層中的Block會(huì)分別執(zhí)行LRU淘汰算法進(jìn)行淘汰。

  3. LRU方案優(yōu)缺點(diǎn)

  LRU方案使用JVM提供的HashMap管理緩存,簡(jiǎn)單有效。但隨著數(shù)據(jù)從single-access區(qū)晉升到mutil-access區(qū),基本就伴隨著對(duì)應(yīng)的內(nèi)存對(duì)象從young區(qū)到old區(qū) ,晉升到old區(qū)的Block被淘汰后會(huì)變?yōu)閮?nèi)存垃圾,最終由CMS回收掉(Conccurent Mark Sweep,一種標(biāo)記清除算法),然而這種算法會(huì)帶來大量的內(nèi)存碎片,碎片空間一直累計(jì)就會(huì)產(chǎn)生臭名昭著的Full GC。尤其在大內(nèi)存條件下,一次Full GC很可能會(huì)持續(xù)較長(zhǎng)時(shí)間,甚至達(dá)到分鐘級(jí)別。大家知道Full GC是會(huì)將整個(gè)進(jìn)程暫停的(稱為stop-the-wold暫停),因此長(zhǎng)時(shí)間Full GC必然會(huì)極大影響業(yè)務(wù)的正常讀寫請(qǐng)求。也正因?yàn)檫@樣的弊端,SlabCache方案和BucketCache方案才會(huì)橫空出世。

  BucketCache

  相比LRUBlockCache,BucketCache實(shí)現(xiàn)相對(duì)比較復(fù)雜。它沒有使用JVM 內(nèi)存管理算法來管理緩存,而是自己對(duì)內(nèi)存進(jìn)行管理,因此不會(huì)因?yàn)槌霈F(xiàn)大量碎片導(dǎo)致Full GC的情況發(fā)生。本節(jié)主要介紹BucketCache的具體實(shí)現(xiàn)方式(包括BucketCache的內(nèi)存組織形式、緩存寫入讀取流程等)以及如何配置使用BucketCache。

  內(nèi)存組織形式

  下圖是BucketCache的內(nèi)存組織形式圖,其中上面部分是邏輯組織結(jié)構(gòu),下面部分是對(duì)應(yīng)的物理組織結(jié)構(gòu)。HBase啟動(dòng)之后會(huì)在內(nèi)存中申請(qǐng)大量的bucket,如下圖中黃色矩形所示,每個(gè)bucket的大小默認(rèn)都為2MB。每個(gè)bucket會(huì)有一個(gè)baseoffset變量和一個(gè)size標(biāo)簽,其中baseoffset變量表示這個(gè)bucket在實(shí)際物理空間中的起始地址,因此block的物理地址就可以通過baseoffset和該block在bucket的偏移量唯一確定;而size標(biāo)簽表示這個(gè)bucket可以存放的block塊的大小,比如圖中左側(cè)bucket的size標(biāo)簽為65KB,表示可以存放64KB的block,右側(cè)bucket的size標(biāo)簽為129KB,表示可以存放128KB的block。

  

  HBase中使用BucketAllocator類實(shí)現(xiàn)對(duì)Bucket的組織管理:

  1. HBase會(huì)根據(jù)每個(gè)bucket的size標(biāo)簽對(duì)bucket進(jìn)行分類,相同size標(biāo)簽的bucket由同一個(gè)BucketSizeInfo管理,如上圖,左側(cè)存放64KB block的bucket由65KB BucketSizeInfo管理,右側(cè)存放128KB block的bucket由129KB BucketSizeInfo管理。

  2. HBase在啟動(dòng)的時(shí)候就決定了size標(biāo)簽的分類,默認(rèn)標(biāo)簽有(4+1)K、(8+1)K、(16+1)K … (48+1)K、(56+1)K、(64+1)K、(96+1)K … (512+1)K。而且系統(tǒng)會(huì)首先從小到大遍歷一次所有size標(biāo)簽,為每種size標(biāo)簽分配一個(gè)bucket,最后所有剩余的bucket都分配最大的size標(biāo)簽,默認(rèn)分配 (512+1)K,如下圖所示:

  

  3. Bucket的size標(biāo)簽可以動(dòng)態(tài)調(diào)整,比如64K的block數(shù)目比較多,65K的bucket被用完了以后,其他size標(biāo)簽的完全空閑的bucket可以轉(zhuǎn)換成為65K的bucket,但是至少保留一個(gè)該size的bucket。

  Block緩存寫入、讀取流程

  下圖是block寫入緩存以及從緩存中讀取block的流程示意圖,圖中主要包括5個(gè)模塊,其中RAMCache是一個(gè)存儲(chǔ)blockkey和block對(duì)應(yīng)關(guān)系的HashMap;WriteThead是整個(gè)block寫入的中心樞紐,主要負(fù)責(zé)異步的寫入block到內(nèi)存空間;BucketAllocator在上一節(jié)詳細(xì)介紹過,主要實(shí)現(xiàn)對(duì)bucket的組織管理,為block分配內(nèi)存空間;IOEngine是具體的內(nèi)存管理模塊,主要實(shí)現(xiàn)將block數(shù)據(jù)寫入對(duì)應(yīng)地址的內(nèi)存空間;BackingMap也是一個(gè)HashMap,用來存儲(chǔ)blockKey與對(duì)應(yīng)物理內(nèi)存偏移量的映射關(guān)系,用來根據(jù)blockkey定位具體的block;其中紫線表示cache block流程,綠線表示get block流程。

  

  Block緩存寫入流程

  1. 將block寫入RAMCache。實(shí)際實(shí)現(xiàn)中,HBase設(shè)置了多個(gè)RAMCache,系統(tǒng)首先會(huì)根據(jù)blockkey進(jìn)行hash,根據(jù)hash結(jié)果將block分配到對(duì)應(yīng)的RAMCache中;

  2. WriteThead從RAMCache中取出所有的block。和RAMCache相同,HBase會(huì)同時(shí)啟動(dòng)多個(gè)WriteThead并發(fā)的執(zhí)行異步寫入,每個(gè)WriteThead對(duì)應(yīng)一個(gè)RAMCache;

  3. 每個(gè)WriteThead會(huì)將遍歷RAMCache中所有block數(shù)據(jù),分別調(diào)用bucketAllocator為這些block分配內(nèi)存空間;

  4. BucketAllocator會(huì)選擇與block大小對(duì)應(yīng)的bucket進(jìn)行存放(具體細(xì)節(jié)可以參考上節(jié)‘內(nèi)存組織形式’所述),并且返回對(duì)應(yīng)的物理地址偏移量offset;

  5. WriteThead將block以及分配好的物理地址偏移量傳給IOEngine模塊,執(zhí)行具體的內(nèi)存寫入操作;

  6. 寫入成功后,將類似<blockkey,offset>這樣的映射關(guān)系寫入BackingMap中,方便后續(xù)查找時(shí)根據(jù)blockkey可以直接定位;

  Block緩存讀取流程

  1. 首先從RAMCache中查找。對(duì)于還沒有來得及寫入到bucket的緩存block,一定存儲(chǔ)在RAMCache中;

  2. 如果在RAMCache中沒有找到,再在BackingMap中根據(jù)blockKey找到對(duì)應(yīng)物理偏移地址offset;

  3. 根據(jù)物理偏移地址offset可以直接從內(nèi)存中查找對(duì)應(yīng)的block數(shù)據(jù);

  BucketCache工作模式

  BucketCache默認(rèn)有三種工作模式:heap、offheap和file;這三種工作模式在內(nèi)存邏輯組織形式以及緩存流程上都是相同的,參見上節(jié)講解。不同的是三者對(duì)應(yīng)的最終存儲(chǔ)介質(zhì)有所不同,即上述所講的IOEngine有所不同。

  其中heap模式和offheap模式都使用內(nèi)存作為最終存儲(chǔ)介質(zhì),內(nèi)存分配查詢也都使用Java NIO ByteBuffer技術(shù),不同的是,heap模式分配內(nèi)存會(huì)調(diào)用byteBuffer.allocate方法,從JVM提供的heap區(qū)分配,而后者會(huì)調(diào)用byteBuffer.allocateDirect方法,直接從操作系統(tǒng)分配。這兩種內(nèi)存分配模式會(huì)對(duì)HBase實(shí)際工作性能產(chǎn)生一定的影響。影響最大的無疑是GC ,相比heap模式,offheap模式因?yàn)閮?nèi)存屬于操作系統(tǒng),所以基本不會(huì)產(chǎn)生CMS GC,也就在任何情況下都不會(huì)因?yàn)閮?nèi)存碎片導(dǎo)致觸發(fā)Full GC。除此之外,在內(nèi)存分配以及讀取方面,兩者性能也有不同,比如,內(nèi)存分配時(shí)heap模式需要首先從操作系統(tǒng)分配內(nèi)存再拷貝到JVM heap,相比offheap直接從操作系統(tǒng)分配內(nèi)存更耗時(shí);但是反過來,讀取緩存時(shí)heap模式可以從JVM heap中直接讀取,而offheap模式則需要首先從操作系統(tǒng)拷貝到JVM heap再讀取,顯得后者更費(fèi)時(shí)。

  file模式和前面兩者不同,它使用Fussion-IO或者SSD等作為存儲(chǔ)介質(zhì),相比昂貴的內(nèi)存,這樣可以提供更大的存儲(chǔ)容量,因此可以極大地提升緩存命中率。

  BucketCache配置使用

  BucketCache方案的配置說明一直被HBaser所詬病,官方一直沒有相關(guān)文檔對(duì)此進(jìn)行介紹。本人也是一直被其所困,后來通過查看源碼才基本了解清楚,在此分享出來,以便大家學(xué)習(xí)。需要注意的是,BucketCache三種工作模式的配置會(huì)有所不同,下面也是分開介紹,并且沒有列出很多不重要的參數(shù):

  heap模式

  <hbase.bucketcache.ioengine>heap</hbase.bucketcache.ioengine>

  //bucketcache占用整個(gè)jvm內(nèi)存大小的比例

  <hbase.bucketcache.size>0.4</hbase.bucketcache.size>

  //bucketcache在combinedcache中的占比

  <hbase.bucketcache.combinedcache.percentage>0.9</hbase.bucketcache.combinedcache.percentage>

  offheap模式

  <hbase.bucketcache.ioengine>offheap</hbase.bucketcache.ioengine>

  <hbase.bucketcache.size>0.4</hbase.bucketcache.size>

  <hbase.bucketcache.combinedcache.percentage>0.9</hbase.bucketcache.combinedcache.percentage>

  file模式

  <hbase.bucketcache.ioengine>file:/cache_path</hbase.bucketcache.ioengine>

  //bucketcache緩存空間大小,單位為MB

  <hbase.bucketcache.size>10 * 1024</hbase.bucketcache.size>

  //高速緩存路徑

  <hbase.bucketcache.persistent.path>file:/cache_path</hbase.bucketcache.persistent.path>

  總結(jié)

  HBase中緩存的設(shè)置對(duì)隨機(jī)讀寫性能至關(guān)重要,本文通過對(duì)LRUBlockCache和BucketCache兩種方案的實(shí)現(xiàn)進(jìn)行介紹,希望能夠讓各位看官更加深入地了解BlockCache的工作原理。BlockCache系列經(jīng)過兩篇內(nèi)容的介紹,基本已經(jīng)解析完畢,那在實(shí)際線上應(yīng)用中到底應(yīng)該選擇哪種方案呢?下一篇文章讓我們一起看看HBase社區(qū)發(fā)布的BlockCache報(bào)告!

  Categories:BucketCache, HBase, LRUBlockCacheTags:blockcache

  HBase – RegionServer宕機(jī)案件偵查

  April 15th, 2016范欣欣 閱讀(0)Comments off

  本來靜謐的晚上,吃著葡萄干看著球賽,何等愜意??善粭l報(bào)警短信如閃電一般打破了夜晚的寧靜,線上集群一臺(tái)RS宕了!于是倏地從床上坐起來,看了看監(jiān)控,瞬間驚呆了:?jiǎn)闻_(tái)機(jī)器的讀寫吞吐量竟然達(dá)到了5w ops/sec!RS宕機(jī)是因?yàn)檫@么大的寫入量造成的?如果真是這樣,它是怎么造成的?如果不是這樣,那又是什么原因?各種疑問瞬間從腦子里一一閃過,甭管那么多,先把日志備份一份,再把RS拉起來。接下來還是Bug排查老套路:日志、監(jiān)控和源碼三管齊下,來看看到底發(fā)生了什么!

  案件現(xiàn)場(chǎng)篇

  下圖是使用監(jiān)控工具Ganglia對(duì)事發(fā)RegionServer當(dāng)時(shí)讀寫吞吐量的監(jiān)控曲線,從圖中可以看出,大約在19點(diǎn)~21點(diǎn)半的時(shí)間段內(nèi),這臺(tái)RS的吞吐量都維持了3w ops/sec左右,峰值更是達(dá)到了6w ops/sec。之前我們就線上單臺(tái)RS能夠承受的最大讀寫吞吐量進(jìn)行過測(cè)定,基本也就維持在2w左右,主要是因?yàn)榫W(wǎng)絡(luò)帶寬瓶頸。而在宕機(jī)前這臺(tái)RS的讀寫吞吐量超出這么多,直覺告訴我RS宕機(jī)原因就是它!

  

  接著就趕緊把日志拉出來看,滿屏的responseTooSlow,如下圖所示:

  

  很顯然,這種異常最大可能原因就是Full GC,果然,經(jīng)過耐心地排查,可以看到很多如下所示的Full GC日志片段:

  2016-04-14 21:27:13,174 WARN [JvmPauseMonitor] util.JvmPauseMonitor: Detected pause in JVM or host machine (eg GC): pause of approximately 20542ms

  GC pool 'ParNew' had collection(s): count=1 time=0ms

  GC pool 'ConcurrentMarkSweep' had collection(s): count=2 time=20898ms

  2016-04-14 21:27:13,174 WARN [regionserver60020.periodicFlusher] util.Sleeper: We slept 20936ms instead of 100ms, this is likely due to a long garbage collecting pause and it's usually bad, see http://hbase.apache.org/book.html#trouble.rs.runtime.zkexpired

  可以看出,HBase執(zhí)行了一次CMS GC,導(dǎo)致整個(gè)進(jìn)程所有線程被掛起了20s。通過對(duì)MemStore的監(jiān)控也可以看出這段時(shí)間GC力度之大,如下圖所示:

  

  GC時(shí)間長(zhǎng)最明顯的危害是會(huì)造成上層業(yè)務(wù)的阻塞,通過日志也可以看出些許端倪:

  java.io.IOException: Connection reset by peer

  at sun.nio.ch.FileDispatcherImpl.read0(Native Method)

  at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39)

  at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)

  at sun.nio.ch.IOUtil.read(IOUtil.java:197)

  at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:384)

  at org.apache.hadoop.hbase.ipc.RpcServer.channelRead(RpcServer.java:2246)

  at org.apache.hadoop.hbase.ipc.RpcServer$Connection.readAndProcess(RpcServer.java:1496)

  ....

  2016-04-14 21:32:40,173 WARN [B.DefaultRpcServer.handler=125,queue=5,port=60020] ipc.RpcServer: RpcServer.respondercallId: 7540 service: ClientService methodName: Multi size: 100.2 K connection: 10.160.247.139:56031: output error

  2016-04-14 21:32:40,173 WARN [B.DefaultRpcServer.handler=125,queue=5,port=60020] ipc.RpcServer: B.DefaultRpcServer.handler=125,queue=5,port=60020: caught a ClosedChannelException, this means that the server was processing a request but the client went away. The error message was: null

  上述日志表示HBase服務(wù)端因?yàn)镕ull GC導(dǎo)致一直無法響應(yīng)用戶請(qǐng)求,用戶客戶端程序在一定時(shí)間過后就會(huì)SocketTimeout并斷掉此Connection。連接斷掉之后,服務(wù)器端就會(huì)打印如上日志。然而,這些和我們的終極目標(biāo)好像并沒有太大關(guān)系,別忘了我們的目標(biāo)是找到RS宕機(jī)的原因哦!

  破案鋪墊篇

  經(jīng)過對(duì)案件現(xiàn)場(chǎng)的排查,唯一有用的線索就是HBase在宕機(jī)前經(jīng)歷了很嚴(yán)重、很頻繁的Full GC,從下面日志可以進(jìn)一步看出,這些Full GC都是在 concurrent mode failure模式下發(fā)生的,也就是虛擬機(jī)還未執(zhí)行完本次GC的情況下又來了大量數(shù)據(jù)導(dǎo)致JVM內(nèi)存不夠,此時(shí)虛擬機(jī)會(huì)將所有用戶線程掛起,執(zhí)行長(zhǎng)時(shí)間的Full GC!

  (concurrent mode failure): 45876255K->21800674K(46137344K), 10.0625300 secs] 48792749K->21800674K(49283072K), [CMS Perm : 43274K->43274K(262144K)], 10.2083040 secs] [Times: user=12.02 sys=0.00, real=10.20 secs]

  2016-04-14 21:22:43,990 WARN [JvmPauseMonitor] util.JvmPauseMonitor: Detected pause in JVM or host machine (eg GC): pause of approximately 10055ms

  GC pool 'ParNew' had collection(s): count=2 time=244ms

  GC pool 'ConcurrentMarkSweep' had collection(s): count=1 time=10062ms

  上文提到Full GC會(huì)對(duì)上層業(yè)務(wù)產(chǎn)生很嚴(yán)重的影響,那有沒有可能會(huì)對(duì)下層依賴方也產(chǎn)生很大的影響呢?事實(shí)是Yes!而且,RS宕機(jī)的大部分原因也要?dú)w咎于此!

  進(jìn)一步查看日志,發(fā)現(xiàn)HBase日志中出現(xiàn)下述異常:

  2016-04-14 21:22:44,006 WARN [ResponseProcessor for block BP-632656502-10.160.173.93-1448595094942:blk_1073941840_201226] hdfs.DFSClient: DFSOutputStream ResponseProcessor exception for block BP-632656502-10.160.173.93-1448595094942:blk_1073941840_201226java.io.IOException: Bad response ERROR for block BP-632656502-10.160.173.93-1448595094942:blk_1073941840_201226 from datanode 10.160.173.93:50010

  at org.apache.hadoop.hdfs.DFSOutputStream$DataStreamer$ResponseProcessor.run(DFSOutputStream.java:732)

  從日志內(nèi)容來看應(yīng)該是hbase調(diào)用DFSClient從datanode加載block數(shù)據(jù)”BP-632656502-10.160.173.93-1448595094942:blk_1073941840_201226″,但是datanode返回失敗。具體失敗原因需要查看datanode節(jié)點(diǎn)日志,如下所示:

  2016-04-14 21:22:43,789 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: opWriteBlock BP-632656502-10.160.173.93-1448595094942:blk_1073941840_201226 received exception java.net.SocketTimeoutException: 10000 millis timeout while waiting for channel to be ready for read. ch : java.nio.channels.SocketChannel[connected local=/10.160.173.94:50010 remote=/10.160.173.94:30110]

  2016-04-14 21:22:43,779 ERROR org.apache.hadoop.hdfs.server.datanode.DataNode: hz-hbase4.photo.163.org:50010:DataXceiver error processing WRITE_BLOCK operation src: /10.160.173.94:30123 dest: /10.160.173.94:50010

  java.net.SocketTimeoutException: 10000 millis timeout while waiting for channel to be ready for read. ch : java.nio.channels.SocketChannel[connected local=/10.160.173.94:50010 remote=/10.160.173.94:30123]

  很顯然,從日志可以看出,datanode一直在等待來自客戶端的read請(qǐng)求,但是直至SocketTimeout,請(qǐng)求都沒有過來,此時(shí)datanode會(huì)將該連接斷開,導(dǎo)致客戶端收到上述”Bad response ERROR ***”的異常。

  那這和Full GC有什么關(guān)系呢?很簡(jiǎn)單,就是因?yàn)镕ull GC導(dǎo)致HBase所有內(nèi)部線程掛起,因此發(fā)往datanode的read請(qǐng)求也被掛起了,datanode就等啊等,左等右等都等不到,萬不得已才將連接斷掉。

  查看Hadoop客戶端源碼可知,如果DFSClient發(fā)生上述異常,DFSClient會(huì)將一個(gè)全局標(biāo)志errorIndex設(shè)為一個(gè)非零值。具體可參見DFSOutputStream類中如下代碼片段:

  破案結(jié)局篇

  上述鋪墊篇最后的結(jié)果就是Hadoop客戶端會(huì)將一個(gè)全局標(biāo)志errorIndex設(shè)為一個(gè)非零值,那這到底和最終RS宕掉有什么關(guān)系呢?來繼續(xù)往下看。下圖HBase日志相關(guān)片段截圖,記錄了比較詳細(xì)的RS宕機(jī)異常信息,我們就以這些異常信息作為切入點(diǎn)進(jìn)行分析,可以看出至少三條有用的線索,如下圖所示:

  

  線索一:RS宕機(jī)最直接的原因是因?yàn)橄到y(tǒng)在關(guān)閉LogWriter(之后會(huì)重新開啟一個(gè)新的HLog)的時(shí)候失敗

  線索二:執(zhí)行LogWriter關(guān)閉失敗的原因是”writing trailer”時(shí)發(fā)生IOException異常

  線索三:而發(fā)生IOException異常的原因是”All datanodes *** are bad”

  到這里為止,我們能夠獲得的最靠譜的情報(bào)就是RS宕機(jī)本質(zhì)是因?yàn)椤盇ll datanodes *** are bad”造成的,看字面意思就是這臺(tái)datanode因?yàn)槟撤N原因壞掉了,那我們趕緊去看看datanode的日志,看看那個(gè)時(shí)間段有沒有相關(guān)的異?;蛘咤e(cuò)誤日志。

  然而很遺憾,datanode日志在那個(gè)時(shí)間點(diǎn)沒有打印任何異?;蛘咤e(cuò)誤日志,而且顯示所有服務(wù)都正常,信息如下所示:

  2016-04-14 21:32:38,893 INFO org.apache.hadoop.hdfs.server.datanode.DataNode.clienttrace: src: 127.0.0.1, dest: 127.0.0.1, op: REQUEST_SHORT_CIRCUIT_FDS, blockid: 1073941669, srvID: DS-22834907-10.160.173.94-50010-1448595406972, success: true

  2016-04-14 21:32:38,894 INFO org.apache.hadoop.hdfs.server.datanode.DataNode.clienttrace: src: 127.0.0.1, dest: 127.0.0.1, op: REQUEST_SHORT_CIRCUIT_FDS, blockid: 1073941669, srvID: DS-22834907-10.160.173.94-50010-1448595406972, success: true

  ...

  看到這里,是不是有點(diǎn)蒙圈:HBase日志里面明明打印說這臺(tái)datanode壞掉了,但是實(shí)際datanode日志顯示服務(wù)一切正常。這個(gè)時(shí)候就得翻翻源碼了,看看HBase在哪里打印的”All datanodes *** are bad“,通過查看源碼,可以看出最終元兇就是上文提到的errorIndex,如下圖所示:

  

  終于撥開天日了,再不完結(jié)就要暈了!上文鋪墊篇鋪墊到最后就得出來因?yàn)镕ull GC最終導(dǎo)致DFSClient將一個(gè)全局標(biāo)志errorIndex設(shè)為一個(gè)非零值,在這里終于碰頭了,簡(jiǎn)直淚流滿面!

  案件梳理篇

  整個(gè)流程走下來本人都有點(diǎn)暈暈的,涉及的方方面面太多,因此有必要把整個(gè)流程完整的梳理一遍,下面簡(jiǎn)單畫了一個(gè)示意圖:

  

  經(jīng)過對(duì)整個(gè)案件的整理分析,一方面再次鍛煉了如何通過監(jiān)控、日志以及源碼定位排查問題的功底,另一方面在HBase運(yùn)維過程中也需要特別關(guān)注如下幾點(diǎn):

  1. Full GC不僅會(huì)嚴(yán)重影響上層業(yè)務(wù),造成業(yè)務(wù)讀寫請(qǐng)求的卡頓。另外還有可能造成與HDFS之間數(shù)據(jù)請(qǐng)求的各種異常,這種異常嚴(yán)重的時(shí)候甚至?xí)?dǎo)致RegionServer宕機(jī)。

  2. 上文中提到Full GC基本是由于Concurrent Mode Failure造成,這種Full GC場(chǎng)景比較少見,通??梢酝ㄟ^減小 JVM 參數(shù)XX:CMSInitiatingOccupancyFraction來避免,這個(gè)參數(shù)用來設(shè)置CMS垃圾回收時(shí)機(jī),假如此時(shí)設(shè)置為60,表示JVM內(nèi)已使用內(nèi)存占到總內(nèi)存的60%的時(shí)候就會(huì)進(jìn)行垃圾回收,減少該值可以使得垃圾回收更早進(jìn)行。

  3. 一定要嚴(yán)格限制業(yè)務(wù)層面的流量。一方面需要和業(yè)務(wù)方交流,由業(yè)務(wù)方進(jìn)行限制,另一方面可以探索HBase業(yè)務(wù)資源隔離的新方案;

  Categories:HBase, RegionServer, 宕機(jī)Tags:

  HBase BlockCache系列 – 走進(jìn)BlockCache

  April 14th, 2016fan xinxin 閱讀(191)No comments

  和其他數(shù)據(jù)庫一樣,優(yōu)化IO也是HBase提升性能的不二法寶,而提供緩存更是優(yōu)化的重中之重。最理想的情況是,所有數(shù)據(jù)都能夠緩存到內(nèi)存,這樣就不會(huì)有任何文件IO請(qǐng)求,讀寫性能必然會(huì)提升到極致。然而現(xiàn)實(shí)是殘酷的,隨著請(qǐng)求數(shù)據(jù)的不斷增多,將數(shù)據(jù)全部緩存到內(nèi)存顯得不合實(shí)際。幸運(yùn)的是,我們并不需要將所有數(shù)據(jù)都緩存起來,根據(jù)二八法則,80%的業(yè)務(wù)請(qǐng)求都集中在20%的熱點(diǎn)數(shù)據(jù)上,因此將這部分?jǐn)?shù)據(jù)緩存起就可以極大地提升系統(tǒng)性能。

  HBase在實(shí)現(xiàn)中提供了兩種緩存結(jié)構(gòu):MemStore和BlockCache。其中MemStore稱為寫緩存,HBase執(zhí)行寫操作首先會(huì)將數(shù)據(jù)寫入MemStore,并順序?qū)懭際Log,等滿足一定條件后統(tǒng)一將MemStore中數(shù)據(jù)刷新到磁盤,這種設(shè)計(jì)可以極大地提升HBase的寫性能。不僅如此,MemStore對(duì)于讀性能也至關(guān)重要,假如沒有MemStore,讀取剛寫入的數(shù)據(jù)就需要從文件中通過IO查找,這種代價(jià)顯然是昂貴的!BlockCache稱為讀緩存,HBase會(huì)將一次文件查找的Block塊緩存到Cache中,以便后續(xù)同一請(qǐng)求或者鄰近數(shù)據(jù)查找請(qǐng)求,可以直接從內(nèi)存中獲取,避免昂貴的IO操作。MemStore相關(guān)知識(shí)可以戳這里,本文將重點(diǎn)分析BlockCache。

  在介紹BlockCache之前,簡(jiǎn)單地回顧一下HBase中Block的概念,詳細(xì)介紹戳這里。 Block是HBase中最小的數(shù)據(jù)存儲(chǔ)單元,默認(rèn)為64K,在建表語句中可以通過參數(shù)BlockSize指定。HBase中Block分為四種類型:Data Block,Index Block,Bloom Block和Meta Block。其中Data Block用于存儲(chǔ)實(shí)際數(shù)據(jù),通常情況下每個(gè)Data Block可以存放多條KeyValue數(shù)據(jù)對(duì);Index Block和Bloom Block都用于優(yōu)化隨機(jī)讀的查找路徑,其中Index Block通過存儲(chǔ)索引數(shù)據(jù)加快數(shù)據(jù)查找,而Bloom Block通過一定算法可以過濾掉部分一定不存在待查KeyValue的數(shù)據(jù)文件,減少不必要的IO操作;Meta Block主要存儲(chǔ)整個(gè)HFile的元數(shù)據(jù)。

  BlockCache是Region Server級(jí)別的,一個(gè)Region Server只有一個(gè)Block Cache,在Region Server啟動(dòng)的時(shí)候完成Block Cache的初始化工作。到目前為止,HBase先后實(shí)現(xiàn)了3種Block Cache方案,LRUBlockCache是最初的實(shí)現(xiàn)方案,也是默認(rèn)的實(shí)現(xiàn)方案;HBase 0.92版本實(shí)現(xiàn)了第二種方案SlabCache,見HBASE-4027;HBase 0.96之后官方提供了另一種可選方案BucketCache,見HBASE-7404。

  這三種方案的不同之處在于對(duì)內(nèi)存的管理模式,其中LRUBlockCache是將所有數(shù)據(jù)都放入JVM Heap中,交給JVM進(jìn)行管理。而后兩者采用了不同機(jī)制將部分?jǐn)?shù)據(jù)存儲(chǔ)在堆外,交給HBase自己管理。這種演變過程是因?yàn)長(zhǎng)RUBlockCache方案中JVM垃圾回收機(jī)制經(jīng)常會(huì)導(dǎo)致程序長(zhǎng)時(shí)間暫停,而采用堆外內(nèi)存對(duì)數(shù)據(jù)進(jìn)行管理可以有效避免這種情況發(fā)生。

  LRUBlockCache

  HBase默認(rèn)的BlockCache實(shí)現(xiàn)方案。Block數(shù)據(jù)塊都存儲(chǔ)在 JVM heap內(nèi),由JVM進(jìn)行垃圾回收管理。它將內(nèi)存從邏輯上分為了三塊:single-access區(qū)、mutil-access區(qū)、in-memory區(qū),分別占到整個(gè)BlockCache大小的25%、50%、25%。一次隨機(jī)讀中,一個(gè)Block塊從HDFS中加載出來之后首先放入signle區(qū),后續(xù)如果有多次請(qǐng)求訪問到這塊數(shù)據(jù)的話,就會(huì)將這塊數(shù)據(jù)移到mutil-access區(qū)。而in-memory區(qū)表示數(shù)據(jù)可以常駐內(nèi)存,一般用來存放訪問頻繁、數(shù)據(jù)量小的數(shù)據(jù),比如元數(shù)據(jù),用戶也可以在建表的時(shí)候通過設(shè)置列族屬性IN-MEMORY= true將此列族放入in-memory區(qū)。很顯然,這種設(shè)計(jì)策略類似于JVM中young區(qū)、old區(qū)以及perm區(qū)。無論哪個(gè)區(qū),系統(tǒng)都會(huì)采用嚴(yán)格的Least-Recently-Used算法,當(dāng)BlockCache總量達(dá)到一定閾值之后就會(huì)啟動(dòng)淘汰機(jī)制,最少使用的Block會(huì)被置換出來,為新加載的Block預(yù)留空間。

  SlabCache

  為了解決LRUBlockCache方案中因?yàn)镴VM垃圾回收導(dǎo)致的服務(wù)中斷,SlabCache方案使用Java NIO DirectByteBuffer技術(shù)實(shí)現(xiàn)了堆外內(nèi)存存儲(chǔ),不再由JVM管理數(shù)據(jù)內(nèi)存。默認(rèn)情況下,系統(tǒng)在初始化的時(shí)候會(huì)分配兩個(gè)緩存區(qū),分別占整個(gè)BlockCache大小的80%和20%,每個(gè)緩存區(qū)分別存儲(chǔ)固定大小的Block塊,其中前者主要存儲(chǔ)小于等于64K大小的Block,后者存儲(chǔ)小于等于128K Block,如果一個(gè)Block太大就會(huì)導(dǎo)致兩個(gè)區(qū)都無法緩存。和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法對(duì)過期Block進(jìn)行淘汰。和LRUBlockCache不同的是,SlabCache淘汰Block的時(shí)候只需要將對(duì)應(yīng)的bufferbyte標(biāo)記為空閑,后續(xù)cache對(duì)其上的內(nèi)存直接進(jìn)行覆蓋即可。

  線上集群環(huán)境中,不同表不同列族設(shè)置的BlockSize都可能不同,很顯然,默認(rèn)只能存儲(chǔ)兩種固定大小Block的SlabCache方案不能滿足部分用戶場(chǎng)景,比如用戶設(shè)置BlockSize = 256K,簡(jiǎn)單使用SlabCache方案就不能達(dá)到這部分Block緩存的目的。因此HBase實(shí)際實(shí)現(xiàn)中將SlabCache和LRUBlockCache搭配使用,稱為DoubleBlockCache。一次隨機(jī)讀中,一個(gè)Block塊從HDFS中加載出來之后會(huì)在兩個(gè)Cache中分別存儲(chǔ)一份;緩存讀時(shí)首先在LRUBlockCache中查找,如果Cache Miss再在SlabCache中查找,此時(shí)如果命中再將該Block放入LRUBlockCache中。

  經(jīng)過實(shí)際測(cè)試,DoubleBlockCache方案有很多弊端。比如SlabCache設(shè)計(jì)中固定大小內(nèi)存設(shè)置會(huì)導(dǎo)致實(shí)際內(nèi)存使用率比較低,而且使用LRUBlockCache緩存Block依然會(huì)因?yàn)镴VM GC產(chǎn)生大量?jī)?nèi)存碎片。因此在HBase 0.98版本之后,該方案已經(jīng)被不建議使用。

  BucketCache

  SlabCache方案在實(shí)際應(yīng)用中并沒有很大程度改善原有LRUBlockCache方案的GC弊端,還額外引入了諸如堆外內(nèi)存使用率低的缺陷。然而它的設(shè)計(jì)并不是一無是處,至少在使用堆外內(nèi)存這個(gè)方面給予了阿里大牛們很多啟發(fā)。站在SlabCache的肩膀上,他們開發(fā)了BucketCache緩存方案并貢獻(xiàn)給了社區(qū)。

  BucketCache通過配置可以工作在三種模式下:heap,offheap和file。無論工作在那種模式下,BucketCache都會(huì)申請(qǐng)?jiān)S多帶有固定大小標(biāo)簽的Bucket,和SlabCache一樣,一種Bucket存儲(chǔ)一種指定BlockSize的數(shù)據(jù)塊,但和SlabCache不同的是,BucketCache會(huì)在初始化的時(shí)候申請(qǐng)14個(gè)不同大小的Bucket,而且即使在某一種Bucket空間不足的情況下,系統(tǒng)也會(huì)從其他Bucket空間借用內(nèi)存使用,不會(huì)出現(xiàn)內(nèi)存使用率低的情況。接下來再來看看不同工作模式,heap模式表示這些Bucket是從JVM Heap中申請(qǐng),offheap模式使用DirectByteBuffer技術(shù)實(shí)現(xiàn)堆外內(nèi)存存儲(chǔ)管理,而file模式使用類似SSD的高速緩存文件存儲(chǔ)數(shù)據(jù)塊。

  實(shí)際實(shí)現(xiàn)中,HBase將BucketCache和LRUBlockCache搭配使用,稱為CombinedBlockCache。和DoubleBlockCache不同,系統(tǒng)在LRUBlockCache中主要存儲(chǔ)Index Block和Bloom Block,而將Data Block存儲(chǔ)在BucketCache中。因此一次隨機(jī)讀需要首先在LRUBlockCache中查到對(duì)應(yīng)的Index Block,然后再到BucketCache查找對(duì)應(yīng)數(shù)據(jù)塊。BucketCache通過更加合理的設(shè)計(jì)修正了SlabCache的弊端,極大降低了JVM GC對(duì)業(yè)務(wù)請(qǐng)求的實(shí)際影響,但也存在一些問題,比如使用堆外內(nèi)存會(huì)存在拷貝內(nèi)存的問題,一定程度上會(huì)影響讀寫性能。當(dāng)然,在后來的版本中這個(gè)問題也得到了解決,見HBASE-11425。

  本文是HBase BlockCache系列文章的第一篇,主要概述了HBase中MemStore和BlockCache,再分別對(duì)三種BlockCache方案進(jìn)行了基本介紹。接下來第二篇文章會(huì)主要對(duì)LRUBlockCache和BucketCache兩種方案進(jìn)行詳細(xì)的介紹,敬請(qǐng)期待!

  Categories:HBaseTags:blockcache, hbase

  MySQL X Plugin來也

  April 12th, 2016AskInside 閱讀(0)Comments off

  MySQL 5.7.12版本發(fā)布,雖然之前5.7已經(jīng)GA,但這個(gè)版本依然承上啟下,舉足輕重,因?yàn)镸ySQL X Plugin來了。

  X Plugin extends MySQL Server to be able to function as a d[......]

  閱讀全文

  Categories:MySQLTags:

  HBase BlockCache系列 – 走進(jìn)BlockCache

  April 8th, 2016范欣欣 閱讀(0)Comments off

  和其他數(shù)據(jù)庫一樣,優(yōu)化IO也是HBase提升性能的不二法寶,而提供緩存更是優(yōu)化的重中之重。最理想的情況是,所有數(shù)據(jù)都能夠緩存到內(nèi)存,這樣就不會(huì)有任何文件IO請(qǐng)求,讀寫性能必然會(huì)提升到極致。然而現(xiàn)實(shí)是殘酷的,隨著請(qǐng)求數(shù)據(jù)的不斷增多,將數(shù)據(jù)全部緩存到內(nèi)存顯得不合實(shí)際。幸運(yùn)的是,我們并不需要將所有數(shù)據(jù)都緩存起來,根據(jù)二八法則,80%的業(yè)務(wù)請(qǐng)求都集中在20%的熱點(diǎn)數(shù)據(jù)上,因此將這部分?jǐn)?shù)據(jù)緩存起就可以極大地提升系統(tǒng)性能。

  HBase在實(shí)現(xiàn)中提供了兩種緩存結(jié)構(gòu):MemStore和BlockCache。其中MemStore稱為寫緩存,HBase執(zhí)行寫操作首先會(huì)將數(shù)據(jù)寫入MemStore,并順序?qū)懭際Log,等滿足一定條件后統(tǒng)一將MemStore中數(shù)據(jù)刷新到磁盤,這種設(shè)計(jì)可以極大地提升HBase的寫性能。不僅如此,MemStore對(duì)于讀性能也至關(guān)重要,假如沒有MemStore,讀取剛寫入的數(shù)據(jù)就需要從文件中通過IO查找,這種代價(jià)顯然是昂貴的!BlockCache稱為讀緩存,HBase會(huì)將一次文件查找的Block塊緩存到Cache中,以便后續(xù)同一請(qǐng)求或者鄰近數(shù)據(jù)查找請(qǐng)求,可以直接從內(nèi)存中獲取,避免昂貴的IO操作。MemStore相關(guān)知識(shí)可以戳這里,本文將重點(diǎn)分析BlockCache。

  在介紹BlockCache之前,簡(jiǎn)單地回顧一下HBase中Block的概念,詳細(xì)介紹戳這里。 Block是HBase中最小的數(shù)據(jù)存儲(chǔ)單元,默認(rèn)為64K,在建表語句中可以通過參數(shù)BlockSize指定。HBase中Block分為四種類型:Data Block,Index Block,Bloom Block和Meta Block。其中Data Block用于存儲(chǔ)實(shí)際數(shù)據(jù),通常情況下每個(gè)Data Block可以存放多條KeyValue數(shù)據(jù)對(duì);Index Block和Bloom Block都用于優(yōu)化隨機(jī)讀的查找路徑,其中Index Block通過存儲(chǔ)索引數(shù)據(jù)加快數(shù)據(jù)查找,而Bloom Block通過一定算法可以過濾掉部分一定不存在待查KeyValue的數(shù)據(jù)文件,減少不必要的IO操作;Meta Block主要存儲(chǔ)整個(gè)HFile的元數(shù)據(jù)。

  BlockCache是Region Server級(jí)別的,一個(gè)Region Server只有一個(gè)Block Cache,在Region Server啟動(dòng)的時(shí)候完成Block Cache的初始化工作。到目前為止,HBase先后實(shí)現(xiàn)了3種Block Cache方案,LRUBlockCache是最初的實(shí)現(xiàn)方案,也是默認(rèn)的實(shí)現(xiàn)方案;HBase 0.92版本實(shí)現(xiàn)了第二種方案SlabCache,見HBASE-4027;HBase 0.96之后官方提供了另一種可選方案BucketCache,見HBASE-7404。

  這三種方案的不同之處在于對(duì)內(nèi)存的管理模式,其中LRUBlockCache是將所有數(shù)據(jù)都放入JVM Heap中,交給JVM進(jìn)行管理。而后兩者采用了不同機(jī)制將部分?jǐn)?shù)據(jù)存儲(chǔ)在堆外,交給HBase自己管理。這種演變過程是因?yàn)長(zhǎng)RUBlockCache方案中JVM垃圾回收機(jī)制經(jīng)常會(huì)導(dǎo)致程序長(zhǎng)時(shí)間暫停,而采用堆外內(nèi)存對(duì)數(shù)據(jù)進(jìn)行管理可以有效避免這種情況發(fā)生。

  LRUBlockCache

  HBase默認(rèn)的BlockCache實(shí)現(xiàn)方案。Block數(shù)據(jù)塊都存儲(chǔ)在 JVM heap內(nèi),由JVM進(jìn)行垃圾回收管理。它將內(nèi)存從邏輯上分為了三塊:single-access區(qū)、mutil-access區(qū)、in-memory區(qū),分別占到整個(gè)BlockCache大小的25%、50%、25%。一次隨機(jī)讀中,一個(gè)Block塊從HDFS中加載出來之后首先放入signle區(qū),后續(xù)如果有多次請(qǐng)求訪問到這塊數(shù)據(jù)的話,就會(huì)將這塊數(shù)據(jù)移到mutil-access區(qū)。而in-memory區(qū)表示數(shù)據(jù)可以常駐內(nèi)存,一般用來存放訪問頻繁、數(shù)據(jù)量小的數(shù)據(jù),比如元數(shù)據(jù),用戶也可以在建表的時(shí)候通過設(shè)置列族屬性IN-MEMORY= true將此列族放入in-memory區(qū)。很顯然,這種設(shè)計(jì)策略類似于JVM中young區(qū)、old區(qū)以及perm區(qū)。無論哪個(gè)區(qū),系統(tǒng)都會(huì)采用嚴(yán)格的Least-Recently-Used算法,當(dāng)BlockCache總量達(dá)到一定閾值之后就會(huì)啟動(dòng)淘汰機(jī)制,最少使用的Block會(huì)被置換出來,為新加載的Block預(yù)留空間。

  SlabCache

  為了解決LRUBlockCache方案中因?yàn)镴VM垃圾回收導(dǎo)致的服務(wù)中斷,SlabCache方案使用Java NIO DirectByteBuffer技術(shù)實(shí)現(xiàn)了堆外內(nèi)存存儲(chǔ),不再由JVM管理數(shù)據(jù)內(nèi)存。默認(rèn)情況下,系統(tǒng)在初始化的時(shí)候會(huì)分配兩個(gè)緩存區(qū),分別占整個(gè)BlockCache大小的80%和20%,每個(gè)緩存區(qū)分別存儲(chǔ)固定大小的Block塊,其中前者主要存儲(chǔ)小于等于64K大小的Block,后者存儲(chǔ)小于等于128K Block,如果一個(gè)Block太大就會(huì)導(dǎo)致兩個(gè)區(qū)都無法緩存。和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法對(duì)過期Block進(jìn)行淘汰。和LRUBlockCache不同的是,SlabCache淘汰Block的時(shí)候只需要將對(duì)應(yīng)的bufferbyte標(biāo)記為空閑,后續(xù)cache對(duì)其上的內(nèi)存直接進(jìn)行覆蓋即可。

  線上集群環(huán)境中,不同表不同列族設(shè)置的BlockSize都可能不同,很顯然,默認(rèn)只能存儲(chǔ)兩種固定大小Block的SlabCache方案不能滿足部分用戶場(chǎng)景,比如用戶設(shè)置BlockSize = 256K,簡(jiǎn)單使用SlabCache方案就不能達(dá)到這部分Block緩存的目的。因此HBase實(shí)際實(shí)現(xiàn)中將SlabCache和LRUBlockCache搭配使用,稱為DoubleBlockCache。一次隨機(jī)讀中,一個(gè)Block塊從HDFS中加載出來之后會(huì)在兩個(gè)Cache中分別存儲(chǔ)一份;緩存讀時(shí)首先在LRUBlockCache中查找,如果Cache Miss再在SlabCache中查找,此時(shí)如果命中再將該Block放入LRUBlockCache中。

  經(jīng)過實(shí)際測(cè)試,DoubleBlockCache方案有很多弊端。比如SlabCache設(shè)計(jì)中固定大小內(nèi)存設(shè)置會(huì)導(dǎo)致實(shí)際內(nèi)存使用率比較低,而且使用LRUBlockCache緩存Block依然會(huì)因?yàn)镴VM GC產(chǎn)生大量?jī)?nèi)存碎片。因此在HBase 0.98版本之后,該方案已經(jīng)被不建議使用。

  BucketCache

  SlabCache方案在實(shí)際應(yīng)用中并沒有很大程度改善原有LRUBlockCache方案的GC弊端,還額外引入了諸如堆外內(nèi)存使用率低的缺陷。然而它的設(shè)計(jì)并不是一無是處,至少在使用堆外內(nèi)存這個(gè)方面給予了阿里大牛們很多啟發(fā)。站在SlabCache的肩膀上,他們開發(fā)了BucketCache緩存方案并貢獻(xiàn)給了社區(qū)。

  BucketCache通過配置可以工作在三種模式下:heap,offheap和file。無論工作在那種模式下,BucketCache都會(huì)申請(qǐng)?jiān)S多帶有固定大小標(biāo)簽的Bucket,和SlabCache一樣,一種Bucket存儲(chǔ)一種指定BlockSize的數(shù)據(jù)塊,但和SlabCache不同的是,BucketCache會(huì)在初始化的時(shí)候申請(qǐng)14個(gè)不同大小的Bucket,而且即使在某一種Bucket空間不足的情況下,系統(tǒng)也會(huì)從其他Bucket空間借用內(nèi)存使用,不會(huì)出現(xiàn)內(nèi)存使用率低的情況。接下來再來看看不同工作模式,heap模式表示這些Bucket是從JVM Heap中申請(qǐng),offheap模式使用DirectByteBuffer技術(shù)實(shí)現(xiàn)堆外內(nèi)存存儲(chǔ)管理,而file模式使用類似SSD的高速緩存文件存儲(chǔ)數(shù)據(jù)塊。

  實(shí)際實(shí)現(xiàn)中,HBase將BucketCache和LRUBlockCache搭配使用,稱為CombinedBlockCache。和DoubleBlockCache不同,系統(tǒng)在LRUBlockCache中主要存儲(chǔ)Index Block和Bloom Block,而將Data Block存儲(chǔ)在BucketCache中。因此一次隨機(jī)讀需要首先在LRUBlockCache中查到對(duì)應(yīng)的Index Block,然后再到BucketCache查找對(duì)應(yīng)數(shù)據(jù)塊。BucketCache通過更加合理的設(shè)計(jì)修正了SlabCache的弊端,極大降低了JVM GC對(duì)業(yè)務(wù)請(qǐng)求的實(shí)際影響,但也存在一些問題,比如使用堆外內(nèi)存會(huì)存在拷貝內(nèi)存的問題,一定程度上會(huì)影響讀寫性能。當(dāng)然,在后來的版本中這個(gè)問題也得到了解決,見HBASE-11425。

  本文是HBase BlockCache系列文章的第一篇,主要概述了HBase中MemStore和BlockCache,再分別對(duì)三種BlockCache方案進(jìn)行了基本介紹。接下來第二篇文章會(huì)主要對(duì)LRUBlockCache和BucketCache兩種方案進(jìn)行詳細(xì)的介紹,敬請(qǐng)期待!。

  Categories:BucketCache, HBase, LRUBlockCache, SlabCacheTags:blockcache

  開源MySQL多線程邏輯導(dǎo)入工具myloader原理與改進(jìn)

  April 5th, 2016wzpywzh 閱讀(0)Comments off

  在上一篇中,介紹了多線程備份工具mydumper的實(shí)現(xiàn)及網(wǎng)易對(duì)其所做的優(yōu)化,本篇聊聊與mydumper配合使用的myloader工具。

  myloader是MySQL領(lǐng)域少有的多線程的恢復(fù)工具,為了能夠更好的理解其如何進(jìn)行工作,有必要對(duì)mydumper[……]

  閱讀全文

  Categories:MySQLTags:

  支持redis節(jié)點(diǎn)高可用的twemproxy

  April 5th, 2016何李夫 閱讀(419)No comments

  原生twemporxy

  twemproxy支持一個(gè)proxy實(shí)例同時(shí)代理多個(gè)分布式集群(server pools),每個(gè)集群使用不同的網(wǎng)絡(luò)端口實(shí)現(xiàn)數(shù)據(jù)流的隔離,下圖中port1應(yīng)用于cluster1代理,port2應(yīng)用于cluster2代理:

  

  今天要介紹的是twemproxy對(duì)redis節(jié)點(diǎn)高可用的支持,拿上圖的其中一個(gè)分布式集群進(jìn)行示例,邏輯結(jié)構(gòu)如下:

  

  客戶端client流入的請(qǐng)求,在proxy上進(jìn)行路由分片,然后轉(zhuǎn)發(fā)到后端的redis節(jié)點(diǎn)上存儲(chǔ)或者讀取。事實(shí)上,大家已經(jīng)注意到后端的redis節(jié)點(diǎn)只有一個(gè)點(diǎn),在出現(xiàn)異常情況下,是很容易掉線的。按twemproxy的設(shè)計(jì),它可以自動(dòng)識(shí)別失效節(jié)點(diǎn)并將其剔除,同時(shí)落在原來節(jié)點(diǎn)上的請(qǐng)求會(huì)分?jǐn)偟狡溆嗟墓?jié)點(diǎn)上。這是分布式緩存系統(tǒng)的一種通用做法,但需要忍受這個(gè)失效節(jié)點(diǎn)上的數(shù)據(jù)丟失,這種情況是否可以接受?

  在業(yè)內(nèi),redis雖然被定位為緩存系統(tǒng),但事實(shí)上,無論哪種業(yè)務(wù)場(chǎng)景(我們接觸過的)都不愿意接受節(jié)點(diǎn)掉線帶來的數(shù)據(jù)丟失,因?yàn)槟菢訉?duì)他們系統(tǒng)的影響實(shí)在太大了,更有甚者在壓力大的時(shí)候引起后端數(shù)據(jù)庫被擊穿的風(fēng)險(xiǎn)。所以,我們打算改造twemproxy,前后總共有幾個(gè)版本,下面分享給各位的是我們目前線上在跑的版本。

  定制化改造

  在上圖的基礎(chǔ)上,我們?cè)黾恿伺cmanager交互的模塊、增加了與sentinel(redis-sentinel)交互的模塊,修改了redis連接管理模塊,圖中三個(gè)紅色虛線框所示:

  

  manager交互模塊

  增加連接manager的客戶端交互模塊,用于發(fā)送心跳消息,從心跳應(yīng)答包里獲取group名稱列表和sentinel列表(IP/PORT信息),即整個(gè)分布式集群的配置信息,其中心跳消息帶有版本信息,發(fā)送間隔可配置。

  sentinel交互模塊

  增加與sentinel客戶端交互模塊(IP/PORT信息來自于manager),發(fā)送group名稱給sentinel獲取redis主節(jié)點(diǎn)的IP/PORT信息,一個(gè)group對(duì)應(yīng)一個(gè)主節(jié)點(diǎn)。取到所有主節(jié)點(diǎn)后,訂閱主從切換頻道,獲取切換消息用于觸發(fā)proxy和主節(jié)點(diǎn)間的連接切換。這里需要解析sentinel的響應(yīng)消息,會(huì)比較繁瑣一些。當(dāng)proxy開始與sentinel節(jié)點(diǎn)的交互過程,需要啟動(dòng)定時(shí)器,用以控制交互結(jié)果,當(dāng)定時(shí)器超時(shí)交互未結(jié)束(或者proxy未正常工作),proxy將主動(dòng)切換到下一個(gè)sentinel節(jié)點(diǎn),并啟動(dòng)新的交互過程??紤]到proxy與sentinel之間網(wǎng)絡(luò)連接的重要性(連接假死,proxy收不到主從切換消息,不能正常切換),增加了定時(shí)心跳機(jī)制,確保這條TCP鏈路的可用性。

  redis連接管理模塊

  原先redis節(jié)點(diǎn)的IP/PORT信息來自于靜態(tài)配置文件,是固定的,而改造以后這些信息是從sentinel節(jié)點(diǎn)獲取。為了確保獲取到的IP/PORT信息的準(zhǔn)確性,需要向IP/PORT對(duì)應(yīng)的節(jié)點(diǎn)驗(yàn)證是否是主節(jié)點(diǎn)的邏輯,只有返回確認(rèn)是主節(jié)點(diǎn),才認(rèn)為是合法的。整個(gè)過程,按官方指導(dǎo)實(shí)現(xiàn),不存在漏洞。

  詳細(xì)消息流

  為了清晰的描述proxy的內(nèi)部處理邏輯,制作了如下消息流圖:

  

  綠色為業(yè)務(wù)通道,用于透?jìng)鳂I(yè)務(wù)層數(shù)據(jù);

  紫色為命令通道(紅線的細(xì)化),用于初始化和節(jié)點(diǎn)主從切換:

箭頭1:manager heartbeat req;

箭頭2:manager heartbeat rsp;

箭頭3:sentinel get-master-addr-by-name req;

箭頭4:sentinel get-master-addr-by-name rsp;

箭頭5:redis auth & role req;

箭頭6:redis auth & role rsp;

箭頭7:sentinel psubscribe +switch-master req;

箭頭8:sentinel psubscribe +switch-master rsp;

箭頭9:sentinel pmessage;

命令通道命令順序按數(shù)字1-8進(jìn)行,7/8是proxy與sentinel的心跳消息,9是主從切換消息;

  高可用影響面分析

在sentinel節(jié)點(diǎn)切換的過程中,存在proxy正在對(duì)外提供業(yè)務(wù)服務(wù)的狀態(tài),這時(shí)候正在處理的數(shù)據(jù)將繼續(xù)處理,不會(huì)受到影響,而新接入的客戶端連接將會(huì)被拒絕,已有的客戶端連接上的新的業(yè)務(wù)請(qǐng)求數(shù)據(jù)也會(huì)被拒絕。sentinel節(jié)點(diǎn)切換,對(duì)系統(tǒng)的影響是毫秒級(jí)別,前面的設(shè)計(jì)對(duì)業(yè)務(wù)系統(tǒng)來講會(huì)顯得比較友好、不那么粗魯;

而redis節(jié)點(diǎn)的主從切換對(duì)系統(tǒng)的影響,主要集中在proxy發(fā)現(xiàn)主節(jié)點(diǎn)異常到sentinel集群做出主從切換這個(gè)過程,這段時(shí)間內(nèi)落在該節(jié)點(diǎn)上的業(yè)務(wù)都將失敗,而該時(shí)間段的長(zhǎng)度主要依賴在sentinel節(jié)點(diǎn)上的down-after-milliseconds配置字段;

  經(jīng)驗(yàn)總結(jié)

作為代理中間件,支持pipeline的能力有限,容易產(chǎn)生消息積壓,導(dǎo)致客戶端大量超時(shí),所以慎用pipeline功能;

高負(fù)荷下容易吃內(nèi)存,struct msg和struct mbuf對(duì)象會(huì)被大量緩存在進(jìn)程內(nèi)(內(nèi)存池化);

zero copy,對(duì)于多個(gè)連續(xù)請(qǐng)求(TCP粘包)進(jìn)行拆分,拷貝是無法避免的,但是有優(yōu)化空間;

  問卷調(diào)查

  Read more…

  Categories:redisTags:

  HBase – 探索HFile索引機(jī)制

  April 3rd, 2016范欣欣 閱讀(0)Comments off

  HFile索引結(jié)構(gòu)解析

  HFile中索引結(jié)構(gòu)根據(jù)索引層級(jí)的不同分為兩種:single-level和mutil-level,前者表示單層索引,后者表示多級(jí)索引,一般為兩級(jí)或三級(jí)。HFile V1版本中只有single-level一種索引結(jié)構(gòu),V2版本中引入多級(jí)索引。之所以引入多級(jí)索引,是因?yàn)殡S著HFile文件越來越大,Data Block越來越多,索引數(shù)據(jù)也越來越大,已經(jīng)無法全部加載到內(nèi)存中(V1版本中一個(gè)Region Server的索引數(shù)據(jù)加載到內(nèi)存會(huì)占用幾乎6G空間),多級(jí)索引可以只加載部分索引,降低內(nèi)存使用空間。上一篇文章 《HBase-存儲(chǔ)文件HFile結(jié)構(gòu)解析》,我們提到Bloom Filter內(nèi)存使用問題是促使V1版本升級(jí)到V2版本的一個(gè)原因,再加上這個(gè)原因,這兩個(gè)原因就是V1版本升級(jí)到V2版本最重要的兩個(gè)因素。

  V2版本Index Block有兩類:Root Index Block和NonRoot Index Block,其中NonRoot Index Block又分為Intermediate Index Block和Leaf Index Block兩種。HFile中索引結(jié)構(gòu)類似于一棵樹,Root Index Block表示索引數(shù)根節(jié)點(diǎn),Intermediate Index Block表示中間節(jié)點(diǎn),Leaf Index block表示葉子節(jié)點(diǎn),葉子節(jié)點(diǎn)直接指向?qū)嶋H數(shù)據(jù)塊。

  HFile中除了Data Block需要索引之外,上一篇文章提到過Bloom Block也需要索引,索引結(jié)構(gòu)實(shí)際上就是采用了single-level結(jié)構(gòu),文中Bloom Index Block就是一種Root Index Block。

  對(duì)于Data Block,由于HFile剛開始數(shù)據(jù)量較小,索引采用single-level結(jié)構(gòu),只有Root Index一層索引,直接指向數(shù)據(jù)塊。當(dāng)數(shù)據(jù)量慢慢變大,Root Index Block滿了之后,索引就會(huì)變?yōu)閙util-level結(jié)構(gòu),由一層索引變?yōu)閮蓪?,根?jié)點(diǎn)指向葉子節(jié)點(diǎn),葉子節(jié)點(diǎn)指向?qū)嶋H數(shù)據(jù)塊。如果數(shù)據(jù)量再變大,索引層級(jí)就會(huì)變?yōu)槿龑印?/p>

  下面就針對(duì)Root index Block和NonRoot index Block兩種結(jié)構(gòu)進(jìn)行解析,因?yàn)镽oot Index Block已經(jīng)在上面一篇文章中分析過,此處簡(jiǎn)單帶過,重點(diǎn)介紹NonRoot Index Block結(jié)構(gòu)(InterMediate Index Block和Ieaf Index Block在內(nèi)存和磁盤中存儲(chǔ)格式相同,都為NonRoot Index Block格式)。

  Root Index Block

  Root Index Block表示索引樹根節(jié)點(diǎn)索引塊,可以作為bloom的直接索引,也可以作為data索引的根索引。而且對(duì)于single-level和mutil-level兩種索引結(jié)構(gòu)對(duì)應(yīng)的Root Index Block略有不同,本文以mutil-level索引結(jié)構(gòu)為例進(jìn)行分析(single-level索引結(jié)構(gòu)是mutual-level的一種簡(jiǎn)化場(chǎng)景),在內(nèi)存和磁盤中的格式如下圖所示:

  

  其中Index Entry表示具體的索引對(duì)象,每個(gè)索引對(duì)象由3個(gè)字段組成,Block Offset表示索引指向數(shù)據(jù)塊的偏移量,BlockDataSize表示索引指向數(shù)據(jù)塊在磁盤上的大小,BlockKey表示索引指向數(shù)據(jù)塊中的第一個(gè)key。除此之外,還有另外3個(gè)字段用來記錄MidKey的相關(guān)信息,MidKey表示HFile所有Data Block中中間的一個(gè)Data Block,用于在對(duì)HFile進(jìn)行split操作時(shí),快速定位HFile的中間位置。需要注意的是single-level索引結(jié)構(gòu)和mutil-level結(jié)構(gòu)相比,就只缺少M(fèi)idKey這三個(gè)字段。

  Root Index Block會(huì)在HFile解析的時(shí)候直接加載到內(nèi)存中,此處需要注意在Trailer Block中有一個(gè)字段為dataIndexCount,就表示此處Index Entry的個(gè)數(shù)。因?yàn)镮ndex Entry并不定長(zhǎng),只有知道Entry的個(gè)數(shù)才能正確的將所有Index Entry加載到內(nèi)存。

  NonRoot Index Block

  當(dāng)HFile中Data Block越來越多,single-level結(jié)構(gòu)的索引已經(jīng)不足以支撐所有數(shù)據(jù)都加載到內(nèi)存,需要分化為mutil-level結(jié)構(gòu)。mutil-level結(jié)構(gòu)中NonRoot Index Block作為中間層節(jié)點(diǎn)或者葉子節(jié)點(diǎn)存在,無論是中間節(jié)點(diǎn)還是葉子節(jié)點(diǎn),其都擁有相同的結(jié)構(gòu),如下圖所示:

  

  和Root Index Block相同,NonRoot Index Block中最核心的字段也是Index Entry,用于指向葉子節(jié)點(diǎn)塊或者數(shù)據(jù)塊。不同的是,NonRoot Index Block結(jié)構(gòu)中增加了block塊的內(nèi)部索引entry Offset字段,entry Offset表示index Entry在該block中的相對(duì)偏移量(相對(duì)于第一個(gè)index Entry),用于實(shí)現(xiàn)block內(nèi)的二分查找。所有非根節(jié)點(diǎn)索引塊,包括Intermediate index block和leaf index block,在其內(nèi)部定位一個(gè)key的具體索引并不是通過遍歷實(shí)現(xiàn),而是使用二分查找算法,這樣可以更加高效快速地定位到待查找key。

  HFile數(shù)據(jù)完整索引流程

  了解了HFile中數(shù)據(jù)索引塊的兩種結(jié)構(gòu)之后,就來看看如何使用這些索引數(shù)據(jù)塊進(jìn)行數(shù)據(jù)的高效檢索。整個(gè)索引體系類似于MySQL的B+樹結(jié)構(gòu),但是又有所不同,比B+樹簡(jiǎn)單,并沒有復(fù)雜的分裂操作。具體見下圖所示:

  

  圖中上面三層為索引層,在數(shù)據(jù)量不大的時(shí)候只有最上面一層,數(shù)據(jù)量大了之后開始分裂為多層,最多三層,如圖所示。最下面一層為數(shù)據(jù)層,存儲(chǔ)用戶的實(shí)際keyvalue數(shù)據(jù)。這個(gè)索引樹結(jié)構(gòu)類似于InnoSQL的聚集索引,只是HBase并沒有輔助索引的概念。

  圖中紅線表示一次查詢的索引過程(HBase中相關(guān)類為HFileBlockIndex和HFileReaderV2),基本流程可以表示為:

  1. 用戶輸入rowkey為fb,在root index block中通過二分查找定位到fb在’a’和’m’之間,因此需要訪問索引’a’指向的中間節(jié)點(diǎn)。因?yàn)閞oot index block常駐內(nèi)存,所以這個(gè)過程很快。

  2. 將索引’a’指向的中間節(jié)點(diǎn)索引塊加載到內(nèi)存,然后通過二分查找定位到fb在index ‘d’和’h’之間,接下來訪問索引’d’指向的葉子節(jié)點(diǎn)。

  3. 同理,將索引’d’指向的中間節(jié)點(diǎn)索引塊加載到內(nèi)存,一樣通過二分查找定位找到fb在index ‘f’和’g’之間,最后需要訪問索引’f’指向的數(shù)據(jù)塊節(jié)點(diǎn)。

  4. 將索引’f’指向的數(shù)據(jù)塊加載到內(nèi)存,通過遍歷的方式找到對(duì)應(yīng)的keyvalue。

  上述流程中因?yàn)橹虚g節(jié)點(diǎn)、葉子節(jié)點(diǎn)和數(shù)據(jù)塊都需要加載到內(nèi)存,所以io次數(shù)正常為3次。但是實(shí)際上HBase為block提供了緩存機(jī)制,可以將頻繁使用的block緩存在內(nèi)存中,可以進(jìn)一步加快實(shí)際讀取過程。所以,在HBase中,通常一次隨機(jī)讀請(qǐng)求最多會(huì)產(chǎn)生3次io,如果數(shù)據(jù)量?。ㄖ挥幸粚铀饕?,數(shù)據(jù)已經(jīng)緩存到了內(nèi)存,就不會(huì)產(chǎn)生io。

  索引塊分裂

  上文中已經(jīng)提到,當(dāng)數(shù)據(jù)量少、文件小的時(shí)候,只需要一個(gè)root index block就可以完成索引,即索引樹只有一層。當(dāng)數(shù)據(jù)不斷寫入,文件變大之后,索引數(shù)據(jù)也會(huì)相應(yīng)變大,索引結(jié)構(gòu)就會(huì)由single-level變?yōu)閙ulit-level,期間涉及到索引塊的寫入和分裂,本節(jié)來關(guān)注一下數(shù)據(jù)寫入是如何引起索引塊分裂的。

  如果大家之前看過HBase系列另一篇博文《HBase數(shù)據(jù)寫入之Memstore Flush》,可以知道m(xù)emstore flush主要分為3個(gè)階段,第一個(gè)階段會(huì)講memstore中的keyvalue數(shù)據(jù)snapshot,第二階段再將這部分?jǐn)?shù)據(jù)flush的HFile,并生成在臨時(shí)目錄,第三階段將臨時(shí)文件移動(dòng)到指定的ColumnFamily目錄下。很顯然,第二階段將keyvalue數(shù)據(jù)flush到HFile將會(huì)是關(guān)注的重點(diǎn)(flush相關(guān)代碼在DefaultStoreFlusher類中)。整個(gè)flush階段又可以分為兩階段:

  1. append階段:memstore中keyvalue首先會(huì)寫入到HFile中數(shù)據(jù)塊

  2. finalize階段:修改HFlie中meta元數(shù)據(jù)塊,索引數(shù)據(jù)塊以及Trailer數(shù)據(jù)塊等

  append流程

  具體keyvalue數(shù)據(jù)的append以及finalize過程在HFileWriterV2文件中,其中append流程可以大體表征為:

  

  a. 預(yù)檢查:檢查key的大小是否大于前一個(gè)key,如果大于則不符合HBase順序排列的原理,拋出異常;檢查value是否是null,如果為null也拋出異常

  b. block是否寫滿:檢查當(dāng)前Data Block是否已經(jīng)寫滿,如果沒有寫滿就直接寫入keyvalue;否則就需要執(zhí)行數(shù)據(jù)塊落盤以及索引塊修改操作;

  c. 數(shù)據(jù)落盤并修改索引:如果DataBlock寫滿,首先將block塊寫入流;再生成一個(gè)leaf index entry,寫入leaf Index block;再檢查該leaf index block是否已經(jīng)寫滿需要落盤,如果已經(jīng)寫滿,就將該leaf index block寫入到輸出流,并且為索引樹根節(jié)點(diǎn)root index block新增一個(gè)索引,指向葉子節(jié)點(diǎn)(second-level index)

  d. 生成一個(gè)新的block:重新reset輸出流,初始化startOffset為-1

  e. 寫入keyvalue:將keyvalue以流的方式寫入輸出流,同時(shí)需要寫入memstoreTS;除此之外,如果該key是當(dāng)前block的第一個(gè)key,需要賦值給變量firstKeyInBlock

  finalize階段

  memstore中所有keyvalue都經(jīng)過append階段輸出到HFile后,會(huì)執(zhí)行一次finalize過程,主要更新HFile中meta元數(shù)據(jù)塊、索引數(shù)據(jù)塊以及Trailer數(shù)據(jù)塊,其中對(duì)索引數(shù)據(jù)塊的更新是我們關(guān)心的重點(diǎn),此處詳細(xì)解析,上述append流程中c步驟’數(shù)據(jù)落盤并修改索引’會(huì)使得root index block不斷增多,當(dāng)增大到一定程度之后就需要分裂,分裂示意圖如下圖所示:

  

  上圖所示,分裂前索引結(jié)構(gòu)為second-level結(jié)構(gòu),圖中沒有畫出Data Blocks,根節(jié)點(diǎn)索引指向葉子節(jié)點(diǎn)索引塊。finalize階段系統(tǒng)會(huì)對(duì)Root Index Block進(jìn)行大小檢查,如果大小大于規(guī)定的大小就需要進(jìn)行分裂,圖中分裂過程實(shí)際上就是將原來的Root Index Block塊分割成4塊,每塊獨(dú)立形成中間節(jié)點(diǎn)InterMediate Index Block,系統(tǒng)再重新生成一個(gè)Root Index Block(圖中紅色部分),分別指向分割形成的4個(gè)interMediate Index Block。此時(shí)索引結(jié)構(gòu)就變成了third-level結(jié)構(gòu)。

  總結(jié)

  這篇文章是HFile結(jié)構(gòu)解析的第二篇文章,主要集中介紹HFile中的數(shù)據(jù)索引塊。首先分Root Index Block和NonRoot Index Block兩部分對(duì)HFile中索引塊進(jìn)行了解析,緊接著基于此介紹了HBase如何使用索引對(duì)數(shù)據(jù)進(jìn)行檢索,最后結(jié)合Memstore Flush的相關(guān)知識(shí)分析了keyvalue數(shù)據(jù)寫入的過程中索引塊的分裂過程。希望通過這兩篇文章的介紹,能夠?qū)Base中數(shù)據(jù)存儲(chǔ)文件HFile有一個(gè)更加全面深入的認(rèn)識(shí)。

  Categories:HBaseTags:HFile, 索引

  新一代列式存儲(chǔ)格式Parquet

  March 28th, 2016feng yu 閱讀(387)No comments

  Apache Parquet是Hadoop生態(tài)圈中一種新型列式存儲(chǔ)格式,它可以兼容Hadoop生態(tài)圈中大多數(shù)計(jì)算框架(Hadoop、Spark等),被多種查詢引擎支持(Hive、Impala、Drill等),并且它是語言和平臺(tái)無關(guān)的。Parquet最初是由Twitter和Cloudera(由于Impala的緣故)合作開發(fā)完成并開源,2015年5月從Apache的孵化器里畢業(yè)成為Apache頂級(jí)項(xiàng)目,最新的版本是1.8.1。

  Parquet是什么

  Parquet的靈感來自于2010年Google發(fā)表的Dremel論文,文中介紹了一種支持嵌套結(jié)構(gòu)的存儲(chǔ)格式,并且使用了列式存儲(chǔ)的方式提升查詢性能,在Dremel論文中還介紹了Google如何使用這種存儲(chǔ)格式實(shí)現(xiàn)并行查詢的,如果對(duì)此感興趣可以參考論文和開源實(shí)現(xiàn)Apache Drill。

  嵌套數(shù)據(jù)模型

  在接觸大數(shù)據(jù)之前,我們簡(jiǎn)單的將數(shù)據(jù)劃分為結(jié)構(gòu)化數(shù)據(jù)和非結(jié)構(gòu)化數(shù)據(jù),通常我們使用關(guān)系數(shù)據(jù)庫存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù),而關(guān)系數(shù)據(jù)庫中使用數(shù)據(jù)模型都是扁平式的,遇到諸如List、Map和自定義Struct的時(shí)候就需要用戶在應(yīng)用層解析。但是在大數(shù)據(jù)環(huán)境下,通常數(shù)據(jù)的來源是服務(wù)端的埋點(diǎn)數(shù)據(jù),很可能需要把程序中的某些對(duì)象內(nèi)容作為輸出的一部分,而每一個(gè)對(duì)象都可能是嵌套的,所以如果能夠原生的支持這種數(shù)據(jù),這樣在查詢的時(shí)候就不需要額外的解析便能獲得想要的結(jié)果。例如在Twitter,在他們的生產(chǎn)環(huán)境中一個(gè)典型的日志對(duì)象(一條記錄)有87個(gè)字段,其中嵌套了7層,如下圖:

  

  另外,隨著嵌套格式的數(shù)據(jù)的需求日益增加,目前Hadoop生態(tài)圈中主流的查詢引擎都支持更豐富的數(shù)據(jù)類型,例如Hive、SparkSQL、Impala等都原生的支持諸如struct、map、array這樣的復(fù)雜數(shù)據(jù)類型,這樣也就使得諸如Parquet這種原生支持嵌套數(shù)據(jù)類型的存儲(chǔ)格式也變得至關(guān)重要,性能也會(huì)更好。

  列式存儲(chǔ)

  列式存儲(chǔ),顧名思義就是按照列進(jìn)行存儲(chǔ)數(shù)據(jù),把某一列的數(shù)據(jù)連續(xù)的存儲(chǔ),每一行中的不同列的值離散分布。列式存儲(chǔ)技術(shù)并不新鮮,在關(guān)系數(shù)據(jù)庫中都已經(jīng)在使用,尤其是在針對(duì)OLAP場(chǎng)景下的數(shù)據(jù)存儲(chǔ),由于OLAP場(chǎng)景下的數(shù)據(jù)大部分情況下都是批量導(dǎo)入,基本上不需要支持單條記錄的增刪改操作,而查詢的時(shí)候大多數(shù)都是只使用部分列進(jìn)行過濾、聚合,對(duì)少數(shù)列進(jìn)行計(jì)算(基本不需要select * from xx之類的查詢)。列式存儲(chǔ)可以大大提升這類查詢的性能,較之于行是存儲(chǔ),列式存儲(chǔ)能夠帶來這些優(yōu)化:

  1、由于每一列中的數(shù)據(jù)類型相同,所以可以針對(duì)不同類型的列使用不同的編碼和壓縮方式,這樣可以大大降低數(shù)據(jù)存儲(chǔ)空間。

  2、讀取數(shù)據(jù)的時(shí)候可以把映射(Project)下推,只需要讀取需要的列,這樣可以大大減少每次查詢的I/O數(shù)據(jù)量,更甚至可以支持謂詞下推,跳過不滿足條件的列。

  3、由于每一列的數(shù)據(jù)類型相同,可以使用更加適合CPU pipeline的編碼方式,減小CPU的緩存失效。

  Parquet的組成

  Parquet僅僅是一種存儲(chǔ)格式,它是語言、平臺(tái)無關(guān)的,并且不需要和任何一種數(shù)據(jù)處理框架綁定,目前能夠和Parquet適配的組件包括下面這些,可以看出基本上通常使用的查詢引擎和計(jì)算框架都已適配,并且可以很方便的將其它序列化工具生成的數(shù)據(jù)轉(zhuǎn)換成Parquet格式。

  查詢引擎: Hive, Impala, Pig, Presto, Drill, Tajo, HAWQ, IBM Big SQL

  計(jì)算框架: MapReduce, Spark, Cascading, Crunch, Scalding, Kite

  數(shù)據(jù)模型: Avro, Thrift, Protocol Buffers, POJOs

  項(xiàng)目組成

  Parquet項(xiàng)目由以下幾個(gè)子項(xiàng)目組成:

  parquet-format項(xiàng)目由java實(shí)現(xiàn),它定義了所有Parquet元數(shù)據(jù)對(duì)象,Parquet的元數(shù)據(jù)是使用Apache Thrift進(jìn)行序列化并存儲(chǔ)在Parquet文件的尾部。

  parquet-mr項(xiàng)目由java實(shí)現(xiàn),它包括多個(gè)模塊,包括實(shí)現(xiàn)了讀寫Parquet文件的功能,并且提供一些和其它組件適配的工具,例如Hadoop Input/Output Formats、Hive Serde(目前Hive已經(jīng)自帶Parquet了)、Pig loaders等。

  parquet-compatibility項(xiàng)目,包含不同編程語言之間(JAVA和C/C++)讀寫文件的測(cè)試代碼。

  parquet-cpp項(xiàng)目,它是用于用于讀寫Parquet文件的C++庫。

  下圖展示了Parquet各個(gè)組件的層次以及從上到下交互的方式。

  

  數(shù)據(jù)存儲(chǔ)層定義了Parquet的文件格式,其中元數(shù)據(jù)在parquet-format中定義,包括Parquet原始類型定義、Page類型、編碼類型、壓縮類型等等。

  對(duì)象轉(zhuǎn)換層完成其他對(duì)象模型與Parquet內(nèi)部數(shù)據(jù)模型的映射和轉(zhuǎn)換,Parquet的編碼方式使用的是striping and assembly算法。

  對(duì)象模型層定義了如何讀取Parquet文件的內(nèi)容,這一層轉(zhuǎn)換包括Avro、Thrift、PB等序列化格式、Hive serde等的適配。并且為了幫助大家理解和使用,Parquet提供了org.apache.parquet.example包實(shí)現(xiàn)了java對(duì)象和Parquet文件的轉(zhuǎn)換。

  數(shù)據(jù)模型

  Parquet支持嵌套的數(shù)據(jù)模型,類似于Protocol Buffers,每一個(gè)數(shù)據(jù)模型的schema包含多個(gè)字段,每一個(gè)字段又可以包含多個(gè)字段,每一個(gè)字段有三個(gè)屬性:重復(fù)數(shù)、數(shù)據(jù)類型和字段名,重復(fù)數(shù)可以是以下三種:required(出現(xiàn)1次),repeated(出現(xiàn)0次或多次),optional(出現(xiàn)0次或1次)。每一個(gè)字段的數(shù)據(jù)類型可以分成兩種:group(復(fù)雜類型)和primitive(基本類型)。例如Dremel中提供的Document的schema示例,它的定義如下:

  message Document {

  required int64 DocId;

  optional group Links {

  repeated int64 Backward;

  repeated int64 Forward;

  }

  repeated group Name {

  repeated group Language {

  required string Code;

  optional string Country;

  }

  optional string Url;

  }

  }

  可以把這個(gè)Schema轉(zhuǎn)換成樹狀結(jié)構(gòu),根節(jié)點(diǎn)可以理解為repeated類型,如下圖:

  

  可以看出在Schema中所有的基本類型字段都是葉子節(jié)點(diǎn),在這個(gè)Schema中一共存在6個(gè)葉子節(jié)點(diǎn),如果把這樣的Schema轉(zhuǎn)換成扁平式的關(guān)系模型,就可以理解為該表包含六個(gè)列。Parquet中沒有Map、Array這樣的復(fù)雜數(shù)據(jù)結(jié)構(gòu),但是可以通過repeated和group組合來實(shí)現(xiàn)這樣的需求。在這個(gè)包含6個(gè)字段的表中有以下幾個(gè)字段和每一條記錄中它們可能出現(xiàn)的次數(shù):

  DocId int64 只能出現(xiàn)一次

  Links.Backward int64 可能出現(xiàn)任意多次,但是如果出現(xiàn)0次則需要使用NULL標(biāo)識(shí)

  Links.Forward int64 同上

  Name.Language.Code string 同上

  Name.Language.Country string 同上

  Name.Url string 同上

  由于在一個(gè)表中可能存在出現(xiàn)任意多次的列,對(duì)于這些列需要標(biāo)示出現(xiàn)多次或者等于NULL的情況,它是由Striping/Assembly算法實(shí)現(xiàn)的。

  Striping/Assembly算法

  上文介紹了Parquet的數(shù)據(jù)模型,在Document中存在多個(gè)非required列,由于Parquet一條記錄的數(shù)據(jù)分散的存儲(chǔ)在不同的列中,如何組合不同的列值組成一條記錄是由Striping/Assembly算法決定的,在該算法中列的每一個(gè)值都包含三部分:value、repetition level和definition level。

  Repetition Levels

  為了支持repeated類型的節(jié)點(diǎn),在寫入的時(shí)候該值等于它和前面的值在哪一層節(jié)點(diǎn)是不共享的。在讀取的時(shí)候根據(jù)該值可以推導(dǎo)出哪一層上需要?jiǎng)?chuàng)建一個(gè)新的節(jié)點(diǎn),例如對(duì)于這樣的一個(gè)schema和兩條記錄。

  message nested {

  repeated group leve1 {

  repeated string leve2;

  }

  }

  r1:[[a,b,c,] , [d,e,f,g]]

  r2:[[h] , [i,j]]

  計(jì)算repetition level值的過程如下:

  value=a是一條記錄的開始,和前面的值(已經(jīng)沒有值了)在根節(jié)點(diǎn)(第0層)上是不共享的,所以repeated level=0.

  value=b它和前面的值共享了level1這個(gè)節(jié)點(diǎn),但是level2這個(gè)節(jié)點(diǎn)上是不共享的,所以repeated level=2.

  同理value=c, repeated level=2.

  value=d和前面的值共享了根節(jié)點(diǎn)(屬于相同記錄),但是在level1這個(gè)節(jié)點(diǎn)上是不共享的,所以repeated level=1.

  value=h和前面的值不屬于同一條記錄,也就是不共享任何節(jié)點(diǎn),所以repeated level=0.

  根據(jù)以上的分析每一個(gè)value需要記錄的repeated level值如下:

  

  在讀取的時(shí)候,順序的讀取每一個(gè)值,然后根據(jù)它的repeated level創(chuàng)建對(duì)象,當(dāng)讀取value=a時(shí)repeated level=0,表示需要?jiǎng)?chuàng)建一個(gè)新的根節(jié)點(diǎn)(新記錄),value=b時(shí)repeated level=2,表示需要?jiǎng)?chuàng)建一個(gè)新的level2節(jié)點(diǎn),value=d時(shí)repeated level=1,表示需要?jiǎng)?chuàng)建一個(gè)新的level1節(jié)點(diǎn),當(dāng)所有列讀取完成之后可以創(chuàng)建一條新的記錄。本例中當(dāng)讀取文件構(gòu)建每條記錄的結(jié)果如下:

  

  可以看出repeated level=0表示一條記錄的開始,并且repeated level的值只是針對(duì)路徑上的repeated類型的節(jié)點(diǎn),因此在計(jì)算該值的時(shí)候可以忽略非repeated類型的節(jié)點(diǎn),在寫入的時(shí)候?qū)⑵淅斫鉃樵摴?jié)點(diǎn)和路徑上的哪一個(gè)repeated節(jié)點(diǎn)是不共享的,讀取的時(shí)候?qū)⑵淅斫鉃樾枰谀囊粚觿?chuàng)建一個(gè)新的repeated節(jié)點(diǎn),這樣的話每一列最大的repeated level值就等于路徑上的repeated節(jié)點(diǎn)的個(gè)數(shù)(不包括根節(jié)點(diǎn))。減小repeated level的好處能夠使得在存儲(chǔ)使用更加緊湊的編碼方式,節(jié)省存儲(chǔ)空間。

  Definition Levels

  有了repeated level我們就可以構(gòu)造出一個(gè)記錄了,為什么還需要definition levels呢?由于repeated和optional類型的存在,可能一條記錄中某一列是沒有值的,假設(shè)我們不記錄這樣的值就會(huì)導(dǎo)致本該屬于下一條記錄的值被當(dāng)做當(dāng)前記錄的一部分,從而造成數(shù)據(jù)的錯(cuò)誤,因此對(duì)于這種情況需要一個(gè)占位符標(biāo)示這種情況。

  definition level的值僅僅對(duì)于空值是有效的,表示在該值的路徑上第幾層開始是未定義的,對(duì)于非空的值它是沒有意義的,因?yàn)榉强罩翟谌~子節(jié)點(diǎn)是定義的,所有的父節(jié)點(diǎn)也肯定是定義的,因此它總是等于該列最大的definition levels。例如下面的schema。

  message ExampleDefinitionLevel {

  optional group a {

  optional group b {

  optional string c;

  }

  }

  }

  它包含一個(gè)列a.b.c,這個(gè)列的的每一個(gè)節(jié)點(diǎn)都是optional類型的,當(dāng)c被定義時(shí)a和b肯定都是已定義的,當(dāng)c未定義時(shí)我們就需要標(biāo)示出在從哪一層開始時(shí)未定義的,如下面的值:

  

  由于definition level只需要考慮未定義的值,而對(duì)于repeated類型的節(jié)點(diǎn),只要父節(jié)點(diǎn)是已定義的,該節(jié)點(diǎn)就必須定義(例如Document中的DocId,每一條記錄都該列都必須有值,同樣對(duì)于Language節(jié)點(diǎn),只要它定義了Code必須有值),所以計(jì)算definition level的值時(shí)可以忽略路徑上的required節(jié)點(diǎn),這樣可以減小definition level的最大值,優(yōu)化存儲(chǔ)。

  一個(gè)完整的例子

  本節(jié)我們使用Dremel論文中給的Document示例和給定的兩個(gè)值r1和r2展示計(jì)算repeated level和definition level的過程,這里把未定義的值記錄為NULL,使用R表示repeated level,D表示definition level。

  

  首先看DocuId這一列,對(duì)于r1,DocId=10,由于它是記錄的開始并且是已定義的,所以R=0,D=0,同樣r2中的DocId=20,R=0,D=0。

  對(duì)于Links.Forward這一列,在r1中,它是未定義的但是Links是已定義的,并且是該記錄中的第一個(gè)值,所以R=0,D=1,在r1中該列有兩個(gè)值,value1=10,R=0(記錄中該列的第一個(gè)值),D=2(該列的最大definition level)。

  對(duì)于Name.Url這一列,r1中它有三個(gè)值,分別為url1=’http://A’,它是r1中該列的第一個(gè)值并且是定義的,所以R=0,D=2;value2=’http://B’,和上一個(gè)值value1在Name這一層是不相同的,所以R=1,D=2;value3=NULL,和上一個(gè)值value2在Name這一層是不相同的,所以R=1,但它是未定義的,而Name這一層是定義的,所以D=1。r2中該列只有一個(gè)值value3=’http://C’,R=0,D=2.

  最后看一下Name.Language.Code這一列,r1中有4個(gè)值,value1=’en-us’,它是r1中的第一個(gè)值并且是已定義的,所以R=0,D=2(由于Code是required類型,這一列repeated level的最大值等于2);value2=’en’,它和value1在Language這個(gè)節(jié)點(diǎn)是不共享的,所以R=2,D=2;value3=NULL,它是未定義的,但是它和前一個(gè)值在Name這個(gè)節(jié)點(diǎn)是不共享的,在Name這個(gè)節(jié)點(diǎn)是已定義的,所以R=1,D=1;value4=’en-gb’,它和前一個(gè)值在Name這一層不共享,所以R=1,D=2。在r2中該列有一個(gè)值,它是未定義的,但是Name這一層是已定義的,所以R=0,D=1.

  Parquet文件格式

  Parquet文件是以二進(jìn)制方式存儲(chǔ)的,所以是不可以直接讀取的,文件中包括該文件的數(shù)據(jù)和元數(shù)據(jù),因此Parquet格式文件是自解析的。在HDFS文件系統(tǒng)和Parquet文件中存在如下幾個(gè)概念。

  HDFS塊(Block):它是HDFS上的最小的副本單位,HDFS會(huì)把一個(gè)Block存儲(chǔ)在本地的一個(gè)文件并且維護(hù)分散在不同的機(jī)器上的多個(gè)副本,通常情況下一個(gè)Block的大小為256M、512M等。

  HDFS文件(File):一個(gè)HDFS的文件,包括數(shù)據(jù)和元數(shù)據(jù),數(shù)據(jù)分散存儲(chǔ)在多個(gè)Block中。

  行組(Row Group):按照行將數(shù)據(jù)物理上劃分為多個(gè)單元,每一個(gè)行組包含一定的行數(shù),在一個(gè)HDFS文件中至少存儲(chǔ)一個(gè)行組,Parquet讀寫的時(shí)候會(huì)將整個(gè)行組緩存在內(nèi)存中,所以如果每一個(gè)行組的大小是由內(nèi)存大的小決定的,例如記錄占用空間比較小的Schema可以在每一個(gè)行組中存儲(chǔ)更多的行。

  列塊(Column Chunk):在一個(gè)行組中每一列保存在一個(gè)列塊中,行組中的所有列連續(xù)的存儲(chǔ)在這個(gè)行組文件中。一個(gè)列塊中的值都是相同類型的,不同的列塊可能使用不同的算法進(jìn)行壓縮。

  頁(Page):每一個(gè)列塊劃分為多個(gè)頁,一個(gè)頁是最小的編碼的單位,在同一個(gè)列塊的不同頁可能使用不同的編碼方式。

  文件格式

  通常情況下,在存儲(chǔ)Parquet數(shù)據(jù)的時(shí)候會(huì)按照Block大小設(shè)置行組的大小,由于一般情況下每一個(gè)Mapper任務(wù)處理數(shù)據(jù)的最小單位是一個(gè)Block,這樣可以把每一個(gè)行組由一個(gè)Mapper任務(wù)處理,增大任務(wù)執(zhí)行并行度。Parquet文件的格式如下圖所示。

  

  上圖展示了一個(gè)Parquet文件的內(nèi)容,一個(gè)文件中可以存儲(chǔ)多個(gè)行組,文件的首位都是該文件的Magic Code,用于校驗(yàn)它是否是一個(gè)Parquet文件,F(xiàn)ooter length了文件元數(shù)據(jù)的大小,通過該值和文件長(zhǎng)度可以計(jì)算出元數(shù)據(jù)的偏移量,文件的元數(shù)據(jù)中包括每一個(gè)行組的元數(shù)據(jù)信息和該文件存儲(chǔ)數(shù)據(jù)的Schema信息。除了文件中每一個(gè)行組的元數(shù)據(jù),每一頁的開始都會(huì)存儲(chǔ)該頁的元數(shù)據(jù),在Parquet中,有三種類型的頁:數(shù)據(jù)頁、字典頁和索引頁。數(shù)據(jù)頁用于存儲(chǔ)當(dāng)前行組中該列的值,字典頁存儲(chǔ)該列值的編碼字典,每一個(gè)列塊中最多包含一個(gè)字典頁,索引頁用來存儲(chǔ)當(dāng)前行組下該列的索引,目前Parquet中還不支持索引頁,但是在后面的版本中增加。

  在執(zhí)行MR任務(wù)的時(shí)候可能存在多個(gè)Mapper任務(wù)的輸入是同一個(gè)Parquet文件的情況,每一個(gè)Mapper通過InputSplit標(biāo)示處理的文件范圍,如果多個(gè)InputSplit跨越了一個(gè)Row Group,Parquet能夠保證一個(gè)Row Group只會(huì)被一個(gè)Mapper任務(wù)處理。

  映射下推(Project PushDown)

  說到列式存儲(chǔ)的優(yōu)勢(shì),映射下推是最突出的,它意味著在獲取表中原始數(shù)據(jù)時(shí)只需要掃描查詢中需要的列,由于每一列的所有值都是連續(xù)存儲(chǔ)的,所以分區(qū)取出每一列的所有值就可以實(shí)現(xiàn)TableScan算子,而避免掃描整個(gè)表文件內(nèi)容。

  在Parquet中原生就支持映射下推,執(zhí)行查詢的時(shí)候可以通過Configuration傳遞需要讀取的列的信息,這些列必須是Schema的子集,映射每次會(huì)掃描一個(gè)Row Group的數(shù)據(jù),然后一次性得將該Row Group里所有需要的列的Cloumn Chunk都讀取到內(nèi)存中,每次讀取一個(gè)Row Group的數(shù)據(jù)能夠大大降低隨機(jī)讀的次數(shù),除此之外,Parquet在讀取的時(shí)候會(huì)考慮列是否連續(xù),如果某些需要的列是存儲(chǔ)位置是連續(xù)的,那么一次讀操作就可以把多個(gè)列的數(shù)據(jù)讀取到內(nèi)存。

  謂詞下推(Predicate PushDown)

  在數(shù)據(jù)庫之類的查詢系統(tǒng)中最常用的優(yōu)化手段就是謂詞下推了,通過將一些過濾條件盡可能的在最底層執(zhí)行可以減少每一層交互的數(shù)據(jù)量,從而提升性能,例如”select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100″SQL查詢中,在處理Join操作之前需要首先對(duì)A和B執(zhí)行TableScan操作,然后再進(jìn)行Join,再執(zhí)行過濾,最后計(jì)算聚合函數(shù)返回,但是如果把過濾條件A.a > 10和B.b < 100分別移到A表的TableScan和B表的TableScan的時(shí)候執(zhí)行,可以大大降低Join操作的輸入數(shù)據(jù)。

  無論是行式存儲(chǔ)還是列式存儲(chǔ),都可以在將過濾條件在讀取一條記錄之后執(zhí)行以判斷該記錄是否需要返回給調(diào)用者,在Parquet做了更進(jìn)一步的優(yōu)化,優(yōu)化的方法時(shí)對(duì)每一個(gè)Row Group的每一個(gè)Column Chunk在存儲(chǔ)的時(shí)候都計(jì)算對(duì)應(yīng)的統(tǒng)計(jì)信息,包括該Column Chunk的最大值、最小值和空值個(gè)數(shù)。通過這些統(tǒng)計(jì)值和該列的過濾條件可以判斷該Row Group是否需要掃描。另外Parquet未來還會(huì)增加諸如Bloom Filter和Index等優(yōu)化數(shù)據(jù),更加有效的完成謂詞下推。

  在使用Parquet的時(shí)候可以通過如下兩種策略提升查詢性能:1、類似于關(guān)系數(shù)據(jù)庫的主鍵,對(duì)需要頻繁過濾的列設(shè)置為有序的,這樣在導(dǎo)入數(shù)據(jù)的時(shí)候會(huì)根據(jù)該列的順序存儲(chǔ)數(shù)據(jù),這樣可以最大化的利用最大值、最小值實(shí)現(xiàn)謂詞下推。2、減小行組大小和頁大小,這樣增加跳過整個(gè)行組的可能性,但是此時(shí)需要權(quán)衡由于壓縮和編碼效率下降帶來的I/O負(fù)載。

  性能

  相比傳統(tǒng)的行式存儲(chǔ),Hadoop生態(tài)圈近年來也涌現(xiàn)出諸如RC、ORC、Parquet的列式存儲(chǔ)格式,它們的性能優(yōu)勢(shì)主要體現(xiàn)在兩個(gè)方面:1、更高的壓縮比,由于相同類型的數(shù)據(jù)更容易針對(duì)不同類型的列使用高效的編碼和壓縮方式。2、更小的I/O操作,由于映射下推和謂詞下推的使用,可以減少一大部分不必要的數(shù)據(jù)掃描,尤其是表結(jié)構(gòu)比較龐大的時(shí)候更加明顯,由此也能夠帶來更好的查詢性能。

  

  上圖是展示了使用不同格式存儲(chǔ)TPC-H和TPC-DS數(shù)據(jù)集中兩個(gè)表數(shù)據(jù)的文件大小對(duì)比,可以看出Parquet較之于其他的二進(jìn)制文件存儲(chǔ)格式能夠更有效的利用存儲(chǔ)空間,而新版本的Parquet(2.0版本)使用了更加高效的頁存儲(chǔ)方式,進(jìn)一步的提升存儲(chǔ)空間。

  

  上圖展示了Twitter在Impala中使用不同格式文件執(zhí)行TPC-DS基準(zhǔn)測(cè)試的結(jié)果,測(cè)試結(jié)果可以看出Parquet較之于其他的行式存儲(chǔ)格式有較明顯的性能提升。

  

  上圖展示了criteo公司在Hive中使用ORC和Parquet兩種列式存儲(chǔ)格式執(zhí)行TPC-DS基準(zhǔn)測(cè)試的結(jié)果,測(cè)試結(jié)果可以看出在數(shù)據(jù)存儲(chǔ)方面,兩種存儲(chǔ)格式在都是用snappy壓縮的情況下量中存儲(chǔ)格式占用的空間相差并不大,查詢的結(jié)果顯示Parquet格式稍好于ORC格式,兩者在功能上也都有優(yōu)缺點(diǎn),Parquet原生支持嵌套式數(shù)據(jù)結(jié)構(gòu),而ORC對(duì)此支持的較差,這種復(fù)雜的Schema查詢也相對(duì)較差;而Parquet不支持?jǐn)?shù)據(jù)的修改和ACID,但是ORC對(duì)此提供支持,但是在OLAP環(huán)境下很少會(huì)對(duì)單條數(shù)據(jù)修改,更多的則是批量導(dǎo)入。

  項(xiàng)目發(fā)展

  自從2012年由Twitter和Cloudera共同研發(fā)Parquet開始,該項(xiàng)目一直處于高速發(fā)展之中,并且在項(xiàng)目之初就將其貢獻(xiàn)給開源社區(qū),2013年,Criteo公司加入開發(fā)并且向Hive社區(qū)提交了向hive集成Parquet的patch(HIVE-5783),在Hive 0.13版本之后正式加入了Parquet的支持;之后越來越多的查詢引擎對(duì)此進(jìn)行支持,也進(jìn)一步帶動(dòng)了Parquet的發(fā)展。

  目前Parquet正處于向2.0版本邁進(jìn)的階段,在新的版本中實(shí)現(xiàn)了新的Page存儲(chǔ)格式,針對(duì)不同的類型優(yōu)化編碼算法,另外豐富了支持的原始類型,增加了Decimal、Timestamp等類型的支持,增加更加豐富的統(tǒng)計(jì)信息,例如Bloon Filter,能夠盡可能得將謂詞下推在元數(shù)據(jù)層完成。

  總結(jié)

  本文介紹了一種支持嵌套數(shù)據(jù)模型對(duì)的列式存儲(chǔ)系統(tǒng)Parquet,作為大數(shù)據(jù)系統(tǒng)中OLAP查詢的優(yōu)化方案,它已經(jīng)被多種查詢引擎原生支持,并且部分高性能引擎將其作為默認(rèn)的文件存儲(chǔ)格式。通過數(shù)據(jù)編碼和壓縮,以及映射下推和謂詞下推功能,Parquet的性能也較之其它文件格式有所提升,可以預(yù)見,隨著數(shù)據(jù)模型的豐富和Ad hoc查詢的需求,Parquet將會(huì)被更廣泛的使用。

  更多技術(shù)分享,請(qǐng)關(guān)注網(wǎng)易視頻云官方網(wǎng)站(http://vcloud.163.com/)或者網(wǎng)易視頻云官方微信(vcloud163)。

-- 展開閱讀全文 --