前文提到,ZNBase 是由浪潮開源的一款 NewSQL 分佈式數據庫,基於谷歌 Spanner+F1 的論文設計,完美繼承了 Spanner 的設計理念,實現了基於對等架構的分佈式 SQL 引擎。ZNBase 的 SQL 引擎包含鏈接、編譯、緩存、分佈式日誌和分佈式執行五大服務組件,實現了多集羣多節點協同的高效計算,大大提高了用戶的查詢效率。node
爲了進一步提高 SQL 引擎的性能,ZNBase 研發團隊結合實際業務需求,在原有架構的基礎上,針對 SQL 引擎的編譯服務、執行服務、算法等方面進行了一系列深度定製化的優化改進工做。本文將這些改進工做逐一展開介紹。git
ZNBase 針對 SQL 引擎的優化改進
1.編譯服務優化
1.1 類型、功能、語法兼容
隨着日益增多的場景須要,ZNBase 陸續完善了對 PostgreSQL 、Oracle、MySQL 語法、類型、函數的兼容。算法
數據庫數據庫 |
PostgreSQL數組 |
Oracle緩存 |
MySQL數據結構 |
整體兼容狀況架構 |
70%框架 |
50%機器學習 |
70% |
類型兼容 |
普通類型65% |
原生數據類型24% 可替換類型:96% |
60% |
函數兼容 |
80%以上 |
70% |
65% |
功能兼容 |
80% |
50% |
50% |
1.2 計劃優化
ZNBase 還擴展了統計信息功能,除了:表的行數,表中列的 Distinct 值(某一列的惟一值總共有多少條),還額外引入了直方圖。爲 CBO 的優化提供了更多的一句。
統計信息獲取的簡單流程以下:
對每一個 range 進行抽樣,用蓄水池算法生成樣本集合,而後用樣本進行各類統計信息的預估,將結果經過寫入函數 writeResults,寫進系統表 system.table_statistics 中。
ZNBase 擴展了對執行計劃的管理,包括執行計劃綁定、自動捕獲綁定、自動更新綁定等。執行計劃綁定功能使得能夠在不修改 SQL 語句的狀況下選擇指定的執行計劃。用戶經過綁定執行計劃,能夠將計劃存入 ZNBase 中,下次再執行解析後計劃相同的 SQL 語句時,只要取出以前存入的計劃便可,省去了構建計劃的時間。ZNBase 還會智能地自動捕獲執行頻率較多的而且用戶以前沒有手動爲其建立綁定的 SQL 語句,在後臺自動爲其建立計劃綁定。
因爲表數據的變化,如:數據變化、數據結構變化、統計信息變化,可能會致使以前綁定的執行計劃執行效率下降,ZNBase 將自動檢測執行時間,將綁定好的執行計劃進行優化,爲用戶提升複合當前數據場景的更高的執行效率。
2. 執行優化
2.1 矢量算子
ZNBase 還引入了矢量算子,相比基於 Goetz Graefe 論文的「火山」模型,「矢量」模型在計算行數明顯大於列數的場景下,性能會有極大的提高。
從原理上講,這是用一系列專門針對數據類型和計算的特定編譯循環代替了通用的相似於解釋器的 SQL 表達式評估器,所以計算機能夠連續執行許多更簡單的任務,大大節省了重複的計算所須要的時間。配合 ZNBase 團隊開發的列式存儲,查詢性能還將有進一步的提高。
目前矢量算子支持的類型有:Array、BIT、BOOL、BYTES、COLLATE、DATE、DECIMAL、INET、INT、INTERVAL、JSONB、SERIAL、TIME、TIMESTAMP、TIMESTAMPTZ、UUID、FLOAT、STRING 等。
目前 ZNBase 支持的矢量算子有:Noop、TableReader、Distinct、Ordinality、Hashjoiner、MergeJoiner、Sorter、Windower 等。
舉例來講,請考慮一個包含三列的 People 表:Id,Name 和 Age。在火山模型中,每一個數據行由每一個算子處理一次 —— 一種逐行執行方法。相比之下,在矢量化執行引擎中,咱們一次傳遞了有限大小的面向列數據的批處理。咱們使用一組列,而不是使用元組數組的數據結構,其中每一列都是特定數據類型的數組。在該示例中,分批處理將由一個Id的整數數組,一個 Name 的字節數組和 Age 的整數數組組成。下圖顯示了兩個模型中數據佈局之間的區別:
火山模型
矢量模型
SELECT Name, (Age - 30) * 50 AS Bonus FROM People WHERE Age > 30;
這樣的語句查詢,在火山模型中,頂級用戶向 Project 算子請求一行,該請求被傳播到底層的 Scan 算子。掃描從鍵值存儲中讀取一行,並將其傳遞給 Select,Select 將檢查該行是否經過了 Age> 30 的謂詞。若是該行經過了檢查,則將其返回給 Project 算子以計算 Bonus = (Age - 30) * 50 做爲最終輸出。
火山模型流程圖
一次處理一行,對於每一行,咱們都在調用一個徹底通用的標量表達式的過濾器!表達式能夠是任何東西:乘法,除法,相等檢查或內置函數,甚至能夠是一長串這樣的東西。因爲這種通用性,計算機在每一行上都有不少工做要作——必須在甚至沒法執行任何工做以前檢查表達式的含義。與編譯後的語言相比,這種計算方式與解釋型語言一樣麻煩。
在矢量化模型中,咱們採用不一樣的方式。每一個矢量化算子背後的原理是在執行期間不容許任何自由度或運行時選擇。這意味着對於任務,數據類型和屬性的任意組合,應該由一個專門的算子來負責這項工做。對於示例查詢,用戶從算子鏈中請求一批。每一個算子都向其子級請求一批,執行其特定任務,而後將一批返回給其父級。
矢量模型流程圖
爲了對此進行可視化,請考慮由 SelectIntGreaterThanInt 處理的 People batch。該算子將選擇全部大於 30 的 Age 值。這個新的 sel_age batch 而後傳遞到 ProjectSubIntInt 算子,該算子執行簡單的減法運算以生成 tmp batch。最後,將這個 tmp batch 傳遞給 ProjectMultIntInt 算子,該算子將計算最終 Bonus =(Age-30)* 50 值。
矢量模型流程圖
2.2 並行優化
在ZNBase的開發過程當中也對算子進行了優化,提升了運算效率。
Tablereader 經過拆分 Span 進行並行的 baRequest 下發讀取數據,返回的數據封裝進 baResponse 裏面,放入管道由 tablereader 進行並行處理。
tablereader的並行分爲如下幾步:
Step1:Span 拆分,邏輯計劃完成後會生成一個 Span ALL(索引、主鍵查詢除外),Span ALL 會根據 table 的 range 邊界拆分從成多個範圍更小的 Span,每一個 Span 會得到相應的 range 信息,根據 rangeID 能夠取得對應 range 的副本信息,再根據副本選擇策略(就近選擇、隨機選擇、lease holder(默認)),獲取到對應副本的 nodeID,再將該 Span 放入一個 Map 結構(Map[nodeID][]Span)中;
當 tablereader 下發到了對應節點後,再將 Spans 進行均勻分配進 tablereader 的各個 worker fethcer 當中進行並行的數據讀取:
Step2:BatchRequest 下發,對應節點的 tablereader 的每一個 fetcher 的 spans 的每個 span 會封裝爲一個 ScanRequest 請求,多個 ScanRequest 請求封裝進一個 BatchRequest(BacthRequest 請求中 header 信息能夠指定一次請求返回的最大 kv 數目),該 BacthRequest 通過分發層邏輯後下發至對應節點的對應 Store 進行數據查詢,返回的數據封裝爲 BatchResponse,包含多個對應的 ScanResponse,將 ScanResponse 的 kv 數據放入 channel 中,再由每一個 fetcher 綁定的 worker 進行 kv 解碼以及後續的處理:
Step3:數據返回,每一個 fetcher 的 worker 協程處理(通過 filter 或 render )完每行 kv 數據後都會放入一個 buffer 當中(默認 buffer 緩存<= 64 行),每一個 worker 每完成一個 buffer 會將該 buffer 放入 tablereader 的結果管道中,提供 NextRow 和 NextChunk 兩類接口供上層算子調用:
原有 hashjoin 流程圖以下:
原有執行流程存在以下問題:
- 單點流程是串行化執行,致使取出 outer 表的一行數據須要等待正在進行的 hashjoin 計算完成。
- hashjoin 計算只由一個協程執行,數據量大的時候比較耗時。
通過優化後 hashjoin 由 3 個部分完成:
- Main thread:構造 hash 表;啓動 Outer Fecther 和 Join Workers;從 join Woker 拿取計算結果,返回至上層;等待全部的 join worker 結束,更新狀態爲計算完成。
- Outer Fetcher(協程):循環讀取 Outer 表每一行數據,將讀取的數據經過 channel 傳遞給 Join Woker 進行計算;通知 Join Wokers Outer 表讀取完成。
- Join Workers(協程):將 Outer Fetcher 發送來的數據進行 hash join 計算;將計算結果經過 channel 發送至 Main thread。
優化先後對比分析:
- 設構造 inner(storedSide)一側的 hash 表時間爲 t1
- 設讀取一條 outer(otherSide)數據時間爲 t2
- 設執行一輪 hashjoin 時間爲 t3
- 設 outer(otherSide)表有 m 條數據
執行完 hashjoin:
優化前耗時≈t1+m*t2+m*t3
優化後耗時≈t1+(m/n)*t3
Δt≈(m(n-1))/n t3+m*t2
預期:隨着 outer 表數據增多和 join worker 協程數增長,理論上優化越明顯。
經優化後有以下優點:
- 計算讀取分離:將讀取 outer 表和 hash join 計算分離,使得讀取 outer 表下一行數據沒必要再等待上一個 hash join 計算完成。
- 並行計算:啓用多個 join worker 參與 hash join 計算,提升了並行度。
展望將來
大數據行業的發展促進了國產分佈式數據庫的演進,爲適應這種發展大潮,云溪 NewSQL 數據庫 ZNBase 也會促使分佈式 SQL 引擎更趨向完善,將來會在語法上徹底兼容 Oracle 等傳統數據庫,計劃上加入更豐富的 HBO,完善 Cascade 框架,更加智能的提升用戶的使用體驗,執行上會兼容 HTAP 計算方式,順應當下飛速增加的數據量,適配更多的大數據、人工智能、機器學習場景,提供一個更智能、更高效的 SQL 計算引擎。
參考文檔 [1]. Volcano-An Extensible and Parallel Query Evaluation System
關於 ZNBase 的更多詳情能夠查看:
官方代碼倉庫:https://gitee.com/ZNBase/zn-kvs
ZNBase 官網:http://www.znbase.com/
對相關技術或產品有任何問題歡迎提 issue 或在社區中留言討論。同時歡迎廣大對分佈式數據庫感興趣的開發者共同參與 ZNBase 項目的建設。
聯繫郵箱:haojingyi@inspur.com