数据湖常用查询优化技术
經濟發展有周期,人的思想活動也是有周期的,是時候進行一場文化領域的整風運動了,尤其是那些空談誤國,亂教誤人子弟的,就是缺少了對其思想改造的過程,嚴重脫離群眾,是時候要常態化地下放了。
本文首發微信公眾號:碼上觀世界
1
MinMax
開放式數據格式文件的的元數據信息部分通常都包含當前文件每個列的最大、最小值,比如下圖中的parquet文件包含兩個字段:year和uid,并且
file1.parquet中列year的最大和最小值分別是2019和2018,列uid的最大和最小值分別是23000和12000,file2.parquet中列year的最大和最小值分別是2020和2018,列uid的最大和最小值分別是14000和12000,file3.parquet中列year的最大和最小值分別是2020和2020,列uid的最大和最小值分別是25000和23000:
當我們進行查詢
select * from event where year=2019 and uid=20000因為這些元數據信息在數據寫入文件時最終收集,因此在查詢時候,很容易利用這些統計信息過濾掉不符合條件的數據文件。示例中,根據year=2020和uid=20000查詢到符合條件的數據文件為file1.parquet,另外兩個數據文件直接過濾掉了,減少了不必須的文件讀取。
為了獲取更好的過濾效果,MinMax通常進行全局排序,但是適合排序字段較少的情況,比如1個字段,當排序字段多于1個,依據索引的最左匹配規則,只有查詢字段覆蓋所有所有索引字段才能獲得最好的查詢效果,否則過濾效果將大打折扣,為此需要更合適的索引,比如Z-Order。
2
Z-Order
因為MinMax索引包括多個字段時,不能保證數據的聚集性,而利用Z-Order索引能夠獲得比MinMax平均更好的數據聚集性。Z-Order原理是把天然沒有有序性的多維數據以某種方式映射成一維數據進行比較。映射后的一維數據,能夠保證各個原始維度按照同種程度去保證其聚集性。如下圖所示:
對X,Y這兩個維度進行比特位的交叉組值,形成了Interleave Index進而得出一個新的值,這個值被稱作Z-Value。從圖中,可以看到針對X,Y這兩個字段的數據,生成的z-value會呈現出一個Z形嵌套。按照這樣的一個結構,在按照Z-Value排序時,能夠同時保證X,Y兩個字段的聚集性。
實現Z-Order 的一個前提是需要保證數據以保序的方式映射成一個正整型,但參與排序的字段類型很多,如String、Long、DateTime、Double等,如何將這些不同類型的值映射為正整型就是個問題。實踐中,雖然可以將String類型取固定的前幾位字符轉為二進制來進行映射,但也帶來了信息損失。另外,即使是正整型數據,由于其數據分布不同,可能導致映射的結果不符合Z-Order曲線的嵌套分布。比如,X的取值是0,1,2,3,4,5,6,7,Y的取值是8,16,24,32這種,計算出來的z-value排序效果實際上和數據按照order by y,x的效果是一樣的。也就是說這種排序并沒有帶來額外的好處,對于X的聚集性無法保證。
為了獲取更好的過濾效果,Z-Order也需要進行全局排序,但是Z-Order排序字段越多,排序效果也會越差。建議2-4個。
3
Bloom Flilter
Bloom Flilter是一種空間節約型的概率數據結構,通過一個長度為M的位數組來存儲元素,可以添加元素但不能刪除元素。每個元素使用k個哈希函數生成k個數值,大小位于區間[0,數組長度-1]中,添加元素到Bloom Flilter時,將相應位置置為1。當查詢是否存在相應元素時,只需要判斷k個位置的值是否全為1,如果不全是1,說明不存在該元素,如果全是1,則不一定說明存在該元素。k是一個遠小于m的常數,m是跟添加到Bloom Flilter的元素個數成正比。兩者的具體取值由Bloom Flilter的誤判(false positive)比例決定。示例見下圖:
圖中,Bloom Flilter長度m為11,哈希函數個數k為2,添加兩個元素A和B,現在查詢元素A、C、D,因為元素A哈希映射的兩個位置都為1,且的確是A的哈希映射結果,所以能檢索到A存在。元素C因為不滿足所有哈希位置都為1,所以可以斷定C不存在(True Negative)。但是因為D的哈希映射位置并非是D的哈希映射結果,即使其對應的哈希位置都為1,也不能斷定D的存在,這對D來講,就是誤判(false positive)。
Bloom Filter利用少量哈希位來存儲和定位元素,無論存儲還是查詢,其時間復雜度都是常量級:O(k),只跟哈希函數的個數有關。其代價是有一定的沖突概率,數組長度同添加的元素數量成正比,當數組長度越長,哈希函數越多,沖突概率越小,反之,沖突概率越高。因此,在使用中,需要確定數組長度和沖突概率。Bloom Flilter使用內存維護,且不存儲元素本身,相比其他數據結構,能獲得較高的空間和查詢效率。缺點是只能確定元素是否存在,不能確定元素的具體位置,且不支持范圍查詢和刪除操作。
4
bitmap indices
位圖索引(bitmap indices)是一種專為多個鍵的簡單查詢而設計的。bitmap索引將每個被索引的列的值作為KEY,使用每個BIT表示一行,當這行中包含這個值時,設置為1,否則設置為0。應用位圖索引的前提是記錄必須被按順序編號,一般從0開始。給出編號n,必須能夠很容易的找到對應的記錄,如果記錄被存放在連續的塊,可以將編號n轉換成塊編號+塊內偏移的表示以快速定位記錄位置。
位圖索引用一個位來對應一條記錄,這便是記錄需要被編號的原因。instructor_info表如上圖,性別的值有男、女兩種,收入等級則劃分為5級,既有5種值。在給性別屬性建立位圖索引時,就會分別為male和female建立,對于male位圖來說,如果一條記錄的性別為male,則位圖上對應的位會置1,female、收入等級位圖也采用相同的做法。
位圖索引的優勢體現在根據多個鍵的查詢的時候,比如查詢:
where gender=’f’ and income_level='L2'只需將gender=’f’的位圖索引和income_level='L2'的位圖索引取位與運算即可:
除此之外,范圍查詢也是進行數據統計時候常見操作,基于位圖索引的位或運算很容易實現范圍查詢,比如下面的查詢:
where gender=’f’ and income_level>=’L2’我們只需要將income_level='L1'和income_level='L2'的位圖索引位或運算,然后再跟gender=’f’的位圖索引位與運算即可:
從上面示例中可見,bitmap索引就是用位圖表示的索引,對列的每個鍵值建立一個位圖。所以相對于b-tree索引,占用的存儲空間非常小,創建和使用非常快。相比BloomFilter索引,bitmap索引不僅支持等值過濾,還支持范圍過濾。經過良好編碼的位圖索引,還能夠獲得比BloomFilter索引更少的存儲空間和更精準的匹配。但bitmap索引使用也有限制,比如適合建在值重復度高的列上,建議在100到100,000之間,如:職業、地市等。重復度過高則對比其他類型索引沒有明顯優勢;重復度過低,則空間效率和性能會大大降低。對于經常更新的列,也不適合使用bitmap索引。
5
小文件合并與去重
流式數據入湖伴隨著大量小文件的產生,根據文件產生的更新方式分為可追加的方式和非追加的方式兩種:
可追加的方式:以只讀事件日志的方式寫入,如IoT事件
非追加的方式:以可更新的方式寫入,如CDC事件
在高頻的流處理場景,每天都可能產生成百上千的新文件,基于Flink實時計算引擎,事務提交的間隔越短,產生的文件大小越小,數量越多。在有些數據湖系統的實現中,即使沒有可提交的數據,也可能會生成空文件(但存在文件元數據)。
這些文件隨后被寫入對象存儲系統,如AWS S3、阿里云OSS等,然后再通過查詢引擎,如Athena 、Trino等查詢數據。大量的小文件將嚴重拖慢系統響應速度,因為讀取每個文件,系統都要完成由下面三個基本步驟組成的動作:
打開文件
查找元數據
關閉文件
比如對ceberg來說,讀取一個文件的數據,首先要打開快照文件,從快照中獲取Manifenst文件,然后從Manifenst獲取數據文件,最后從數據文件中讀取數據。雖然打開一個文件可能只需要數毫秒的時間,但是當文件數量規模足夠大,這個時間開銷就會達到無法忍受的程度。當使用云上對象存儲服務時,考慮到訪問頻次限制和調用預算,讀取大量的文件有是無法接受的。
對于非追加的方式,有兩種處理方式:
COW(Copy-on-Write):每次更新事件,會以該事件所在文件為副本,創建新的文件,最新的數據由當前最新的副本文件數據組成。COW會導致”寫放大“和并發提交沖突問題,適合于寫少讀多的場景。
MOR(Merge-on-Read):每次創建增量更新的文件,最新的數據是由前一次提交的快照數據和當前增量更新數據組成。MOR會導致查詢效率低,適合于寫多讀少的場景。MOR通常實現為一個持續性地合并并提交增量更新的后臺進程。
當前三大數據湖技術中,Delta Lake 和 Iceberg僅支持 Copy-on-Write,因此它們不適合寫負載重的場合,而Hudi同時支持Copy-on-Write和Merge-on-Read。Iceberg實現了 一種 Copy-on-Write 變體:每個快照只存儲增量數據,最新全量數據由最新的快照通過引用的Manifest包含的所有數據文件組成。為了盡可能保證寫入的效率,Iceberg將非追加的事件轉換為可追加的Insert事件和Delete事件,分別存儲在普通數據文件(data file)和刪除數據文件(delete file)這兩種類型的文件中。而刪除數據文件存儲的內容根據刪除方式分為文件路徑(position-delete)刪除和等值刪除(equality-delete)兩種,前者為解決在當前快照周期內反復增加和刪除相同主鍵記錄的問題而引入,后者因為Iceberg缺少主鍵索引,為了避免更新記錄去查詢歷史數據帶來的開銷,直接在刪除文件中記錄等值刪除,它適用于跨快照周期數據更新和刪除的場景。數據更新或刪除的方式雖然保證了寫入的高吞吐,但也帶來了新的問題:
每條更新事件都會涉及到兩個文件(追加數據文件和刪除數據文件),相比可追加存儲的方式,多了一份數據文件
在查詢時需要將追加數據文件與刪除數據文件關聯,排除掉刪除數據記錄,特別是等值刪除方式因為沒有記錄事件在追加數據文件中的位置,需要遍歷所有的追加數據文件,在沒有數據索引的情況下,該過程會很漫長
解決浙這些問題的方式就是合并:將小文件合并成大文件,在合并小文件的過程中過濾掉刪除的數據記錄,從而提升查詢效率。這是目前最有效的方式,通文件合并讓計算引擎花費更多時間在讀取數據內容,而不是將時間花在頻繁地打開文件、查找文件和關閉文件上面。
實現文件合并常見的方式是定時啟動Spark 或者 Hadoop離線作業合并小文件,該過程通常較長,具體時長視合并文件的大小而定。企業應用通常啟動獨立的集群運行,比如基于自建集群服務或者EMR云服務。在實踐中,合并小文件會出現很多問題,比較突出的是合并效率低和內存溢出。為什么會有這種問題呢?我們通過流程圖來看數據合并的過程:
如圖所示,合并數據文件時候,首先獲取待合并的數據文件列表,然后迭代讀取每個數據文件并將相關聯的刪除數據文件的數據加載到內存中的刪除數據集合(delete set)。迭代讀取數據文件的過程,類似數據庫中一個大表和一個小表進行join的過程,大表是流式表,小表是構建表,遍歷數據文件的每一條數據時,在刪除文件數據集合中檢查是否存在,如果存在,當前數據記錄就不會被保留。要知道每個數據文件可能關聯多個刪除數據文件,這些文件都是壓縮存儲的,比如parquet或avro,一旦刪除數據集加載到內存,只有當數據文件迭代結束之后才會釋放,因此很容易導致內存溢出。
另外,當前的Iceberg實現只合并了普通數據文件,對刪除數據文件并沒有合并,在更新數據頻繁的情況下,刪除數據文件數量也很可觀,不得不對其合并,這個也是在實踐中不得不考慮的問題。
這里介紹了在合并文件過程常見的問題,實際上還有很多細節問題需要考慮,這里簡單匯總下,做個小結:
確定何時進行文件合并,考慮因素可以是文件數量、文件大小。如果存在分區,還要考慮到分區的變更。
為節約存儲空間和費用,確保刪除未合并的文件。
盡可能調大合并文件的大小,同時解壓后內存能夠容得下。
盡可能避免文件合并作業和流作業提交時的鎖爭用和沖突。
總結
以上是生活随笔為你收集整理的数据湖常用查询优化技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java okhttp 实现对有道翻译的
- 下一篇: ABAC权限模型的设计