深度剖析 StarRocks 讀取 ORC 加密文件背后的技術
一、背景
為了提升對敏感數據的保護,需要對Hive表一些敏感數據進行加密存儲。
Spark組件已經通過引入了Apache ORC項目(Java版本)對ORC格式的Hive表的數據進行加解密。
StarRocks也使用了Apache ORC項目的C++版本讀寫ORC文件,但是C++版本沒有實現加解密功能,在使用StarRocks對Hive表進行即席分析時,無法對具有加密列的Hive表進行查詢,因此,需要對StarRocks 的Apache ORC模塊進行改造,使其支持對ORC格式的Hive加密表數據讀取功能,數據架構圖如下圖所示:
希望通過本文對ORC加密文件讀取功能的實現細節的剖析,讓讀者更加深刻理解ORC文件,同時了解StarRocks支持加解密數據分析的方案。
二、問題引入
在正式開啟全文的閱讀之前,我們首先引入幾個問題,然后帶著這些問題去閱讀后面的內容,將會更有針對性與啟發性,通過深入解答這些問題,我們不僅能夠更好地理解相關的概念和技術,還能提升分析和解決問題的能力。
問題如下:
- 程序解壓某個文件時,是否需要一次性讀取整個文件后再進行解壓操作?
- ORC 文件究竟是如何做到在不掃描全文件的情況下就能精準查詢到想要的數據?
- 當 SQL 查詢條件不符合最左前綴原則時,ORC文件中的索引是否就會失效?
- 數據加密、解密、解壓以及壓縮之間的關聯關系到底是怎樣的?
- 在寫ORC文件時為什么是先壓縮后加密,而不是先加密后解壓?
三、ORC文件介紹
ORC(Optimized Row Columnar)文件格式是一種高度優化的列式存儲格式,它主要用于Hadoop生態系統中的大數據處理和分析。ORC文件結構的設計旨在提高I/O效率、減少數據讀取時間,并支持復雜的數據類型和壓縮算法。
3.1 四層結構 File ,Stripe,Stream,Group
一個File中包含多個Stripe,一個Stripe包含多個Steam,一個Stream包含多個Group,每個Group默認存儲1萬行數據,如下圖所示:
3.2 三層索引
- FileStat :文件級別各列的統計信息,用于判斷SQL條件是否下推。
- StripeStat:Stripe級別各列的統計信息,用于判斷SQL條件是否下推。
- IndexData:每個Stripe 內部各列的索引信息,用于判斷SQL條件是否下推。
在讀取文件中數據之前,會先讀取以上3類索引數據,根據SQL條件逐層進行比對,來決定是否跳過某些數據的讀取,減少數據掃描量,從而提升SQL查詢效率。
下表是只包含id和name兩列的ORC文件的各層統計信息的案例:
FileStat
StripeStat
IndexData
3.3 ORC文件內部詳細結構
前面已經大體介紹了ORC文件的結構,下面詳細介紹其內部結構,ORC文件由多個邏輯層次組成,每個層次都有特定的作用和結構,下圖具體描述了包含2列(id,name)的ORC文件結構圖:
- Tail:存儲文件的元數據,如列的壓縮信息、統計信息、版本等,包含了三個部分:PostScript、Footer、MetaData。
- Body:實際存儲數據的部分,由多個Stripe組成。
下面分別介紹Tail和Body內部包含哪些結構:
Tail文件尾部是讀取ORC文件的起點,它包含了文件關鍵信息:
- PostScript:存儲文件的壓縮類型、壓縮塊大小、版本信息,Footer和MetaData的長度等,這部分數據不會被壓縮。
- Footer:記錄了整個文件所有列的統計信息(FileStat),所有Stripe的元數據信息(stripesList),加密信息(encryption)以及文件body長度。
- MetaData:存儲該文件所有Stripe的統計信息(StripeStat)。
Body實際存儲數據的部分,由多個Stripe組成,每個Stripe包含多個Stream,先存儲索引相關的Stream(index-Stream),后面存儲實際數據相關的Stream(data-Stream),每一列包含多個index-Stream和data-Stream,Stripe是ORC文件中數據存儲的基本單元,每個Stripe數據大小一般不超過200M,主要包含下面幾塊內容:
- Stripe Footer:包含所有Stream的元數據(streamsList)和加密信息(encryption)等。
- Index-Stream:存儲索引相關數據的Stream,按列存儲。
- Data-Stream:儲實際數據相關的Stream,按列存儲。
ORC文件的讀取是從尾部最后一個字節開始的,得到PostScript的長度,讀取PostScript,然后根據PostScript中的FooteLength,MetaDataLength信息讀取MetaData和Footer,最后根據Footer中的Stripe信息讀取具體的數據Stripe,上面的文字介紹可能不是很直觀,如果想更細節了解ORC文件結構內容可以參考(ORC文件結構思維導圖,ORC文件官網介紹)。
四、相關概念的理解
4.1 對稱加解密
對稱加解密的要素包括密鑰、明文、密文和加密算法。以下是對這些要素關系的描述:
- 密鑰:密鑰是加密和解密過程中的關鍵元素,它是由隨機數生成的,通常是固定長度的一串二進制數。
- 明文:明文是指原始的信息,可以是文本、圖片、音頻等各種形式的數據。
- 密文:密文是經過加密算法處理后的數據,只有知道密鑰的人才能解密還原成明文。
- 加密算法:加密算法是將明文轉換成密文的過程,這個過程通常涉及到一系列的數學運算,比如AEC,RSA等。
注意:對稱加密的加密密鑰 和 解密密鑰是一樣的。
4.2 文件的壓縮和解壓縮
4.2.1 壓縮算法
壓縮算法是用于減小文件大小的數學方法。它通過各種技術,如替換、重新編碼、差分編碼、運行長度編碼、字典編碼、變換編碼等,來減少數據的冗余和實現數據的體積縮小。壓縮算法可以是無損的或有損的:
- 無損壓縮:意味著原始數據可以完全從壓縮文件中恢復,常用于文本和某些類型的數據文件。
- 有損壓縮:為了獲得更高的壓縮率,允許丟失一些數據,常用于圖像、音頻和視頻文件。
4.2.2 解壓算法
解壓算法是壓縮算法的逆過程,它用于將壓縮文件恢復到其原始狀態。無損壓縮的解壓算法能夠完全恢復原始數據,而有損壓縮的解壓算法則可能無法完全恢復所有原始數據。
文件壓縮和解壓縮簡單流程圖如下:
注意:數據的壓縮算法和解壓算法要一樣
4.2.3 壓縮塊
文件壓縮塊是指對文件進行壓縮處理后生成的一組連續的數據塊。在文件壓縮過程中,文件被分割成多個塊,每個塊都經過壓縮算法處理。一般來說,文件壓縮塊的大小可配置。例如,ZIP壓縮的每個壓縮塊的大小可以達到64KB或更大,而在其他壓縮格式如7z中,壓縮塊的大小可以更大,通常為數MB。這些大小可以根據文件的特性和壓縮算法的性能進行調整,以達到更好的壓縮比和解壓性能。
注意:在解壓文件的過程中會從文件中讀取整個壓縮塊數據到內存之后再使用解壓算法進行解壓處理,所以壓縮塊越大每次解壓讀取到內存里的數據會越大。
4.3 加密壓縮文件讀寫大致流程
在掌握了數據加密和壓縮的基礎知識之后,讓我們從宏觀的角度了解一下ORC加密文件讀寫流程,如下圖所示:在寫入時,內存中的數據首先被序列化,然后壓縮以減少體積,最后對數據加密。在讀取時,數據首先被解密以恢復原始格式,然后解壓數據得到原始數據,最后通過反序列化原始數據轉換為內存對象。
詳細說明寫入和讀取過程中的各個步驟:
(1)寫入過程(序列化、壓縮、加密)
- 序列化:在數據寫入存儲系統之前,首先需要將內存中的對象轉換成可以存儲或傳輸的格式,這個過程稱為序列化。序列化后的數據通常是一個二進制格式,便于后續的處理。
- 壓縮:序列化后的數據可能會占用較大的空間。為了減少存儲需求和提升后續數據加密處理效率,接下來對數據進行壓縮。壓縮算法會嘗試去除數據中的冗余,從而減少數據的體積。
- 加密:壓縮后的數據需要進行加密,以確保數據的安全性。加密算法會使用密鑰對數據進行加密,生成密文。
- 存入文件中:加密后的密文被存儲在文件中,等待后續的讀取或傳輸。
(2)讀取過程(解密、解壓、反序列化)
- 解密:當需要讀取文件中的數據時,首先需要使用正確的密鑰和加密算法對密文進行解密,恢復為壓縮前的數據。
- 解壓:解密后,應用解壓算法對數據進行解壓,恢復到序列化前的狀態。
- 反序列化:解壓后的數據是一個二進制格式,需要進行反序列化,將其轉換為內存中的對象。反序列化是序列化的逆過程,它將二進制數據轉換為可讀可操作的數據結構。
- 內存對象:經過解密、解壓和反序列化之后,數據最終以內存對象的形式被程序處理。
五、StarRocks 讀取 ORC 加密文件實現方案
5.1 ORC文件內部數據加密關系
首先,介紹幾個密鑰的含義:
statKey:用于解密加密列的FileStat,StripeStat的密鑰,每個列一個,加密存儲在文件Footer里。
dataKey:用于解密加密列的IndexData 和 RowData,每個Stripe的每一列都有一個,加密存儲在Stripe的Footer里。
masterKey:文件的根密鑰,用于解密ORC文件中被加密的statKey 和 dataKey,該密鑰沒有存儲在文件中,一般存儲在Hive表屬性上。要解密ORC文件中的數據,首先需要獲取這個masterKey。然而,masterKey本身也是加密的,因此在讀取Hive表之前,必須先從表屬性中提取出加密的masterKey,訪問密鑰管理服務(Key Management Service, KMS),對加密的masterKey進行解密,從而獲得可用于實際解密操作的明文masterKey密鑰,一旦獲得了masterKey的明文形式,就可以用它來解密ORC文件中的dataKey和statKey。
下圖是描述了masterKey、statKey,dataKey之間的關系,灰色部分代表是存儲在文件中被加密的數據,綠色部分則是解密之后的數據,包括我們解密后的statKey,dataKey。獲得這兩個密鑰之后分別用于解密統計信息和文件中的真實數據。
5.2 StarRocks讀取ORC加密文件流程
在深入掌握了ORC文件中密鑰的相互關系和功能后,我們現在轉向探討StarRocks是如何讀取ORC加密表的數據。這個過程如下圖所示:
1)提交SQL查詢:用戶首先通過SQL客戶端向StarRocks FE節點提交查詢請求。這通常涉及到對Hive表下存儲的ORC加密文件進行讀取操作。
2)獲取解密的masterKey:查詢提交后,系統首要根據SQL獲取Hive表中的ORC文件所需的masterKey。這個masterKey一般存儲在表屬性里,并且是加密存儲的,必須調用KMS服務來解密,得到密鑰明文。
3)傳遞masterKey明文:解密后的masterKey,以明文形式傳遞給StarRocks BE節點。
4)讀取并解密密鑰:BE 拿到已解密的masterKey 之后 讀取并解密ORC文件中的statKey和dataKey,這兩個密鑰分別用于解密統計信息(FileStat,StripeStat)和實際數據內容,為接下來的統計信息和數據解密做準備。
5)使用statKey和dataKey解密數據:BE使用statKey來解密文件的統計信息(fileStat和StripeStat)同時使用dataKey來解密實際的數據內容。
5.3 讀取ORC加密文件的關鍵實現細節
通過了解前文StarRocks讀取ORC加密文件流程,我們將深入探討讀取ORC加密文件的數據關鍵實現細節。首先,我們提出一個問題:在物理存儲中,文件存儲的是什么內容?答案是二進制數據。這些二進制數據通常會經過壓縮處理。
ORC文件的讀取流程是自外向內的,類似于剝洋蔥的過程,逐步深入到我們需要讀取的目標數據。讀取流程可以概括為:首先讀取文件元數據,通過元數據獲取目標數據的偏移量(offset)和數據長度如下圖所示,然后通過流的方式讀取目標數據。
具體到ORC加密文件的讀取實現代碼,主要采用了設計模式中的裝飾模式方式來組織代碼的。在這個模式中,原始的文件流(SeekableFileInputStream)首先被解密流(DecryptionInputStream)所包裝,如果是非加密文件就沒有這一層,然后解密流又被解壓縮流(DecompressionStream)所包裝。每一層流都只負責向其包裝的流請求數據,并在接收到一定量數據后開始處理自己的邏輯。如下圖所示:
這種分層的方法保證每一層都專注于自己的職責,共同協作完成ORC文件的讀取任務。通過這種方式,我們不僅能夠高效地讀取ORC文件,還能確保數據的安全性和完整性。綜上所述,ORC文件的讀取流程是一個從文件元數據到具體數據內容的逐步深入過程。
5.4 加密字段跳讀機制
為了提升數據的查詢效率,查詢數據時會根據索引數據跳過不必要的數據讀取,下面我們介紹加密列跳讀機制,理解了這部分的內容,就能非常清晰的知道,讀取加密字段時,對數據解密與解壓是怎樣協作的。
5.4.1 加密塊與壓縮塊的關系
加密列的數據劃分了多個加密塊與壓縮塊,一個壓縮塊 包含多個加密塊,讀取數據時,先對每個加密塊進行解密,解密多個加密塊之后,把這些解密后的數據塊合并成一個完整的壓縮塊,然后對這個壓縮塊進行解壓得到原始數據下圖是加密塊與壓縮塊的關系圖:
5.4.2 ORC文件使用的加解密算法和模式
下圖描述了具體的數據加解密過程中以及設計到整個過程中各種元素輸入輸出的關系:
注意:同一個數據塊(16字節)加密過程和解密過程中的 密鑰、IV值、加密算法和加密模式必須相同。
明文塊:我們對ORC文件加密使用的加密算法是AES-128-CTR/NoPadding,該算法加密數據時 ,會把明文按照16個字節劃分多個塊,每個塊加密之后得到的數據就是加密塊。
加密塊:每個明文數據塊加密之后得到的數據就是加密塊。
初始向量IV:初始向量IV的作用是使加密更加安全可靠(加鹽),我們使用AES加密時需要主動提供這個初始向量IV,而且只需要提供一個初始向量就夠了,后面每個數據塊的加密向量由加密模式決定,所以每個數據塊的加密向量都不一樣。初始向量IV的長度規定為128位16個字節,ORC文件解密參數 IV的描述如下:總共16個字節,前面8個字節分別存儲:列ID,Stream類型,Stripe的ID ,后面8個字節用于填充min_count,由于我們使用的是CTR加密模式,所以這個min_count就是加密塊在整個加密數據中的計數,iv各個內容長度定義如下圖:
密鑰:AES要求密鑰的長度可以是128位16個字節、192位或者256位,位數越高,加密強度自然越大,但是加密的效率自然會低一些,因此要做好權衡。我們開發通常采用128位16個字節的密鑰,我們使用AES加密時需要主動提供密鑰,而且只需要提供一個密鑰就夠了,每個數據塊加解密使用的都是同一個密鑰。
加密模式:有5種加密模式,這些加密模式的主要目的是為了不讓重復的明文加密之后得到的密文一樣,提升數據安全性,我們使用的是CTR模式(計數器模式)對數據加密,那解密的時候也需要CTR模式對數據解密,計數器模式介紹請參考鏈接,CTR模式 的iv參數 包含了 加密塊計數(min_count),所以 每次對一個加密塊解密時 需要知道 當前加密塊的初始計數值。
5.4.3 舉例說明跳讀流程
學習了前面讀取加密數據的關鍵細節之后,舉個例子說明跳讀ORC文件流程,假設根據索引數據和查詢條件確定需要讀取某個文件中第1個Strip中第1列的第5個group的數據,那么我們知道group5數據的偏移量offset,文件結構如下圖所示:
具體邏輯大體流程如下:
注意:解壓數據塊時,必須把當前解壓塊的所有數據讀出來才能使用對應的解壓算法解壓數據。
1)group5數據的偏移量group_offset計算出 group5數據在哪個壓縮塊里,計算公式為:block_index = group_offset/zipBlockSize(壓縮塊大小),并得到該壓縮塊的起始位置zip_head_offset 公式為:zip_head_offset = block_index*zipBlockSize。
2)獲取zip_head_offset位置對應的加密塊計數,加密塊計數值計算公式為:min_count = zip_head_offset/encrypted-size(加密塊大小) 更新iv向量的min_count值。
3)文件讀指針定位到zip_head_offset,開始讀取壓縮塊的數據,這個壓縮塊的數據全部讀出之后,使用解壓算法進行解壓。
4)通過group5在解壓的數據上偏移量和長度,讀取group5 數據,然后再對數據進行解碼。
六、問題解答
通過前面對相關內容的講解,下面我們來解答前文提出的問題:
1)文件解壓是否意味著一定是對整個文件進行解壓操作?
答:不需要,文件是按照一定大小劃分出若干個壓縮塊,只要讀出相應的壓縮塊進行解壓就行。
2)ORC 文件究竟是如何做到在不掃描全文件的情況下就能精準查詢到想要的數據?
答:ORC文件有三層索引,在讀取文件數據之前先讀取各層級的索引信息,根據過濾條件過濾掉不必要的數據掃描,從而提升數據查詢效率。
3)當 SQL 查詢條件不符合最左前綴原則時,其索引效果是否就會失效呢?
答:不會失效,ORC文件是列式存儲的,各列信息都是相互獨立的,有自己的索引信息,與行式數據庫的索引最左前綴規則不同。
4)數據加密、解密、解壓以及壓縮之間的關聯關系到底是怎樣的?
答:請參考本文:5.1 ORC文件內部數據加密關系 內容。
5)在寫加密列數據時,為什么不是先加密數據再壓縮,而是先壓縮后加密?
答:主要是為了提升加密效率,數據被壓縮處理之后,數據量變少了,加密效率就提升了。
七、總結
本文介紹了StarRocks數據庫如何讀取ORC文件的加密數據,包括相關概念理解、ORC文件介紹、以及StarRocks讀取加密ORC文件的具體實現方案。闡述了出于數據安全的需要,對Hive表中的敏感數據進行加密存儲的必要性,介紹了對稱加密、文件壓縮與解壓、加密壓縮文件讀寫流程等概念,深入探討了ORC文件的三層結構和索引機制,以及如何利用這些特性實現高效查詢加密數據。還詳細描述了StarRocks讀取加密ORC文件的流程,包括獲取解密的masterKey、使用masterKey解密ORC文件中的密鑰、以及使用這些密鑰解密數據。
希望通過本文對ORC加密文件讀取功能的實現細節,讓讀者對ORC文件的理解更深刻。最后如果想從代碼層面了解ORC文件解密過程可以參考開源PR。