1、總體介紹
Doris是基於MPP架構的交互式SQL數據倉庫,主要用於解決了近實時的報表和多維分析。Doris高效的導入、查詢離不開其存儲結構精巧的設計。本文主要經過閱讀Doris BE模塊代碼,詳細分析了Doris BE模塊存儲層的實現原理,闡述和解密Doris高效的寫入、查詢能力背後的核心技術。其中包括Doris列存的設計、索引設計、數據讀寫流程、Compaction流程等功能。這裏會經過三篇文章來逐步進行介紹,分別爲《Doris存儲層設計介紹1——存儲結構設計解析》、《Doris存儲層設計介紹2——寫入流程、刪除流程分析》、《Doris存儲層設計介紹3——讀取、Compaction流程分析》。html
本文爲第三篇《Doris存儲層設計介紹3——讀取、Compaction流程分析》,文章詳細介紹了Doris存儲層的讀取數據、Compaction流程的實現。git
2、讀取流程
2.1 總體讀取流程
讀取流程爲寫入的逆過程,但讀取流程相對複雜些,主要由於進行大量的讀取優化。整個讀取流程分爲兩個階段,一個是init流程,一個是獲取next_block數據塊的過程。具體過程以下圖所示:github
層級關係以下:apache
- OlapScanner對一個tablet數據讀取操做總體的封裝。
- Reader對讀取的參數進行處理,並提供了按三種不一樣模型讀取的差別化處理。
- CollectIterator包含了tablet中多個RowsetReader,這些RowsetReader有版本順序,CollectIterator將這些RowsetReader歸併Merge成統一的Iterator功能,提供了歸併的比較器。
- RowsetReader則負責了對一個Rowset的讀取。
- RowwiseIterator提供了一個Rowset中全部Segment的統一訪問的Iterator功能。這裏的歸併策略能夠根據數據排序的狀況採用Merge或Union。
- SegmentIterator對應了一個Segment的數據讀取,Segment的讀取會根據查詢條件與索引進行計算找到讀取的對應行號信息,seek到對應的page,對數據進行讀取。其中,通過過濾條件後會對可訪問的行信息生成bitmap來記錄,BitmapRangeIterator爲單獨實現的能夠按照範圍訪問這個bitmap的迭代器。
- ColumnIterator提供了對列的相關數據和索引統一訪問的迭代器。ColumnReader、各個IndexReader等對應了具體的數據和索引信息的讀取。
2.2 讀取init階段的主要流程
init階段的執行流程以下:數組
2.2.1 OlapScanner查詢參數構造
- 根據查詢指定的version版本查找出須要讀取的RowsetReader(依賴於版本管理的rowset_graph版本路徑圖,取得查詢version範圍的最短路徑)。
- 設置查詢信息,包括_tablet、讀取類型reader_type=READER_QUERY、是否進行聚合、_version(從0到指定版本)。
- 設置查詢條件信息,包括filter過濾字段、is_nulls字段。
- 設置返回列信息。
- 設置查詢的key_ranges範圍(key的範圍數組,能夠經過short key index進行過濾)。
- 初始化Reader對象。
2.2.2 Reader的Init流程
- 初始化conditions查詢條件對象。
- 初始化bloomFilter列集合(eq、in條件,添加了bloomFilter的列)。
- 初始化delete_handler。包括了tablet中存在的全部刪除信息,其中包括了版本和對應的刪除條件數組。
- 初始化傳遞給下層要讀取返回的列,包括了返回值和條件對象中的列。
- 初始化key_ranges的start key、end key對應的RowCusor行遊標對象等。
- 構建的信息設置RowsetReader、CollectIterator。Rowset對象進行初始化,將RowsetReader加入到CollectIterator中。
- 調用CollectIterator獲取當前行(這裏其實爲第一行),這裏開啓讀取流程,第一次讀取。
2.2.3 RowsetReader的Init流程
- 構建SegmentIterator並過濾掉delete_handler中比當前Rowset版本小的刪除條件。
- 構建RowwiseIterator(對SegmentIterator的聚合iterator),將要讀取的SegmentIterator加入到RowwiseIterator。當全部Segment爲總體有序時採用union iterator順序讀取的方式,不然採用merge iterator歸併讀取的方式。
2.2.4 SegmentIterator的Init流程
- 初始化ReadableBlock,用來讀取當前的Segment文件的對象,實際讀取文件。
- 初始化_row_bitmap,用來存儲經過索引過濾後的行號,使用bitmap結構。
- 構建ColumnIterator,這裏僅是須要讀取列。
- 若是Column有BitmapIndex索引,初始化每一個Column的BitmapIndexIterator。
- 經過SortkeyIndex索引過濾數據。當查詢存在key_ranges時,經過key_range獲取命中數據的行號範圍。步驟以下:(1)根據每個key_range的上、下key,經過Segment的SortkeyIndex索引找到對應行號upper_rowid,lower_rowid,而後將獲得的RowRanges合併到row_bitmap中。
- 經過各類索引按條件過濾數據。條件包括查詢條件和刪除條件過濾信息。(1)按查詢條件,對條件中含有bitmap索引的列,使用bitmap索引進行過濾,查詢出存在數據的行號列表與row_bitmap求交。由於是精確過濾,將過濾的條件從Condition對象中刪除。 (2)按查詢條件中的等值(eq,in,is)條件,使用BloomFilter索引過濾數據。這裏會判斷當前條件可否命中Page,將這個Page的行號範圍與row_bitmap求交。(3)按查詢條件和刪除條件,使用ZoneMapIndex過濾數據,與ZoneMap每一個Page的索引求交,找到符合條件的Page。ZoneMapIndex索引匹配到的行號範圍與row_bitmap求交。
- 使用row_bitmap構造BitmapRangerInterator迭代器,用於後續讀取數據。
2.3 讀取next階段的主要流程
next階段的執行流程以下:微信
2.3.1 Reader讀取next_row_with_aggregation
在reader讀取時預先讀取一行,記錄爲當前行。在被調用next返回結果時會返回當前行,而後再預先讀取下一行做爲新的當前行。數據結構
- (reader的讀取會根據模型的類型分爲三種狀況。
- _dup_key_next_row讀取(明細數據模型)下,返回當前行,再直接讀取CollectorIterator讀取next做爲當前行。
- _agg_key_next_row讀取(聚合模型)下,會取CollectorIterator讀取next以後,判斷下一行是否與當前行的key相同,相同時則進行聚合計算,循環讀取下一行;不相同則返回當前累計的聚合結果,更新當前行。
- _unique_key_next_row讀取(unique key模型)下,與_agg_key_next_row模型方式邏輯相同,但存在一些差別。因爲支持了刪除操做,會查看聚合後的當前行是否標記爲刪除行。若是爲刪除行捨棄數據,直到找到一個不爲刪除行的數據才進行返回。
2.3.2 CollectIterator讀取next
CollectIterator中使用heap數據結構維護了要讀取RowsetReader集合,比較規則以下:按照各個RowsetReader當前行的key的順序,當key相同時比較Rowset的版本。架構
- CollectIterator從heap中pop出上一個最大的RowsetReader。
- 爲剛pop出的RowsetReader再讀取下一個新的row做爲RowsetReader的當前行並再放入heap中進行比較。讀取過程當中調用RowsetReader的nextBlock按RowBlock讀取。(若是當前取到的塊是部分刪除的page,還要對當前行按刪除條件對行進行過濾。)
- 取隊列的top的RowsetReader的當前行,做爲當前行返回
2.3.3 RowsetReader讀取next
- RowsetReader直接讀取了RowwiseIterator的next_batch。
- RowwiseIterator整合了SegmentIterator。當Rowset中的Segment總體有序時直接按Union方式迭代返回。當無序時按Merge歸併方式返回。RowwiseIterator一樣返回了當前最大的SegmentIterator的行數據,每次會調用SegmentIterator的next_batch獲取數據。
2.3.4 SegmentIterator讀取next_batch
- 根據init階段構造的BitmapRangerInterator,使用next_range每次取出要讀取的行號的一個範圍range_from、range_to。
- 先讀取條件列從range_from到range_to行的數據。過程以下:
- 調用有條件列各個columnIterator的seek_to_ordinal,各個列的讀取位置current_rowid定位到SegmentIterator的cur_rowid。這裏是經過二分查ordinal_index對齊到對應的data page。
- 讀出條件列的數據。按條件再進行一次過濾(此次是精確的過濾)。
- 再讀取無條件列的數據,放入到Rowblock中。返回Rowblock。
三、Compaction流程
3.一、Compaction總體介紹
Doris經過Compaction將增量聚合Rowset文件提高性能,Rowset的版本信息中設計了有兩個字段first、second來表示Rowset合併後的版本範圍。當未合併的cumulative rowset的版本first和second相等。Compaction時相鄰的Rowset會進行合併,生成一個新的Rowset,版本信息的first,second也會進行合併,變成一個更大範圍的版本。另外一方面,compaction流程大大減小rowset文件數量,提高查詢效率。性能
如上圖所示,Compaction任務分爲兩種,base compaction和cumulative compaction。cumulative_point是分割兩種策略關鍵。能夠這樣理解理解,cumulative_point右邊是從未合併過的增量Rowset,其每一個Rowset的first與second版本相等;cumulative_point左邊是合併過的Rowset,first版本與second版本不等。base compaction和cumulative compaction任務流程基本一致,差別僅在選取要合併的InputRowset邏輯有所不一樣。優化
3.二、Compaction詳細流程
Compaction合併總體流程以下圖所示:
(1)計算cumulative_point。
(2)選擇compaction的須要合併的InputRowsets集合:
base compaction選取條件:
- 當存在大於5個的非cumulative的rowset,將全部非cumulative的rowset進行合併。
- 版本first爲0的base rowset與其餘非cumulative的磁盤比例小於10:3時,合併全部非cumulative的rowset進行合併。
- 其餘狀況,不進行合併。
cumulative compaction選取條件:
- 選出Rowset集合的segment數量須要大於等於5而且小於等於1000(可配置),進行合併。
- 當輸出Rowset數量小於5時,但存在刪除條件版本大於Rowset second版本時,進行合併(讓刪除的Rowset快速合併進來)。
- 當累計的base compaction和cumulative compaction都時間大於1天時,進行合併。
- 其餘狀況不合並
(3)執行compaction
Compaction執行基本能夠理解爲讀取流程加寫入流程。這裏會將待合併的inputRowsets開啓Reader,而後經過next_row_with_aggregation讀取記錄。寫入到輸出的RowsetWriter中,生產新的OutputRowset,這個Rowset的版本爲InputRowsets版本全集範圍。
(4)更新cumulative_point
更新cumulative_point,將cumulative compaction的產出的OutputRowset交給後續的base compaction流程。
Compaction後對於aggregation key模型和unique key模型分散在不一樣Rowset但相同key的數據進行合併,達到了預計算的效果。同時減小了Rowset文件數量,提高了查詢效率。
四、總結
本文詳細介紹了Doris系統底層存儲層的讀取相關流程。讀取流程依賴於徹底的列存實現,對於olap的寬表場景(讀取大量行,少許列)可以快速掃描,基於多種索引功能進行過濾(包括short key、bloom filter、zoon map、bitmap等),可以跳過大量的數據掃描,還進行了延遲物化等優化,能夠對應多種場景的數據分析;Compaction執行流程一樣作了分場的優化。可以保證數據量接近的Rowset結合進行compact,減小IO操做提高效率。