本文經受權轉載自公衆號 PostgreSQL 中文社區,主要介紹了 Greenplum 集羣概述、分佈式數據存儲和分佈式查詢優化。數據庫
1、數據庫內核揭祕
Greenplum 是最成熟的開源分佈式分析型數據庫(今年 6 月份預計發佈的 Greenplum 6 之 OLTP 性能大幅提高,將成爲一款真正的 HTAP 數據庫,評測數據將於近期發佈),Gartner 2019 最新評測顯示 Greenplum 在經典數據分析領域位列全球第三,在實時數據分析領域位列並列第四。兩個領域中前十名中惟一一款開源數據庫產品。這意味着若是選擇一款基於開源的產品,前十名中別無選擇,惟此一款。Gartner 報告原文。bash
那麼 Greenplum 分佈式數據庫是如何煉成?衆所周知 Greenplum 基於 PostgreSQL。PostgreSQL 是最早進的單節點數據庫,其相關內核文檔、論文資源不少。而有關如何將單節點 PostgreSQL 改形成分佈式數據庫的資料相對較少。本文從 6 個方面介紹將單節點 PostgreSQL 數據庫發展成分佈式 MPP 數據庫所涉及的主要工做。固然這些僅僅是極簡概述,作到企業級產品化耗資數億美圓,百人規模的數據庫尖端人才團隊十幾年的研發投入結晶而成。數據結構
雖然不是必需,然而瞭解 PostgreSQL 基本內核知識對理解本文中的一些細節有幫助。Bruce Momjian 的 PPT 是極佳入門資料。架構
2、 Greenplum 集羣化概述
PostgreSQL 是世界上最早進的單機開源數據庫。Greenplum 基於 PostgreSQL,是世界上最早進的開源 MPP 數據庫 (有關 Greenplum 更多資訊請訪問 Greenplum 中文社區)。從用戶角度來看,Greenplum 是一個完備的關係數據庫管理系統(RDBMS)。從物理層面,它內含多個 PostgreSQL 實例,這些實例能夠單獨訪問。爲了實現多個獨立的 PostgreSQL 實例的分工和合做,呈現給用戶一個邏輯的數據庫,Greenplum 在不一樣層面對數據存儲、計算、通訊和管理進行了分佈式集羣化處理。Greenplum 雖然是一個集羣,然而對用戶而言,它封裝了全部分佈式的細節,爲用戶提供了單個邏輯數據庫。這種封裝極大的解放了開發人員和運維人員。app
把單節點 PostgreSQL 轉化成集羣涉及多個方面的工做,本文主要介紹數據分佈、查詢計劃並行化、執行並行化、分佈式事務、數據洗牌(shuffle)和管理並行化等 6 個方面。運維
Greenplum 在 PostgreSQL 之上還添加了大量其餘功能,例如 Append-Optimized 表、列存表、外部表、多級分區表、細粒度資源管理器、ORCA 查詢優化器、備份恢復、高可用、故障檢測和故障恢復、集羣數據遷移、擴容、MADlib 機器學習算法庫、容器化執行 UDF、PostGIS 擴展、GPText 套件、監控管理、集成 Kubernetes 等。機器學習
下圖展現了一個 Greenplum 集羣的俯瞰圖,其中一個 master 節點,兩個 segment 節點,每一個 segment 節點上部署了 4 個 segment 實例以提升資源利用率。每一個實例,無論是 master 實例仍是 segment 實例都是一個物理上獨立的 PostgreSQL 數據庫。分佈式
3、分佈式數據存儲
數據存儲分佈化是分佈式數據庫要解決的第一個問題。分佈式數據存儲基本原理相對簡單,實現比較容易,不少數據庫中間件也能夠作到基本的分佈式數據存儲。Greenplum 在這方面不僅僅作到了基本的分佈式數據存儲,還提供了不少更高級靈活的特性,譬如多級分區、多態存儲。Greenplum 6 進一步加強了這一領域,實現了一致性哈希和複製表,並容許用戶根據應用干預數據分佈方法。函數
以下圖所示,用戶看到的是一個邏輯數據庫,每一個數據庫有系統表(例如 pg_catalog 下面的 pg_class, pg_proc 等)和用戶表(下例中爲 sales 表和 customers 表)。在物理層面,它有不少個獨立的數據庫組成。每一個數據庫都有它本身的一份系統表和用戶表。master 數據庫僅僅包含元數據而不保存用戶數據。master 上仍然有用戶數據表,這些用戶數據表都是空表,沒有數據。優化器須要使用這些空表進行查詢優化和計劃生成。segment 數據庫上絕大多數系統表(除了少數表,例如統計信息相關表)和 master 上的系統表內容同樣,每一個 segment 都保存用戶數據表的一部分。
在 Greenplum 中,用戶數據按照某種策略分散到不一樣節點的不一樣 segment 實例中。每一個實例都有本身獨立的數據目錄,以磁盤文件的方式保存用戶數據。使用標準的 INSERT SQL 語句能夠將數據自動按照用戶定義的策略分佈到合適的節點,然而 INSERT 性能較低,僅適合插入少許數據。Greenplum 提供了專門的並行化數據加載工具以實現高效數據導入,詳情能夠參考 gpfdist 和 gpload 的官方文檔。此外 Greenplum 還支持並行 COPY,若是數據已經保存在每一個 segment 上,這是最快的數據加載方法。下圖形象的展現了用戶的 sales 表數據被分佈到不一樣的 segment 實例上。
除了支持數據在不一樣的節點間水平分佈,在單個節點上 Greenplum 還支持按照不一樣的標準分區,且支持多級分區。Greenplum 支持的分區方法有:
範圍分區:根據某個列的時間範圍或者數值範圍對數據分區。譬如如下 SQL 將建立一個分區表,該表按天分區,從 2016-01-01 到 2017-01-01 把所有一年的數據按天分紅了 366 個分區:
CREATE TABLE sales (id int, date date, amt decimal(10,2))
|
|
DISTRIBUTED BY (id)
|
|
PARTITION BY RANGE (date)
|
|
(
START (date '2016-01-01') INCLUSIVE
|
|
END (date '2017-01-01') EXCLUSIVE
|
|
EVERY (
INTERVAL '1 day') );
|
- 列表分區:按照某個列的數據值列表,將數據分不到不一樣的分區。譬如如下 SQL 根據性別建立一個分區表,該表有三個分區:一個分區存儲女士數據,一個分區存儲男士數據,對於其餘值譬如 NULL,則存儲在單獨 other 分區。
CREATE TABLE rank (id int, rank int, year int, gender char(1), count int )
|
|
DISTRIBUTED BY (id)
|
|
PARTITION BY LIST (gender)
|
|
(
PARTITION girls VALUES ('F'),
|
|
PARTITION boys VALUES ('M'),
|
|
DEFAULT PARTITION other );
|
下圖展現了用戶的 sales 表首先被分佈到兩個節點,而後每一個節點又按照某個標準進行了分區。分區的主要目的是實現分區裁剪以經過下降數據訪問量來提升性能。分區裁剪指根據查詢條件,優化器自動把不須要訪問的分區過濾掉,以下降查詢執行時的數據掃描量。PostgreSQL 支持靜態條件分區裁剪,Greenplum 經過 ORCA 優化器實現了動態分區裁剪。動態分區裁剪能夠提高十幾倍至數百倍性能。
Greenplum 支持多態存儲,即單張用戶表,能夠根據訪問模式的不一樣使用不一樣的存儲方式存儲不一樣的分區。一般不一樣年齡的數據具備不一樣的訪問模式,不一樣的訪問模式有不一樣的優化方案。多態存儲以用戶透明的方式爲不一樣數據選擇最佳存儲方式,提供最佳性能。Greenplum 提供如下存儲方式:
-
堆表(Heap Table):堆表是 Greenplum 的默認存儲方式,也是 PostgreSQL 的存儲方式。支持高效的更新和刪除操做,訪問多列時速度快,一般用於 OLTP 型查詢。
-
Append-Optimized 表:爲追加而專門優化的表存儲模式,一般用於存儲數據倉庫中的事實表。不適合頻繁的更新操做。
-
AOCO (Append-Optimized, Column Oriented) 表:AOCO 表爲列表,具備較好的壓縮比,支持不一樣的壓縮算法,適合訪問較少的列的查詢場景。
-
外部表:外部表的數據存儲在外部(數據不被 Greenplum 管理),Greenplum 中只有外部表的元數據信息。Greenplum 支持不少外部數據源譬如 S三、HDFS、文件、Gemfire、各類關係數據庫等和多種數據格式譬如 Text、CSV、Avro、Parquet 等。
以下圖所示,假設前面提到的 sales 表按照月份分區,那麼能夠採用不一樣的存儲策略保存不一樣時間的數據,例如最近三個月的數據使用堆表(Heap)存儲,更老的數據使用列存儲,一年之前的數據使用外部表的方式存儲在 S3 或者 HDFS 中。
數據分佈是任何 MPP 數據庫的基礎,也是 MPP 數據庫是否高效的關鍵之一。經過把海量數據分散到多個節點上,一方面大大下降了單個節點處理的數據量,另外一方面也爲處理並行化奠基了基礎,二者結合起來能夠極大的提升整個系統的性能。譬如在一百個節點的集羣上,每一個節點僅保存總數據量的百分之一,一百個節點同時並行處理,性能會是單個配置更強節點的幾十倍。若是數據分佈不均勻出現數據傾斜,受短板效應制約,整個系統的性能將會和最慢的節點相同。於是數據分佈是否合理對 Greenplum 總體性能影響很大。
Greenplum 6 提供瞭如下數據分佈策略。
-
哈希分佈
-
隨機分佈
-
複製表(Replicated Table)
Hash 分佈
哈希分佈是 Greenlum 最經常使用的數據分佈方式。根據預約義的分佈鍵計算用戶數據的哈希值,而後把哈希值映射到某個 segment 上。 分佈鍵能夠包含多個字段。分佈鍵選擇是否恰當是 Greenplum 可否發揮性能的主要因素。好的分佈鍵將數據均勻分佈到各個 segment 上,避免數據傾斜。
Greenplum 計算分佈鍵哈希值的代碼在 cdbhash.c 中。結構體 CdbHash 是處理分佈鍵哈希的主要數據結構。 計算分佈鍵哈希值的邏輯爲:
-
使用 makeCdbHash(int segnum) 建立一個 CdbHash 結構體
-
而後對每一個 tuple 執行下面操做,計算該 tuple 對應的哈希值,並肯定該 tuple 應該分佈到哪一個 segment 上:
1) cdbhashinit():執行初始化操做
2) cdbhash(), 這個函數會調用 hashDatum() 針對不一樣類型作不一樣的預處理,最後 addToCdbHash() 將處理後的列值添加到哈希計算中
3) cdbhashreduce() 映射哈希值到某個 segment
CdbHash 結構體:
typedef struct CdbHash
|
|
{
|
|
uint32 hash; /* 哈希結果值 */
|
|
int numsegs; /* segment 的個數 */
|
|
CdbHashReduce reducealg;
/* 用於減小桶的算法 */
|
|
uint32 rrindex; /* 循環索引 */
|
|
} CdbHash;
|
主要的函數
-
makeCdbHash(int numsegs): 建立一個 CdbHash 結構體,它維護瞭如下信息: Segment 的個數;Reduction 方法,若是 segment 個數是 2 的冪,則使用 REDUCE_BITMASK,不然使用 REDUCE_LAZYMOD;結構體內的 hash 值將會爲每一個 tuple 初始化,這個操做發生在 cdbhashinit() 中。
-
void cdbhashinit(CdbHash *h)
|
-
void cdbhash(CdbHash *h, Datum datum, Oid type): 添加一個屬性到 CdbHash 計算中,也就是添加計算 hash 時考慮的一個屬性。 這個函數會傳入函數指針: addToCdbHash。
-
void addToCdbHash(void *cdbHash, void *buf, size_t len); 實現了 datumHashFunction
h->hash = fnv1
_32_buf(buf, len, h->hash); // 在緩衝區執行 32 位 FNV 1 哈希
|
一般調用路徑是: evalHashKey -> cdbhash -> hashDatum -> addToCdbHash
- unsigned int cdbhashreduce(CdbHash *h): 映射哈希值到某個 segment,主要邏輯是取模,以下所示:
switch (h->reducealg)
|
|
{
|
|
case REDUCE_BITMASK:
|
|
result = FASTMOD(h->hash, (uint32) h->numsegs);
/* fast mod (bitmask) */
|
|
break;
|
|
case REDUCE_LAZYMOD:
|
|
result = (h->hash) % (h->numsegs);
/* simple mod */
|
|
break;
|
|
}
|
對於每個 tuple 要執行下面的 flow:
void cdbhashinit(CdbHash *h)
|
|
void cdbhash(CdbHash *h, Datum datum, Oid
type)
|
|
void add
ToCdbHash(void *cdbHash, void *buf, size_t len)
|
|
unsigned
int cdbhashreduce(CdbHash *h)
|
隨機分佈
若是不能肯定一張表的哈希分佈鍵或者不存在合理的避免數據傾斜的分佈鍵,則可使用隨機分佈。隨機分佈會採用循環的方式將一次插入的數據存儲到不一樣的節點上。隨機性只在單個 SQL 中有效,不考慮跨 SQL 的狀況。譬如若是每次插入一行數據到隨機分佈表中,最終的數據會所有保存在第一個節點上。
test=#
create table t1 (id int) DISTRIBUTED RANDOMLY;
|
|
CREATE TABLE
|
|
test=#
INSERT INTO t1 VALUES (1);
|
|
INSERT 0 1
|
|
test=#
INSERT INTO t1 VALUES (2);
|
|
INSERT 0 1
|
|
test=#
INSERT INTO t1 VALUES (3);
|
|
INSERT 0 1
|
|
test=#
SELECT gp_segment_id, * from t1;
|
|
gp_segment_id | id
|
|
---------------+----
|
|
1 | 1
|
|
1 | 2
|
|
1 | 3
|
|
有些工具使用隨機分佈實現數據管理,譬如擴容工具 gpexpand 在增長節點後須要對數據進行重分佈。在初始化的時候,gpexpand 會把全部表都標記爲隨機分佈,而後執行從新分佈操做,這樣重分佈操做不影響業務的正常運行。(Greenplum 6 從新設計了 gpexpand,再也不須要修改分佈策略爲隨機分佈)
複製表(Replicated Table)
Greenplum 6 支持一種新的分佈策略:複製表,即整張表在每一個節點上都有一個完整的拷貝。
test=# CREATE TABLE t2 (id int) DISTRIBUTED REPLICATED;
|
|
CREATE TABLE
|
|
test=# INSERT INTO t2 VALUES (1), (2), (3);
|
|
INSERT 0 3
|
|
test=# SELECT * FROM t2;
|
|
id
|
|
----
|
|
1
|
|
2
|
|
3
|
|
(3 rows)
|
|
test=# SELECT gp
_segment_id, * from t2;
|
|
gp_segment_id | id
|
|
---------------+----
|
|
0 | 1
|
|
0 | 2
|
|
0 | 3
|
複製表解決了兩個問題:
- UDF 在 segment 上不能訪問任何表。因爲 MPP 的特性,任何 segment 僅僅包含部分數據,於是在 segment 執行的 UDF 不能訪問任何表,不然數據計算錯誤。
yydzero=#
CREATE FUNCTION c() RETURNS bigint AS $$
|
|
yydzero$#
SELECT count(*) from t1 AS result;
|
|
yydzero$# $$
LANGUAGE SQL;
|
|
CREATE FUNCTION
|
|
yydzero=#
SELECT c();
|
|
c
|
|
---
|
|
6
|
|
(
1 row)
|
|
yydzero=#
select c() from t2;
|
|
ERROR:
function cannot execute on a QE slice because it accesses relation "public.t1" (seg0 slice1 192.168.1.107:25435 pid=76589)
|
若是把上面的 t1 改爲複製表,則不存在這個問題。
複製表有不少應用場景,譬如 PostGIS 的 spatial_ref_sys (PostGIS 有大量的 UDF 須要訪問這張表)和 PLR 中的 plr_modules 均可以採用複製表方式。在支持這個特性以前,Greenplum 只能經過一些小技巧來支持諸如 spatial_ref_sys 之類的表。
- 避免分佈式查詢計劃:若是一張表的數據在各個 segment 上都有拷貝,那麼就能夠生成本地鏈接計劃,而避免數據在集羣的不一樣節點間移動。若是用複製表存儲數據量比較小的表(譬如數千行),那麼性能有明顯的提高。 數據量大的表不適合使用複製表模式。
4、查詢計劃並行化
PostgreSQL 生成的查詢計劃只能在單節點上執行,Greenplum 須要將查詢計劃並行化,以充分發揮集羣的優點。
Greenplum 引入 Motion 算子(操做符)實現查詢計劃的並行化。Motion 算子實現數據在不一樣節點間的傳輸,它爲其餘算子隱藏了 MPP 架構和單機的不一樣,使得其餘大多數算子不用關心是在集羣上執行仍是在單機上執行。每一個 Motion 算子都有發送方和接收方。此外 Greenplum 還對某些算子進行了分佈式優化,譬如彙集。(本小節須要理解 PostgreSQL 優化器基礎知識,可參閱 src/backend/optimizer/README)
優化實例
在介紹技術細節以前,先看幾個例子。
下面的例子中建立了 2 張表 t1 和 t2,它們都有兩個列 c1, c2,都是以 c1 爲分佈鍵。
CREATE table t1 AS SELECT g c1, g +
1 as c2 FROM generate_series(1, 10) g DISTRIBUTED BY (c1);
|
|
CREATE table t2 AS SELECT g c1, g +
1 as c2 FROM generate_series(5, 15) g DISTRIBUTED BY (c1);
|
|
SQL1:
|
|
SELECT *
from t1, t2 where t1.c1 = t2.c1;
|
|
c1 | c2 | c1 | c2
|
|
----+----+----+----
|
|
5 | 6 | 5 | 6
|
|
6 | 7 | 6 | 7
|
|
7 | 8 | 7 | 8
|
|
8 | 9 | 8 | 9
|
|
9 | 10 | 9 | 10
|
|
10 | 11 | 10 | 11
|
|
(
6 rows)
|
SQL1 的查詢計劃爲以下所示,由於關聯鍵是兩個表的分佈鍵,因此關聯能夠在本地執行,HashJoin 算子的子樹不須要數據移動,最後 GatherMotion 在 master 上作彙總便可。
QUERY PLAN
|
|
------------------------------------------------------------------------------
|
|
Gather Motion 3:1 (slice1; segments: 3) (
cost=3.23..6.48 rows=10 width=16)
|
|
-> Hash Join (
cost=3.23..6.48 rows=4 width=16)
|
|
Hash Cond: t2.c1 = t1.c1
|
|
-> Seq Scan on t2 (
cost=0.00..3.11 rows=4 width=8)
|
|
-> Hash (
cost=3.10..3.10 rows=4 width=8)
|
|
-> Seq Scan on t1 (
cost=0.00..3.10 rows=4 width=8)
|
|
Optimizer: legacy query optimizer
|
SQL2:
|
|
SELECT * from t1, t2 where t1.c1 = t2.c2;
|
|
c1 | c2 | c1 | c2
|
|
----+----+----+----
|
|
9 | 10 | 8 | 9
|
|
10 | 11 | 9 | 10
|
|
8 | 9 | 7 | 8
|
|
6 | 7 | 5 | 6
|
|
7 | 8 | 6 | 7
|
|
(
5 rows)
|
|
SQL2 的查詢計劃以下所示,t1 表的關聯鍵 c1 也是其分佈鍵,t2 表的關聯鍵 c2 不是分佈鍵,因此數據須要根據 t2.c2 重分佈,以便全部 t1.c1 = t2.c2 的行都在同一個 segment 上執行關聯操做。
QUERY PLAN
|
|
----------------------------------------------------------------------------------------------
|
|
Gather Motion
3:1 (slice2; segments: 3) (cost=3.23..6.70 rows=10 width=16)
|
|
-> Hash Join (cost=
3.23..6.70 rows=4 width=16)
|
|
Hash Cond: t2.c2 = t1.c1
|
|
-> Redistribute Motion
3:3 (slice1; segments: 3) (cost=0.00..3.33 rows=4 width=8)
|
|
Hash Key: t2.c2
|
|
-> Seq Scan on t2 (cost=
0.00..3.11 rows=4 width=8)
|
|
-> Hash (cost=
3.10..3.10 rows=4 width=8)
|
|
-> Seq Scan on t1 (cost=
0.00..3.10 rows=4 width=8)
|
|
Optimizer: legacy query optimizer
|
SQL3:
|
|
SELECT * from t1, t2 where t1.c2 = t2.c2;
|
|
c1 | c2 | c1 | c2
|
|
----+----+----+----
|
|
8 | 9 | 8 | 9
|
|
9 | 10 | 9 | 10
|
|
10 | 11 | 10 | 11
|
|
5 | 6 | 5 | 6
|
|
6 | 7 | 6 | 7
|
|
7 | 8 | 7 | 8
|
|
(
6 rows)
|
|
SQL3 的查詢計劃以下所示,t1 的關聯鍵 c2 不是分佈鍵,t2 的關聯鍵 c2 也不是分佈鍵,因此採用廣播 Motion,使得其中一個表的數據能夠廣播到全部節點上,以保證關聯的正確性。最新的 master 代碼對這個查詢生成的計劃會對兩個表選擇重分佈,爲什麼這麼作能夠做爲一個思考題:)。
QUERY PLAN
|
|
--------------------------------------------------------------------------------------------
|
|
Gather Motion
3:1 (slice2; segments: 3) (cost=3.25..6.96 rows=10 width=16)
|
|
-> Hash Join (cost=
3.25..6.96 rows=4 width=16)
|
|
Hash Cond: t1.c2 = t2.c2
|
|
-> Broadcast Motion
3:3 (slice1; segments: 3) (cost=0.00..3.50 rows=10 width=8)
|
|
-> Seq Scan on t1 (cost=
0.00..3.10 rows=4 width=8)
|
|
-> Hash (cost=
3.11..3.11 rows=4 width=8)
|
|
-> Seq Scan on t2 (cost=
0.00..3.11 rows=4 width=8)
|
|
Optimizer: legacy query optimizer
|
|
SQL4:
|
|
SELECT * from t1 LEFT JOIN t2 on t1.c2 = t2.c2 ;
|
|
c1 | c2 | c1 | c2
|
|
----+----+----+----
|
|
1 | 2 | |
|
|
2 | 3 | |
|
|
3 | 4 | |
|
|
4 | 5 | |
|
|
5 | 6 | 5 | 6
|
|
6 | 7 | 6 | 7
|
|
7 | 8 | 7 | 8
|
|
8 | 9 | 8 | 9
|
|
9 | 10 | 9 | 10
|
|
10 | 11 | 10 | 11
|
|
(
10 rows)
|
|
SQL4 的查詢計劃以下所示,儘管關聯鍵和 SQL3 同樣,然而因爲採用了 left join,因此不能使用廣播 t1 的方法,不然數據會有重複,於是這個查詢的計劃對兩張表都進行了重分佈。根據路徑代價的不一樣,對於 SQL4 優化器也可能選擇廣播 t2 的方法。(若是數據量同樣,單表廣播代價要高於雙表重分佈,對於雙表重分佈,每一個表的每一個元組傳輸一次,至關於單表每一個元組傳輸兩次,而廣播則須要單表的每一個元組傳輸 nSegments 次。)
QUERY PLAN
|
|
----------------------------------------------------------------------------------------------
|
|
Gather Motion
3:1 (slice3; segments: 3) (cost=3.47..6.91 rows=10 width=16)
|
|
-> Hash Left Join (cost=
3.47..6.91 rows=4 width=16)
|
|
Hash Cond: t1.c2 = t2.c2
|
|
-> Redistribute Motion
3:3 (slice1; segments: 3) (cost=0.00..3.30 rows=4 width=8)
|
|
Hash Key: t1.c2
|
|
-> Seq Scan on t1 (cost=
0.00..3.10 rows=4 width=8)
|
|
-> Hash (cost=
3.33..3.33 rows=4 width=8)
|
|
-> Redistribute Motion
3:3 (slice2; segments: 3) (cost=0.00..3.33 ...
|
|
Hash Key: t2.c2
|
|
-> Seq Scan on t2 (cost=
0.00..3.11 rows=4 width=8)
|
|
Optimizer: legacy query optimizer
|
SQL5:
|
|
SELECT c2, count(
1) from t1 group by c2;
|
|
c2 | count
|
|
----+-------
|
|
5 | 1
|
|
6 | 1
|
|
7 | 1
|
|
4 | 1
|
|
3 | 1
|
|
10 | 1
|
|
11 | 1
|
|
8 | 1
|
|
9 | 1
|
|
2 | 1
|
|
(
10 rows)
|
|
上面四個 SQL 顯示不一樣類型的 JOIN 對數據移動類型 (Motion 類型) 的影響。SQL5 演示了 Greenplum 對彙集的優化:兩階段彙集。第一階段彙集在每一個 Segment 上對本地數據執行,而後經過重分佈到每一個 segment 上執行第二階段彙集。最後由 Master 經過 Gather Motion 進行彙總。 Greenplum 對某些 SQL 譬如 DISTINCT GROUP BY 也會採用三階段彙集。
QUERY PLAN
|
|
-----------------------------------------------------------------------------------------------
|
|
Gather Motion 3:1 (slice2; segments: 3) (
cost=3.55..3.70 rows=10 width=12)
|
|
-> HashAggregate (
cost=3.55..3.70 rows=4 width=12)
|
|
Group Key: t1.c2
|
|
-> Redistribute Motion 3:3 (slice1; segments: 3) (
cost=3.17..3.38 rows=4 width=12)
|
|
Hash Key: t1.c2
|
|
-> HashAggregate (
cost=3.17..3.17 rows=4 width=12)
|
|
Group Key: t1.c2
|
|
-> Seq Scan on t1 (
cost=0.00..3.10 rows=4 width=4)
|
|
Optimizer: legacy query optimizer
|
|
(9 rows)
|
Greenplum 爲查詢優化引入的新數據結構和概念
前面幾個直觀的例子展現了 Greenplum 對不一樣 SQL 生成的不一樣分佈式查詢計劃。下面介紹其主要內部機制。
爲了把單機查詢計劃變成並行計劃,Greenplum 引入了一些新的概念,分別對 PostgreSQL 的 Node、Path 和 Plan 結構體進行了加強:
-
新增一種節點(Node)類型:Flow
-
新增一種路徑(Path)類型:CdbMotionPath
-
新增一個新的查詢計劃(Plan)算子:Motion(Motion 的第一個字段是 Plan, Plan 結構體的第一個字段是 NodeTag type。Flow 的第一個節點也是 NodeTag type,和 RangeVar、IntoClause、Expr、RangeTableRef 是一個級別的概念)
-
爲 Path 結構體添加了 CdbPathLocus locus 這個字段,以表示結果元組在這個路徑下的重分佈策略
-
爲 Plan 結構體增長 Flow 字段,以表示這個算子的元組流向;
新 Node 類型:Flow
新節點類型 Flow 描述了並行計劃中元組的流向。 每一個查詢計劃節點(Plan 結構體)都有一個 Flow 字段,以表示當前節點的輸出元組的流向。 Flow 是一個新的節點類型,但不是一個查詢計劃節點。此外 Flow 結構體還包括一些用於計劃並行化的成員字段。
Flow 有三個主要字段:
FlowType,表示 Flow 的類型
-
UNDEFINED: 未定義 Flow
-
SINGLETON:表示的是 GatherMotion
-
REPLICATED:表示的是廣播 Motion
-
PARTITIONED: 表示的是重分佈 Motion。
Movement,肯定當前計劃節點的輸出,該使用什麼樣的 motion。主要用於把子查詢的計劃進行處理以適應分佈式環境。
-
None:不須要 motion
-
FOCUS:聚焦到單個 segment,至關於 GatherMotion
-
BROADCAST: 廣播 motion
-
REPARTITION: 哈希重分佈
-
EXPLICIT:定向移動元組到 segid 字段標記的 segments
CdbLocusType: Locus 的類型,優化器使用這個信息以選擇最合適的節點進行最合適的數據流向處理,肯定合適 Motion。
-
CdbLocusType_Null:不用 Locus
-
CdbLocusType_Entry: 表示 entry db (即 master) 上單個 backend 進程,能夠是 QD (Query Dispatcher),也能夠是 entrydb 上的 QE(Query Executor)
-
CdbLocusType_SingleQE:任何節點上的單個 backend 進程,能夠是 QD 或者任意 QE 進程
-
CdbLocusType_General:和任何 locus 都兼容
-
CdbLocusType_Replicated:在全部 QEs 都有副本
-
CdbLocusType_Hashed:哈希分佈到全部 QEs
-
CdbLocusType_Strewn:數據分佈存儲,可是分佈鍵未知
新 Path 類型:CdbMotionPath
Path 表示了一種可能的計算路徑(譬如順序掃描或者哈希關聯),更復雜的路徑會繼承 Path 結構體並記錄更多信息以用於優化。 Greenplum 爲 Path 結構體新加 CdbPathLocus locus 這個字段,用於表示結果元組在當前路徑下的重分佈和執行策略。
Greenplum 中表的分佈鍵決定了元組存儲時的分佈狀況,影響元組在那個 segment 的磁盤上的存儲。CdbPathLocus 決定了在執行時一個元組在不一樣的進程間(不一樣 segment 的 QE)的重分佈狀況,即一個元組該被那個進程處理。元組可能來自於表,也可能來自於函數。
Greenplum 還引入了一個新的路徑: CdbMotionPath, 用以表示子路徑的結果如何從發送方進程傳送給接收方進程。
新 Plan 算子:Motion
如上面所述,Motion 是一種查詢計劃樹節點,它實現了數據的洗牌(Shuffle),使得其父算子能夠從其子算子獲得須要的數據。Motion 有三種類型:
-
MOTIONTYPE_HASH:使用哈希算法根據重分佈鍵對數據進行重分佈,把通過算子的每一個元組發送到目標 segment,目標 segment 由重分佈鍵的哈希值肯定。
-
MOTIONTYPE_FIXED:發送元組給固定的 segment 集合,能夠是廣播 Motion(發送給全部的 segments)或者 Gather Motion (發送給固定的某個 segment)
-
MOTIONTYPE_EXPLICIT:發送元組給其 segid 字段指定的 segments,對應於顯式重分佈 Motion。和 MOTIONTYPE_HASH 的區別是不須要計算哈希值。
前面提到,Greenplum 爲 Plan 結構體引入了 Flow *flow 這個字段表示結果元組的流向。此外 Plan 結構體還引入了其餘幾個與優化和執行相關的字段,譬如表示是否須要 MPP 調度的 DispatchMethod dispatch 字段、是否能夠直接調度的 directDispatch 字段(直接調度到某個 segment,一般用於主鍵查詢)、方便 MPP 執行的分佈式計劃的 sliceTable、用於記錄當前計劃節點的父 motion 節點的 motionNode 等。
生成分佈式查詢計劃
下圖展現了 Greenplum 中傳統優化器(ORCA 優化器於此不一樣)的優化流程,本節強調與 PostgreSQL 的單機優化器不一樣的部分。
standard_planner 是 PostgreSQL 缺省的優化器,它主要調用了 subquery_planner 和 set_plan_references。在 Greenplum 中,set_plan_references 以後又調用了 cdbparallelize 以對查詢樹作最後的並行化處理。
subquery_planner 如名字所示對某個子查詢進行優化,生成查詢計劃樹,它主要有兩個執行階段:
-
基本查詢特性(也稱爲 SPJ:Select/Projection/Join)的優化,由 query_planner() 實現
-
高級查詢特性 (Non-SPJ) 的優化,例如彙集等,由 grouping_planner() 實現,grouping_planner() 會調用 query_planner() 進行基本優化,而後對高級特性進行優化。
Greenplum 對單機計劃的分佈式處理主要發生在兩個地方:
-
單個子查詢:Greenplum 的 subquery_planner() 返回的子查詢計劃樹已經進行了某些分佈式處理,譬如爲 HashJoin 添加 Motion 算子,二階段彙集等。
-
多個子查詢間:Greenplum 須要設置多個子查詢間恰當的數據流向,以使得某個子查詢的結果能夠被上層查詢樹使用。這個操做是由函數 cdbparallelize 實現的。
單個子查詢的並行化
Greenplum 優化單個子查詢的流程和 PostgreSQL 類似,主要區別在於:
-
關聯:根據關聯算子的左右子表的數據分佈狀況肯定是否添加 Motion 節點、什麼類型的 Motion 等。
-
彙集等高級操做的優化,譬如前面提到的兩階段彙集。
下面簡要介紹下主要流程:
首先使用 build_simple_rel() 構建簡單表的信息。build_simple_rel 得到表的基本信息,譬如表裏面有多少元組,佔用了多少個頁等。其中很重要的一個信息是數據分佈信息:GpPolicy 描述了基本表的數據分佈類型和分佈鍵。
而後使用 set_base_rel_pathlists() 設置基本表的訪問路徑。set_base_rel_pathlists 根據表類型的不一樣,調用不一樣的函數:
RTE_FUNCTION: create_functionscan_path()
|
|
RTE_RELATION: create_external_path()/create_aocs_path()/create_seqscan_path()/create_index_paths()
|
|
RTE_VALUES: create_valuesscan_path
|
|
這些函數會肯定路徑節點的 locus 類型,表示數據分佈處理相關的一種特性。 這個信息對於子查詢並行化很是重要,在後面把 path 轉換成 plan 的時候,被用於決定一個計劃的 FLOW 類型,而 FLOW 會決定執行器使用什麼樣類型的 Gang 來執行。
如何肯定 locus?
對於普通的堆表(Heap),順序掃描路徑 create_seqscan_path() 使用下面方式肯定路徑的 locus 信息:
-
若是表是哈希分佈,則 locus 類型爲 CdbLocusType_Hashed
-
若是是隨機分佈,則 locus 類型爲 CdbLocusType_Strewn
-
若是是系統表,則 locus 類型爲 CdbLocusType_Entry
對於函數,則 create_function_path() 使用下面方式肯定路徑的 locus:
-
若是函數是 immutable 函數,則使用:CdbLocusType_General
-
若是函數是 mutable 函數,則使用:CdbLocusType_Entry
-
若是函數須要在 master 上執行,則使用: CdbLocusType_Entry
-
若是函數須要在全部 segments 上執行,則使用 CdbLocusType_Strewn
若是 SQL 語句中包含關聯,則使用 make_rel_from_joinlist() 爲關聯樹生成訪問路徑。相應的函數有:create_nestloop_path/create_mergejoin_path/create_hashjoin_path。這個過程最重要的一點是肯定是否須要添加 Motion 節點以及什麼類型的 Motion 節點。 譬如前面 SQL1 關聯鍵是兩張表 t1/t2 的分佈鍵,於是不須要添加 Motion;而 SQL2 則須要對 t2 進行重分佈,以使得對於任意 t1 的元組,知足關聯條件 (t1.c1 = t2.c2) 的全部 t2 的元組都在同一個 segment 上。
若是 SQL 包含彙集、窗口函數等高級特性,則調用 cdb_grouping_planner() 進行優化處理,譬如將彙集轉換成兩階段彙集或者三階段彙集等。
最後一步是從全部可能的路徑中選擇最廉價的路徑,並調用 create_plan() 把最優路徑樹轉換成最優查詢樹。
在這個階段, Path 路徑的 Locus 影響生成的 Plan 計劃的 Flow 類型。Flow 和執行器一節中的 Gang 相關,Flow 使得執行器不用關心數據以什麼形式分佈、分佈鍵是什麼,而只關心數據是在多個 segment 上仍是單個 segment 上。 Locus 和 Flow 之間的對應關係:
FLOW_SINGLETON: Locus_Entry/Locus_SingleQE/Locus_General
|
|
FLOW_PARTITIONED: Locus_Hash/Locus_Strewn/Locus_Replicated
|
多個子查詢間的並行化
cdbparallelize() 主要目的是解決多個子查詢之間的數據流向,生成最終的並行化查詢計劃。它含有兩個主要步驟:prescan 和 apply_motion
prescan 有兩個目的,一個目的是對某些類型的計劃節點(譬如 Flow )作標記以備後面 apply_motion 處理;第二個目的是對子計劃節點 (SubPlan)進行標記或者變形。SubPlan 實際上不是查詢計劃節點,而是表達式節點,它包含一個計劃節點及其範圍表(Range Table)。 SubPlan 對應於查詢樹中的 SubLink(SQL 子查詢表達式),可能出如今表達式中。prescan 對 SubPlan 包含的計劃樹作如下處理:
-
若是 Subplan 是個 Initplan,則在查詢樹的根節點作一個標註,表示須要之後調用 apply_motion 添加一個 motion 節點。
-
若是 Subplan 是不相關的多行子查詢,則根據計劃節點中包含的 Flow 信息對子查詢執行 Gather 或者廣播操做。並在查詢樹之上添加一個新的 materialized (物化)節點,以防止對 Subplan 進行從新掃描。由於避免了每次從新執行子查詢,因此效率提升。
-
若是 Subplan 是相關子查詢,則轉換成可執行的形式。遞歸掃描直到遇到葉子掃描節點,而後使用下面的形式替換該掃描節點。通過這個轉換後,查詢樹能夠並行執行,由於相關子查詢已經變成結果節點的一部分,和外層的查詢節點在同一個 Slice 中。
Result
|
|
\
|
|
\_Material
|
|
\
|
|
\_Broadcast (or Gather)
|
|
\
|
|
\_SeqScan
|
- apply_motion: 根據計劃中的 Flow 節點,爲頂層查詢樹添加 motion 節點。根據 SubPlan 類型的不一樣(譬如 InitPlan、不相關多行子查詢、相關子查詢)添加不一樣的 Motion 節點。
譬如 SELECT * FROM tbl WHERE id = 1,prescan() 遍歷到查詢樹的根節點時會在根節點上標註,apply_motion() 時在根節點之上添加一個 GatherMotion。
做者介紹:
姚延棟,山東大學本科,中科院軟件所研究生。PostgreSQL 中文社區委員,致力於 Greenplum/PostgreSQL 開源數據庫產品、社區和生態的發展。
原文連接:
https://mp.weixin.qq.com/s/DI4U8UoddOHBRiJPzwfr-Q?scene=25#wechat_redirect