http://blog.csdn.net/yu616568/article/details/51868447背景隨著大數(shù)據(jù)時代的到來,越來越多的數(shù)據(jù)流向了Hadoop生態(tài)圈币旧,同時對于能夠快速的從TB甚至PB級別的數(shù)據(jù)中獲取有價值的數(shù)據(jù)對于一個產(chǎn)品和公司來說更加重要融涣,在hadoop生態(tài)圈的快速發(fā)展過程中柬讨,涌現(xiàn)了一批開源的數(shù)據(jù)分析引擎,例如Hive伪货、Spark SQL们衙、Impala、Presto等碱呼,同時也產(chǎn)生了多個高性能的列式存儲格式蒙挑,例如RCFile、ORC愚臀、Parquet等忆蚀,本文主要從實現(xiàn)的角度上對比分析ORC和Parquet兩種典型的列存格式,并對它們做了相應的對比測試姑裂。列式存儲由于OLAP查詢的特點馋袜,列式存儲可以提升其查詢性能,但是它是如何做到的呢舶斧?這就要從列式存儲的原理說起欣鳖,從圖1中可以看到,相對于關系數(shù)據(jù)庫中通常使用的行式存儲茴厉,在使用列式存儲時每一列的所有元素都是順序存儲的泽台。由此特點可以給查詢帶來如下的優(yōu)化:查詢的時候不需要掃描全部的數(shù)據(jù),而只需要讀取每次查詢涉及的列矾缓,這樣可以將I/O消耗降低N倍怀酷,另外可以保存每一列的統(tǒng)計信息(min、max嗜闻、sum等)蜕依,實現(xiàn)部分的謂詞下推。由于每一列的成員都是同構的,可以針對不同的數(shù)據(jù)類型使用更高效的數(shù)據(jù)壓縮算法笔横,進一步減小I/O竞滓。由于每一列的成員的同構性咐吼,可以使用更加適合CPU pipeline的編碼方式吹缔,減小CPU的緩存失效。
orcStructTable
( name
string, course
struct, score
map, work_locations
array)
ORC格式會將其轉換成如下的樹狀結構:
圖5 ORC的schema結構
在ORC的結構中這個schema包含10個column浙宜,其中包含了復雜類型列和原始類型的列,前者包括LIST蛹磺、STRUCT粟瞬、MAP和UNION類型,后者包括BOOLEAN萤捆、整數(shù)裙品、浮點數(shù)、字符串類型等俗或,其中STRUCT的孩子節(jié)點包括它的成員變量市怎,可能有多個孩子節(jié)點,MAP有兩個孩子節(jié)點辛慰,分別為key和value区匠,LIST包含一個孩子節(jié)點,類型為該LIST的成員類型帅腌,UNION一般不怎么用得到驰弄。每一個Schema樹的根節(jié)點為一個Struct類型,所有的column按照樹的中序遍歷順序編號速客。
ORC只需要存儲schema樹中葉子節(jié)點的值戚篙,而中間的非葉子節(jié)點只是做一層代理,它們只需要負責孩子節(jié)點值得讀取溺职,只有真正的葉子節(jié)點才會讀取數(shù)據(jù)岔擂,然后交由父節(jié)點封裝成對應的數(shù)據(jù)結構返回位喂。
文件結構
和Parquet類似,ORC文件也是以二進制方式存儲的乱灵,所以是不可以直接讀取塑崖,ORC文件也是自解析的,它包含許多的元數(shù)據(jù)阔蛉,這些元數(shù)據(jù)都是同構ProtoBuffer進行序列化的弃舒。ORC的文件結構入圖6,其中涉及到如下的概念:
ORC文件:保存在文件系統(tǒng)上的普通二進制文件状原,一個ORC文件中可以包含多個stripe聋呢,每一個stripe包含多條記錄,這些記錄按照列進行獨立存儲颠区,對應到Parquet中的row group的概念削锰。
文件級元數(shù)據(jù):包括文件的描述信息PostScript、文件meta信息(包括整個文件的統(tǒng)計信息)毕莱、所有stripe的信息和文件schema信息器贩。
stripe:一組行形成一個stripe,每次讀取文件是以行組為單位的朋截,一般為HDFS的塊大小蛹稍,保存了每一列的索引和數(shù)據(jù)。
stripe元數(shù)據(jù):保存stripe的位置部服、每一個列的在該stripe的統(tǒng)計信息以及所有的stream類型和位置唆姐。
row group:索引的最小單位,一個stripe中包含多個row group廓八,默認為10000個值組成奉芦。
stream:一個stream表示文件中一段有效的數(shù)據(jù),包括索引和數(shù)據(jù)兩類剧蹂。索引stream保存每一個row group的位置和統(tǒng)計信息声功,數(shù)據(jù)stream包括多種類型的數(shù)據(jù),具體需要哪幾種是由該列類型和編碼方式?jīng)Q定宠叼。
圖6 ORC文件結構
在ORC文件中保存了三個層級的統(tǒng)計信息先巴,分別為文件級別、stripe級別和row group級別的车吹,他們都可以用來根據(jù)Search ARGuments(謂詞下推條件)判斷是否可以跳過某些數(shù)據(jù)筹裕,在統(tǒng)計信息中都包含成員數(shù)和是否有null值,并且對于不同類型的數(shù)據(jù)設置一些特定的統(tǒng)計信息窄驹。
數(shù)據(jù)訪問
讀取ORC文件是從尾部開始的朝卒,第一次讀取16KB的大小,盡可能的將Postscript和Footer數(shù)據(jù)都讀入內(nèi)存乐埠。文件的最后一個字節(jié)保存著PostScript的長度抗斤,它的長度不會超過256字節(jié)囚企,PostScript中保存著整個文件的元數(shù)據(jù)信息,它包括文件的壓縮格式瑞眼、文件內(nèi)部每一個壓縮塊的最大長度(每次分配內(nèi)存的大小)龙宏、Footer長度,以及一些版本信息伤疙。在Postscript和Footer之間存儲著整個文件的統(tǒng)計信息(上圖中未畫出)银酗,這部分的統(tǒng)計信息包括每一個stripe中每一列的信息,主要統(tǒng)計成員數(shù)徒像、最大值黍特、最小值、是否有空值等锯蛀。
接下來讀取文件的Footer信息灭衷,它包含了每一個stripe的長度和偏移量,該文件的schema信息(將schema樹按照schema中的編號保存在數(shù)組中)旁涤、整個文件的統(tǒng)計信息以及每一個row group的行數(shù)翔曲。
處理stripe時首先從Footer中獲取每一個stripe的其實位置和長度、每一個stripe的Footer數(shù)據(jù)(元數(shù)據(jù)劈愚,記錄了index和data的的長度)瞳遍,整個striper被分為index和data兩部分,stripe內(nèi)部是按照row group進行分塊的(每一個row group中多少條記錄在文件的Footer中存儲)菌羽,row group內(nèi)部按列存儲傅蹂。每一個row group由多個stream保存數(shù)據(jù)和索引信息。每一個stream的數(shù)據(jù)會根據(jù)該列的類型使用特定的壓縮算法保存算凿。在ORC中存在如下幾種stream類型:
PRESENT:每一個成員值在這個stream中保持一位(bit)用于標示該值是否為NULL,通過它可以只記錄部位NULL的值
DATA:該列的中屬于當前stripe的成員值犁功。
LENGTH:每一個成員的長度氓轰,這個是針對string類型的列才有的。
DICTIONARY_DATA:對string類型數(shù)據(jù)編碼之后字典的內(nèi)容浸卦。
SECONDARY:存儲Decimal署鸡、timestamp類型的小數(shù)或者納秒數(shù)等。
ROW_INDEX:保存stripe中每一個row group的統(tǒng)計信息和每一個row group起始位置信息限嫌。
在初始化階段獲取全部的元數(shù)據(jù)之后靴庆,可以通過includes數(shù)組指定需要讀取的列編號,它是一個boolean數(shù)組怒医,如果不指定則讀取全部的列炉抒,還可以通過傳遞SearchArgument參數(shù)指定過濾條件,根據(jù)元數(shù)據(jù)首先讀取每一個stripe中的index信息稚叹,然后根據(jù)index中統(tǒng)計信息以及SearchArgument參數(shù)確定需要讀取的row group編號焰薄,再根據(jù)includes數(shù)據(jù)決定需要從這些row group中讀取的列拿诸,通過這兩層的過濾需要讀取的數(shù)據(jù)只是整個stripe多個小段的區(qū)間,然后ORC會盡可能合并多個離散的區(qū)間盡可能的減少I/O次數(shù)塞茅。然后再根據(jù)index中保存的下一個row group的位置信息調(diào)至該stripe中第一個需要讀取的row group中亩码。
由于ORC中使用了更加精確的索引信息,使得在讀取數(shù)據(jù)時可以指定從任意一行開始讀取野瘦,更細粒度的統(tǒng)計信息使得讀取ORC文件跳過整個row group描沟,ORC默認會對任何一塊數(shù)據(jù)和索引信息使用ZLIB壓縮,因此ORC文件占用的存儲空間也更小鞭光,這點在后面的測試對比中也有所印證吏廉。
在新版本的ORC中也加入了對Bloom Filter的支持,它可以進一步提升謂詞下推的效率衰猛,在Hive 1.2.0版本以后也加入了對此的支持迟蜜。
性能測試
為了對比測試兩種存儲格式,我選擇使用TPC-DS數(shù)據(jù)集并且對它進行改造以生成寬表啡省、嵌套和多層嵌套的數(shù)據(jù)娜睛。使用最常用的Hive作為SQL引擎進行測試。
測試環(huán)境
Hadoop集群:物理測試集群卦睹,四臺DataNode/NodeManager機器畦戒,每個機器32core+128GB,測試時使用整個集群的資源结序。
Hive:Hive 1.2.1版本障斋,使用hiveserver2啟動,本機MySql作為元數(shù)據(jù)庫,jdbc方式提交查詢SQL
數(shù)據(jù)集:100GB TPC-DS數(shù)據(jù)集靶衍,選取其中的Store_Sales為事實表的模型作為測試數(shù)據(jù)
查詢SQL:選擇TPC-DS中涉及到上述模型的10條SQL并對其進行改造坯门。
測試場景和結果
整個測試設置了四種場景,每一種場景下對比測試數(shù)據(jù)占用的存儲空間的大小和相同查詢執(zhí)行消耗的時間對比遂庄,除了場景一基于原始的TPC-DS數(shù)據(jù)集外,其余的數(shù)據(jù)都需要進行數(shù)據(jù)導入劲赠,同時對比這幾個場景的數(shù)據(jù)導入時間涛目。
場景一:一個事實表、多個維度表凛澎,復雜的join查詢霹肝。
基于原始的TPC-DS數(shù)據(jù)集。
Store_Sales表記錄數(shù):287,997,024塑煎,表大小為:
原始Text格式沫换,未壓縮 : 38.1 G
ORC格式,默認壓縮(ZLIB),一共1800+個分區(qū) : 11.5 G
Parquet格式轧叽,默認壓縮(Snappy)苗沧,一共1800+個分區(qū) : 14.8 G
查詢測試結果:
場景二:維度表和事實表join之后生成的寬表刊棕,只在一個表上做查詢。
整個測試設置了四種場景待逞,每一種場景下對比測試數(shù)據(jù)占用的存儲空間的大小和相同查詢執(zhí)行消耗的時間對比甥角,除了場景一基于原始的TPC-DS數(shù)據(jù)集外,其余的數(shù)據(jù)都需要進行數(shù)據(jù)導入识樱,同時對比這幾個場景的數(shù)據(jù)導入時間嗤无。選取數(shù)據(jù)模型中的store_sales, household_demographics, customer_address, date_dim, store表生成一個扁平式寬表(store_sales_wide_table),基于這個表執(zhí)行查詢怜庸,由于場景一種選擇的query大多數(shù)不能完全match到這個寬表当犯,所以對場景1中的SQL進行部分改造。
store_sales_wide_table表記錄數(shù):263,704,266割疾,表大小為:
原始Text格式嚎卫,未壓縮 : 149.0 G
ORC格式,默認壓縮 : 10.6 G
PARQUET格式宏榕,默認壓縮 : 12.5 G
查詢測試結果:
場景三:復雜的數(shù)據(jù)結構組成的寬表拓诸,struct、list麻昼、map等(1層)
整個測試設置了四種場景奠支,每一種場景下對比測試數(shù)據(jù)占用的存儲空間的大小和相同查詢執(zhí)行消耗的時間對比,除了場景一基于原始的TPC-DS數(shù)據(jù)集外抚芦,其余的數(shù)據(jù)都需要進行數(shù)據(jù)導入倍谜,同時對比這幾個場景的數(shù)據(jù)導入時間。在場景二的基礎上叉抡,將維度表(除了store_sales表)轉換成一個struct或者map對象尔崔,源store_sales表中的字段保持不變。生成有一層嵌套的新表(store_sales_wide_table_one_nested)褥民,使用的查詢邏輯相同您旁。
store_sales_wide_table_one_nested表記錄數(shù):263,704,266,表大小為:
原始Text格式轴捎,未壓縮 : 245.3 G
ORC格式,默認壓縮 : 10.9 G 比store_sales表還胁显唷侦副?
PARQUET格式,默認壓縮 : 29.8 G
查詢測試結果:
場景四:復雜的數(shù)據(jù)結構驼鞭,多層嵌套秦驯。(3層)
整個測試設置了四種場景,每一種場景下對比測試數(shù)據(jù)占用的存儲空間的大小和相同查詢執(zhí)行消耗的時間對比挣棕,除了場景一基于原始的TPC-DS數(shù)據(jù)集外译隘,其余的數(shù)據(jù)都需要進行數(shù)據(jù)導入亲桥,同時對比這幾個場景的數(shù)據(jù)導入時間。在場景三的基礎上固耘,將部分維度表的struct內(nèi)的字段再轉換成struct或者map對象题篷,只存在struct中嵌套map的情況,最深的嵌套為三層厅目。生成一個多層嵌套的新表(store_sales_wide_table_more_nested)番枚,使用的查詢邏輯相同。
該場景中只涉及一個多層嵌套的寬表损敷,沒有任何分區(qū)字段葫笼,store_sales_wide_table_more_nested表記錄數(shù):263,704,266,表大小為:
原始Text格式拗馒,未壓縮 : 222.7 G
ORC格式路星,默認壓縮 : 10.9 G 比store_sales表還小诱桂?
PARQUET格式洋丐,默認壓縮 : 23.1 G 比一層嵌套表store_sales_wide_table_one_nested要小访诱?
查詢測試結果:
結果分析
從上述測試結果來看垫挨,星狀模型對于數(shù)據(jù)分析場景并不是很合適,多個表的join會大大拖慢查詢速度触菜,并且不能很好的利用列式存儲帶來的性能提升九榔,在使用寬表的情況下,列式存儲的性能提升明顯涡相,ORC文件格式在存儲空間上要遠優(yōu)于Text格式哲泊,較之于PARQUET格式有一倍的存儲空間提升,在導數(shù)據(jù)(insert into table select 這樣的方式)方面ORC格式也要優(yōu)于PARQUET催蝗,在最終的查詢性能上可以看到切威,無論是無嵌套的扁平式寬表,或是一層嵌套表丙号,還是多層嵌套的寬表先朦,兩者的查詢性能相差不多,較之于Text格式有2到3倍左右的提升犬缨。
另外喳魏,通過對比場景二和場景三的測試結果,可以發(fā)現(xiàn)扁平式的表結構要比嵌套式結構的查詢性能有所提升怀薛,所以如果選擇使用大寬表刺彩,則設計寬表的時候盡可能的將表設計的扁平化,減少嵌套數(shù)據(jù)。
通過這三種文件存儲格式的測試對比创倔,ORC文件存儲格式無論是在空間存儲嗡害、導數(shù)據(jù)速度還是查詢速度上表現(xiàn)的都較好一些,并且ORC可以一定程度上支持ACID操作畦攘,社區(qū)的發(fā)展目前也是Hive中比較提倡使用的一種列式存儲格式霸妹,另外,本次測試主要針對的是Hive引擎念搬,所以不排除存在Hive與ORC的敏感度比PARQUET要高的可能性抑堡。
總結
本文主要從數(shù)據(jù)模型、文件格式和數(shù)據(jù)訪問流程等幾個方面詳細介紹了Hadoop生態(tài)圈中的兩種列式存儲格式——Parquet和ORC朗徊,并通過大數(shù)據(jù)量的測試對兩者的存儲和查詢性能進行了對比首妖。對于大數(shù)據(jù)場景下的數(shù)據(jù)分析需求,使用這兩種存儲格式總會帶來存儲和性能上的提升爷恳,但是在實際使用時還需要針對實際的數(shù)據(jù)進行選擇有缆。另外由于不同開源產(chǎn)品可能對不同的存儲格式有特定的優(yōu)化,所以選擇時還需要考慮查詢引擎的因素温亲。