1. 程式人生 > 其它 >大資料開發Hive中 ORC 儲存格式分析

大資料開發Hive中 ORC 儲存格式分析

一、ORC File檔案結構

  1. ORC是列式儲存,有多種檔案壓縮方式,並且有著很高的壓縮比。
  2. 檔案是可切分(Split)的。因此,在Hive中使用ORC作為表的檔案儲存格式,不僅節省HDFS儲存資源,查詢任務的輸入資料量減少,使用的MapTask也就減少了。
  3. 提供了多種索引,row group index、bloom filter index。
  4. ORC可以支援複雜的資料結構(比如Map等)

列式儲存

由於OLAP查詢的特點,列式儲存可以提升其查詢效能,但是它是如何做到的呢?這就要從列式儲存的原理說​大資料培訓​起,從圖1中可以看到,相對於關係資料庫中通常使用的行式儲存,在使用列式儲存時每一列的所有元素都是順序儲存的。由此特點可以給查詢帶來如下的優化:

  • 查詢的時候不需要掃描全部的資料,而只需要讀取每次查詢涉及的列,這樣可以將I/O消耗降低N倍,另外可以儲存每一列的統計資訊(min、max、sum等),實現部分的謂詞下推。
  • 由於每一列的成員都是同構的,可以針對不同的資料型別使用更高效的資料壓縮演算法,進一步減小I/O。
  • 由於每一列的成員的同構性,可以使用更加適合CPU pipeline的編碼方式,減小CPU的快取失效。

需要注意的是,ORC在讀寫時候需要消耗額外的CPU資源來壓縮和解壓縮,當然這部分的CPU消耗是非常少的。

資料模型

和Parquet不同,ORC原生是不支援巢狀資料格式的,而是通過對複雜資料型別特殊處理的方式實現巢狀格式的支援,例如對於如下的hive表:

CREATE TABLE `orcStructTable`(
`name` string,
`course` struct<course:string,score:int>,
`score` map<string,int>,
`work_locations` array<string>)

在ORC的結構中包含了複雜型別列和原始型別,前者包括LIST、STRUCT、MAP和UNION型別,後者包括BOOLEAN、整數、浮點數、字串型別等,其中STRUCT的孩子節點包括它的成員變數,可能有多個孩子節點,MAP有兩個孩子節點,分別為key和value,LIST包含一個孩子節點,型別為該LIST的成員型別,UNION一般不怎麼用得到。每一個Schema樹的根節點為一個Struct型別,所有的column按照樹的中序遍歷順序編號。

ORC只需要儲存schema樹中葉子節點的值,而中間的非葉子節點只是做一層代理,它們只需要負責孩子節點值得讀取,只有真正的葉子節點才會讀取資料,然後交由父節點封裝成對應的資料結構返回。

檔案結構

和Parquet類似,ORC檔案也是以二進位制方式儲存的,所以是不可以直接讀取,ORC檔案也是自解析的,它包含許多的元資料,這些元資料都是同構ProtoBuffer進行序列化的。ORC的檔案結構如下圖,其中涉及到如下的概念:

  • ORC檔案:儲存在檔案系統上的普通二進位制檔案,一個ORC檔案中可以包含多個stripe,每一個stripe包含多條記錄,這些記錄按照列進行獨立儲存,對應到Parquet中的row group的概念。
  • 檔案級元資料:包括檔案的描述資訊PostScript、檔案meta資訊(包括整個檔案的統計資訊)、所有stripe的資訊和檔案schema資訊。
  • stripe:一組行形成一個stripe,每次讀取檔案是以行組為單位的,一般為HDFS的塊大小,儲存了每一列的索引和資料。
  • stripe元資料:儲存stripe的位置、每一個列的在該stripe的統計資訊以及所有的stream型別和位置。
  • row group:索引的最小單位,一個stripe中包含多個row group,預設為10000個值組成。
  • stream:一個stream表示檔案中一段有效的資料,包括索引和資料兩類。索引stream儲存每一個row group的位置和統計資訊,資料stream包括多種型別的資料,具體需要哪幾種是由該列型別和編碼方式決定。

在ORC檔案中儲存了三個層級的統計資訊,分別為檔案級別、stripe級別和row group級別的,他們都可以用來根據Search ARGuments(謂詞下推條件)判斷是否可以跳過某些資料,在統計資訊中都包含成員數和是否有null值,並且對於不同型別的資料設定一些特定的統計資訊。

(1)file level
在ORC檔案的末尾會記錄檔案級別的統計資訊,會記錄整個檔案中columns的統計資訊。這些資訊主要用於查詢的優化,也可以為一些簡單的聚合查詢比如max, min, sum輸出結果。

(2)stripe level
ORC檔案會儲存每個欄位stripe級別的統計資訊,ORC reader使用這些統計資訊來確定對於一個查詢語句來說,需要讀入哪些stripe中的記錄。比如說某個stripe的欄位max(a)=10,min(a)=3,那麼當where條件為a >10或者a <3時,那麼這個stripe中的所有記錄在查詢語句執行時不會被讀入。

(3)row level
為了進一步的避免讀入不必要的資料,在邏輯上將一個column的index以一個給定的值(預設為10000,可由引數配置)分割為多個index組。以10000條記錄為一個組,對資料進行統計。Hive查詢引擎會將where條件中的約束傳遞給ORC reader,這些reader根據組級別的統計資訊,過濾掉不必要的資料。如果該值設定的太小,就會儲存更多的統計資訊,使用者需要根據自己資料的特點權衡一個合理的值。

資料訪問

讀取ORC檔案是從尾部開始的,第一次讀取16KB的大小,儘可能的將Postscript和Footer資料都讀入記憶體。檔案的最後一個位元組儲存著PostScript的長度,它的長度不會超過256位元組,PostScript中儲存著整個檔案的元資料資訊,它包括檔案的壓縮格式、檔案內部每一個壓縮塊的最大長度(每次分配記憶體的大小)、Footer長度,以及一些版本資訊。在Postscript和Footer之間儲存著整個檔案的統計資訊(上圖中未畫出),這部分的統計資訊包括每一個stripe中每一列的資訊,主要統計成員數、最大值、最小值、是否有空值等。

接下來讀取檔案的Footer資訊,它包含了每一個stripe的長度和偏移量,該檔案的schema資訊(將schema樹按照schema中的編號儲存在陣列中)、整個檔案的統計資訊以及每一個row group的行數。

處理stripe時首先從Footer中獲取每一個stripe的其實位置和長度、每一個stripe的Footer資料(元資料,記錄了index和data的的長度),整個striper被分為index和data兩部分,stripe內部是按照row group進行分塊的(每一個row group中多少條記錄在檔案的Footer中儲存),row group內部按列儲存。每一個row group由多個stream儲存資料和索引資訊。每一個stream的資料會根據該列的型別使用特定的壓縮演算法儲存。在ORC中存在如下幾種stream型別:

  • PRESENT:每一個成員值在這個stream中保持一位(bit)用於標示該值是否為NULL,通過它可以只記錄部位NULL的值
  • DATA:該列的中屬於當前stripe的成員值。
  • LENGTH:每一個成員的長度,這個是針對string型別的列才有的。
  • DICTIONARY_DATA:對string型別資料編碼之後字典的內容。
  • SECONDARY:儲存Decimal、timestamp型別的小數或者納秒數等。
  • ROW_INDEX:儲存stripe中每一個row group的統計資訊和每一個row group起始位置資訊。

在初始化階段獲取全部的元資料之後,可以通過includes陣列指定需要讀取的列編號,它是一個boolean陣列,如果不指定則讀取全部的列,還可以通過傳遞SearchArgument引數指定過濾條件,根據元資料首先讀取每一個stripe中的index資訊,然後根據index中統計資訊以及SearchArgument引數確定需要讀取的row group編號,再根據includes資料決定需要從這些row group中讀取的列,通過這兩層的過濾需要讀取的資料只是整個stripe多個小段的區間,然後ORC會盡可能合併多個離散的區間儘可能的減少I/O次數。然後再根據index中儲存的下一個row group的位置資訊調至該stripe中第一個需要讀取的row group中。

ORC檔案格式只支援讀取指定欄位,還不支援只讀取特殊欄位型別中的指定部分。

使用ORC檔案格式時,使用者可以使用HDFS的每一個block儲存ORC檔案的一個stripe。對於一個ORC檔案來說,stripe的大小一般需要設定得比HDFS的block小,如果不這樣的話,一個stripe就會分別在HDFS的多個block上,當讀取這種資料時就會發生遠端讀資料的行為。如果設定stripe的只儲存在一個block上的話,如果當前block上的剩餘空間不足以儲存下一個strpie,ORC的writer接下來會將資料打散儲存在block剩餘的空間上,直到這個block存滿為止。這樣,下一個stripe又會從下一個block開始儲存。

由於ORC中使用了更加精確的索引資訊,使得在讀取資料時可以指定從任意一行開始讀取,更細粒度的統計資訊使得讀取ORC檔案跳過整個row group,ORC預設會對任何一塊資料和索引資訊使用ZLIB壓縮,因此ORC檔案佔用的儲存空間也更小,這點在後面的測試對比中也有所印證。

檔案壓縮

ORC檔案使用兩級壓縮機制,首先將一個數據流使用流式編碼器進行編碼,然後使用一個可選的壓縮器對資料流進行進一步壓縮。
一個column可能儲存在一個或多個數據流中,可以將資料流劃分為以下四種類型:
• Byte Stream
位元組流儲存一系列的位元組資料,不對資料進行編碼。

• Run Length Byte Stream
位元組長度位元組流儲存一系列的位元組資料,對於相同的位元組,儲存這個重複值以及該值在位元組流中出現的位置。

• Integer Stream
整形資料流儲存一系列整形資料。可以對資料量進行位元組長度編碼以及delta編碼。具體使用哪種編碼方式需要根據整形流中的子序列模式來確定。

• Bit Field Stream
位元流主要用來儲存boolean值組成的序列,一個位元組代表一個boolean值,在位元流的底層是用Run Length Byte Stream來實現的。

接下來會以Integer和String型別的欄位舉例來說明。

(1)Integer
對於一個整形欄位,會同時使用一個位元流和整形流。位元流用於標識某個值是否為null,整形流用於儲存該整形欄位非空記錄的整數值。

(2)String
對於一個String型別欄位,ORC writer在開始時會檢查該欄位值中不同的內容數佔非空記錄總數的百分比不超過0.8的話,就使用字典編碼,欄位值會儲存在一個位元流,一個位元組流及兩個整形流中。位元流也是用於標識null值的,位元組流用於儲存字典值,一個整形流用於儲存字典中每個詞條的長度,另一個整形流用於記錄欄位值。

如果不能用字典編碼,ORC writer會知道這個欄位的重複值太少,用字典編碼效率不高,ORC writer會使用一個位元組流儲存String欄位的值,然後用一個整形流來儲存每個欄位的位元組長度。

在ORC檔案中,在各種資料流的底層,使用者可以自選ZLIB, Snappy和LZO壓縮方式對資料流進行壓縮。編碼器一般會將一個數據流壓縮成一個個小的壓縮單元,在目前的實現中,壓縮單元的預設大小是256KB。

二、Hive+ORC建立資料倉庫

在建Hive表的時候我們就應該指定檔案的儲存格式。所以你可以在Hive QL語句裡面指定用ORCFile這種檔案格式,如下:

CREATE TABLE ... STORED AS ORC


ALTER TABLE ... [PARTITION partition_spec] SET FILEFORMAT ORC


SET hive.default.fileformat=Orc

所有關於ORCFile的引數都是在Hive QL語句的TBLPROPERTIES欄位裡面出現,他們是:

三、Java操作ORC

到https://orc.apache.org官網下載orc原始碼包,然後編譯獲取orc-core-1.3.0.jar、orc-mapreduce-1.3.0.jar、orc-tools-1.3.0.jar,將其加入專案中

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hive.ql.exec.vector.LongColumnVector;
import org.apache.hadoop.hive.ql.exec.vector.VectorizedRowBatch;
import org.apache.orc.CompressionKind;
import org.apache.orc.OrcFile;
import org.apache.orc.TypeDescription;
import org.apache.orc.Writer;


public class TestORCWriter {


public static void main(String[] args) throws Exception {
Path testFilePath = new Path("/tmp/test.orc");
Configuration conf = new Configuration();
TypeDescription schema = TypeDescription.fromString("struct<field1:int,field2:int,field3:int>");
Writer writer = OrcFile.createWriter(testFilePath, OrcFile.writerOptions(conf).setSchema(schema).compress(CompressionKind.SNAPPY));
VectorizedRowBatch batch = schema.createRowBatch();
LongColumnVector first = (LongColumnVector) batch.cols[0];
LongColumnVector second = (LongColumnVector) batch.cols[1];
LongColumnVector third = (LongColumnVector) batch.cols[2];


final int BATCH_SIZE = batch.getMaxSize();
// add 1500 rows to file
for (int r = 0; r < 15000000; ++r) {
int row = batch.size++;
first.vector[row] = r;
second.vector[row] = r * 3;
third.vector[row] = r * 6;
if (row == BATCH_SIZE - 1) {
writer.addRowBatch(batch);
batch.reset();
}
}
if (batch.size != 0) {
writer.addRowBatch(batch);
batch.reset();
}
writer.close();
}
}

大多情況下,還是建議在Hive中將文字檔案轉成ORC格式,這種用JAVA在本地生成ORC檔案,屬於特殊需求場景。