資源消耗降低 90%,速度提升 50%,解讀 Apache Doris Compaction 最新優化與實現

語言: CN / TW / HK

背景

LSM-Tree( Log Structured-Merge Tree)是數據庫中最為常見的存儲結構之一,其核心思想在於充分發揮磁盤連續讀寫的性能優勢、以短時間的內存與 IO 的開銷換取最大的寫入性能,數據以 Append-only 的方式寫入 Memtable、達到閾值後凍結 Memtable 並 Flush 為磁盤文件、再結合 Compaction 機制將多個小文件進行多路歸併排序形成新的文件,最終實現數據的高效寫入。

Apache Doris 的存儲模型也是採用類似的 LSM-Tree 數據模型。用户不同批次導入的數據會先寫入內存結構,隨後在磁盤上形成一個個的 Rowset 文件,每個 Rowset 文件對應一次數據導入版本。而 Doris 的 Compaction 則是負責將這些 Rowset 文件進行合併,將多個 Rowset 小文件合併成一個 Rowset 大文件。

在此過程中 Compaction 發揮着以下作用:

  • 每個 Rowset 內的數據是按主鍵有序的,但 Rowset 與 Rowset 之間數據是無序的,Compaction 會將多個 Rowset 的數據從無序變為有序,提升數據在讀取時的效率;
  • 數據以 Append-only 的方式進行寫入,因此 Delete、Update 等操作都是標記寫入,Compaction 會將標記的數據進行真正刪除或更新,避免數據在讀取時進行額外的掃描及過濾;
  • 在 Aggregate 模型上,Compaction 還可以將不同 Rowset 中相同 Key 的數據進行預聚合,減少數據讀取時的聚合計算,進一步提升讀取效率。

問題與思考

儘管 Compaction 在寫入和查詢性能方面發揮着十分關鍵的作用,但 Compaction 任務執行期間的寫放大問題以及隨之而來的磁盤 I/O 和 CPU 資源開銷,也為系統穩定性和性能的充分發揮帶來了新的挑戰。

在用户真實場景中,往往面臨着各式各樣的數據寫入需求,並行寫入任務的多少、單次提交數據量的大小、提交頻次的高低等,各種場景可能需要搭配不同的 Compaction 策略。而不合理的 Compaction 策略則會帶來一系列問題:

  • Compaction 任務調度不及時導致大量版本堆積、Compaction Score 過高,最終導致寫入失敗(-235/-238);
  • Compaction 任務執行速度慢,CPU 消耗高;
  • Compaction 任務內存佔用高,影響查詢性能甚至導致 BE OOM;

與此同時,儘管 Apache Doris 提供了多個參數供用户進行調整,但相關參數眾多且語義複雜,用户理解成本過高,也為人工調優增加了難度。

基於以上問題,從 Apache Doris 1.1.0 版本開始,我們增加了主動觸發式 QuickCompaction、引入了 Cumulative Compaction 任務的隔離調度並增加了小文件合併的梯度合併策略,對高併發寫入和數據實時可見等場景都進行了針對性優化。

而在 Apache Doris 最新的 1.2.2 版本和即將發佈的 2.0.0 版本中,我們對系統 Compaction 能力進行了全方位增強,在觸發策略、執行 方式 工程實現 以及參數配置上都進行了大幅優化, 在實時性、易用性與穩定性得到提升的同時更是徹底解決了查詢效率問題

Compaction 優化與實現

在設計和評估 Compaction 策略之時,我們需要綜合權衡 Compaction 的任務模型和用户真實使用場景,核心優化思路包含以下幾點:

  • 實時性和高效性。Compaction 任務觸發策略的實時性和任務執行方式的高效性直接影響到了查詢執行的速度,版本堆積將導致 Compaction Score 過高且觸發自我保護機制,導致後續數據寫入失敗。
  • 穩定性。Compaction 任務對系統資源的消耗可控,不會因 Compaction 任務帶來過多的內存與 CPU 開銷造成系統不穩定。
  • 易用性。由於 Compaction 任務涉及調度、策略、執行多個邏輯單元,部分特殊場景需要對 Compaction 進行調優,因此需要 Compaction 涉及的參數能夠精簡明瞭,指導用户快速進行場景化的調優。

具體在實現過程中,包含了觸發策略、執行方式、工程實現以及參數配置這四個方面的優化。

Compaction 觸發策略

調度策略決定着 Compaction 任務的實時性。在 Apache Doris 2.0.0 版本中,我們在主動觸發和被動掃描這兩種方式的基礎之上引入了 Tablet 休眠機制,力求在各類場景均能以最低的消耗保障最高的實時性。

主動觸發

主動觸發是一種最為實時的方式,在數據導入的階段就檢查 Tablet 是否有待觸發的 Compaction 任務,這樣的方式保證了 Compaction 任務與數據導入任務同步進行,在新版本產生的同時就能夠立即觸發數據合併,能夠讓 Tablet 版本數維持在一個非常穩定的狀態。主動觸發主要針對增量數據的 Compaction (Cumulative Compaction),存量數據則依賴被動掃描完成。

被動掃描

與主動觸發不同,被動掃描主要負責觸發大數據量的 Base Compaction 任務。Doris 通過啟動一個後台線程,對該節點上所有的 Tablet 元數據進行掃描,根據 Tablet Compaction 任務的緊迫程度進行打分,選擇得分最高的 Tablet 觸發 Compaction 任務。這樣的全局掃描模式能夠選出最緊急的 Tablet 進行 Compaction,但一般其執行週期較長,所以需要配合主動觸發策略實施。

休眠機制

頻繁的元信息掃描會導致大量的 CPU 資源浪費。因此在 Doris 2.0.0 版本中我們引入了 Tablet 休眠機制,來降低元數據掃描帶來的 CPU 開銷。通過對長時間沒有 Compaction 任務的 Tablet 設置休眠時間,一段時間內不再對該 Tablet 進行掃描,能夠大幅降低任務掃描的壓力。同時如果休眠的 Tablet 有突發的導入,通過主動觸發的方式也能顧喚醒 Compaction 任務,不會對任務的實時性有任何影響。

通過上述的主動掃描+被動觸發+休眠機制,使用最小的資源消耗,保證了 Compaction 任務觸發的實時性。

Compaction 執行方式

在 Doris 1.2.2 版本中中,我們引入了兩種全新的 Compaction 執行方式:

  • Vertical Compaction,用以徹底解決 Compaction 的內存問題以及大寬表場景下的數據合併;
  • Segment Compaction,用以徹底解決上傳過程中的 Segment 文件過多問題;

而在即將發佈的 Doris 2.0.0 版本,我們引入了 Ordered Data Compaction 以提升時序數據場景的數據合併能力。

Vertical Compaction

在之前的版本中,Compaction 通常採用行的方式進行,每次合併的基本單元為整行數據。由於存儲引擎採用列式存儲,行 Compaction 的方式對數據讀取極其不友好,每次 Compaction 都需要加載所有列的數據,內存消耗極大,而這樣的方式在寬表場景下也將帶來內存的極大消耗。

針對上述問題,我們在 Doris 1.2.2 版本中實現了對列式存儲更加友好的 Vertical Compaction,具體執行流程如下圖:

整體分為如下幾個步驟:

  1. 切分列組。將輸入 Rowset 按照列進行切分,所有的 Key 列一組、Value 列按 N 個一組,切分成多個 Column Group;
  2. Key 列合併。Key 列的順序就是最終數據的順序,多個 Rowset 的 Key 列採用堆排序進行合併,產生最終有序的 Key 列數據。在產生 Key 列數據的同時,會同時產生用於標記全局序 RowSources。
  3. Value 列的合併。逐一合併 Column Group 中的 Value 列,以 Key 列合併時產生的 RowSources 為依據對數據進行排序。
  4. 數據寫入。數據按列寫入,形成最終的 Rowset 文件。

由於採用了按列組的方式進行數據合併,Vertical Compaction 天然與列式存儲更加貼合,使用列組的方式進行數據合併,單次合併只需要加載部分列的數據,因此能夠極大減少合併過程中的內存佔用。在實際測試中,Vertical C ompaction 使用內存僅為原有 Compaction 算法的 1/10,同時 Compaction 速率提升 15%。

Vertical Compaction 在 1.2.2 版本中默認關閉狀態,需要在 BE 配置項中設置 enable_vertical_compaction=true 開啟該功能。

相關PR:https://github.com/apache/doris/pull/14524

Segment Compaction

在數據導入階段,Doris 會在內存中積攢數據,到達一定大小時 Flush 到磁盤形成一個個的 Segment 文件。大批量數據導入時會形成大量的 Segment 文件進而影響後續查詢性能,基於此 Doris 對一次導入的 Segment 文件數量做了限制。當用户導入大量數據時,可能會觸發這個限制,此時系統將反饋 -238 (TOO_MANY_SEGMENTS) 同時終止對應的導入任務。Segment compaction 允許我們在導入數據的同時進行數據的實時合併,以有效控制 Segment 文件的數量,增加系統所能承載的導入數據量,同時優化後續查詢效率。具體流程如下所示:

在新增的 Segment 數量超過一定閾值(例如 10)時即觸發該任務執行,由專門的合併線程異步執行。通過將每組 10個 Segment 合併成一個新的 Segment 並刪除舊 Segment,導入完成後的實際 Segment 文件數量將下降 10 倍。Segment Compaction 會伴隨導入的過程並行執行,在大數據量導入的場景下,能夠在不顯著增加導入時間的前提下大幅降低文件個數,提升查詢效率。

Segment Compaction 在 1.2.2 版本中默認關閉狀態,需要在 BE 配置項中設置 enable_segcompaction = true開啟該功能。

相關 PR : https://github.com/apache/doris/pull/12866

Ordered Data Compaction

隨着越來越多用户在時序數據分析場景應用 Apache Doris,我們在 Apache Doris 2.0.0 版本實現了全新的 Ordered Data Compaction。

時序數據分析場景一般具備如下特點:數據整體有序、寫入速率恆定、單次導入文件大小相對平均。針對如上特點,Ordered Data Compaction 無需遍歷數據,跳過了傳統 Compaction 複雜的讀數據、排序、聚合、輸出的流程,通過文件 Link 的方式直接操作底層文件生成 Compaction 的目標文件。

Ordered Data Compaction 執行流程包含如下幾個關鍵階段:

  1. 數據上傳階段。記錄 Rowset 文件的 Min/Max Key,用於後續合併 Rowset 數據交叉性的判斷;
  2. 數據檢查階段。檢查參與 Compaction 的 Rowset 文件的有序性與整齊度,主要通過數據上傳階段的 Min /Max Key 以及文件大小進行判斷。
  3. 數據合併階段。將輸入 Rowset 的文件硬鏈接到新 Rowset,然後構建新 Rowset 的元數據(包括行數,Size,Min/Max Key 等)。

可以看到上述階段與傳統的 Compaction 流程完全不一樣,只需要文件的 Link 以及內存元信息的構建,極其簡潔、輕量。針對時序場景設計的 Ordered Data Compaction 能夠在毫秒級別完成大規模的 Compaction 任務,其內存消耗幾乎為 ****0,對用户極其友好。

Ordered Data Compaction 在 2.0.0 版本中默認開啟狀態,如需調整在 BE 配置項中修改 enable_segcompaction即可。

使用方式:BE 配置 enable_ordered_data_compaction=true

Compaction 工程實現

除了上述在觸發策略和 Compaction 算法上的優化之外,Apache Doris 2.0.0 版本還對 Compaction 的工程實現進行了大量細節上的優化,包括數據零拷貝、按需加載、Idle Schedule 等。

數據零拷貝

Doris 採用分層的數據存儲模型,數據在 BE 上可以分為如下幾層:Tablet -> Rowset -> Segment -> Column -> Page,數據需要經過逐層處理。由於 Compaction 每次參與的數據量大,數據在各層之間的流轉會帶來大量的 CPU 消耗,在新版本中我們設計並實現了全流程無拷貝的 Compaction 邏輯,Block 從文件加載到內存中後,後續無序再進行拷貝,各個組件的使用都通過一個 BlockView 的數據結構完成,這樣徹底的解決了數據逐層拷貝的問題,將 Compaction 的效率再次提升了 5%。

按需加載

Compaction 的邏輯本質上是要將多個無序的 Rowset 合併成一個有序的 Rowset,在大部分場景中,Rowset 內或者 Rowset 間的數據並不是完全無序的,可以充分利用局部有序性進行數據合併,在同一時間僅需加載有序文件中的第一個,這樣隨着合併的進行再逐漸加載。利用數據的局部有序性按需加載,能夠極大減少數據合併過程中的內存消耗。

Idle schedule

在實際運行過程中,由於部分 Compaction 任務佔用資源多、耗時長,經常出現因為 Compaction 任務影響查詢性能的 Case。這類 Compaction 任務一般存在於 Base compaction 中,具備數據量大、執行時間長、版本合併少的特點,對任務執行的實時性要求不高。在新版本中,針對此類任務開啟了線程 Idle Schedule 特性,降低此類任務的執行優先級,避免 Compaction 任務造成線上查詢的性能波動。

易用性

在 Compaction 的易用性方面,Doris 2.0.0 版本進行了系統性優化。結合長期以來 Compaction 調優的一些經驗數據,默認配置了一套通用環境下表現最優的參數,同時大幅精簡了 Compaction 相關參數及語義,方便用户在特殊場景下的 Compaction 調優。

總結規劃

通過上述一系列的優化方式, 全新版本在 Compaction 過程中取得了極為顯著的改進效果。在 ClickBench 性能測試中,新版本 Compaction 執行速度 達到 30w row/s,相較於舊版本 提升 50 % ;資源消耗降幅巨大, 內存佔用僅為原先的 10% 。高併發數據導入場景下,Compaction Score 始終保持在 50 左右,且系統表現極為平穩。同時在時序數據場景中,Compaction 寫放大係數降低 90%,極大提升了可承載的寫入吞吐量。

後續我們仍將進一步探索迭代優化的空間,主要的工作方向將聚焦在自動化、可觀測性以及執行效率等方向上:

  1. 自動化調優。針對不同的用户場景,無需人工干預,系統支持進行自動化的 Compaction 調優;
  2. 可觀測性增強。收集統計 Compaction 任務的各項指標,用於指導自動化以及手動調優;
  3. 並行 Vertical Compaction。通過 Value 列併發執行,進一步提升 Vertical Compaction 效率。

以上方向的工作都已處於規劃或開發中,如果有小夥伴對以上方向感興趣,也歡迎參與到社區中的開發來。期待有更多人蔘與到 Apache Doris 社區的建設中 ,歡迎你的加入!

作者介紹:

一休,Apache Doris contributor,SelectDB 資深研發工程師

張正宇,Apache Doris contributor,SelectDB 資深研發工程師

# 相關鏈接:

SelectDB 官網

https://selectdb.com

Apache Doris 官網

http://doris.apache.org

Apache Doris Github

https://github.com/apache/doris

「其他文章」