PostgreSQL GreenPlum HAWQ三者的關係及演變過程
這個日程安排同時也是我們公司核心團隊的技術進階史。公司創始團隊成員有幸以核心開發者的角色參與,從單機版的關係型資料庫(PostgreSQL),大規模並行處理(MPP)資料庫(Greenplum Database)到SQL on Hadoop解決方案(Apache HAWQ),以及最新的SQL on Cloud資料倉庫(HashData)。通過回顧這個技術演進的歷程,我們將闡述如何一步一步地解決聯機分析(OLAP)系統低延遲、高併發以及擴充套件性問題。
PostgreSQL
由於後面討論的所有的分散式資料庫,包括Greenplum Database,Apache HAWQ以及HashData雲端資料倉庫,都是基於單機版關係型資料庫PostgreSQL的,所以我們首先簡單介紹一下PostgreSQL,作為後續討論的基礎。
每個PostgreSQL資料庫的例項包含一個PostMaster的damon程序和多個子程序,包括負責寫出髒資料的BG Writer程序,收集統計資訊的Stats Collector程序,寫事務日誌的WAL Writer程序,等等。
客戶端應用通過libpq協議連線到PostMaster程序;PostMaster收到連線請求後,fork出一個子程序Postgres Server來處理來自這個連線的查詢語句。Postgres Server程序的功能元件可以分成兩大類:查詢執行和儲存管理。查詢執行元件包括解析器、分析器、優化器以及執行器。在查詢執行過程中,需要訪問和更新系統狀態和資料,包括快取,鎖,檔案和頁面等等。
Greenplum
作為一個單機版的關係型資料庫,PostgreSQL更多地是作為聯機事務處理(OLTP)系統使用的。當然,由於其豐富的分析功能,很多企業也會基於PostgreSQL來構建資料倉庫,特別是在資料量不大的情況下。但是,隨著資料量的增大,基於單機PostgreSQL構建的資料倉庫就無法滿足企業使用者對查詢響應時間的要求:低延遲。
為了解決這個問題,MPP架構就被引入了。這是MPP架構分散式資料庫的簡單示意圖。MPP資料庫通過將資料切片分佈到各個計算節點後並行處理來解決海量資料分析的難題。每個MPP資料庫叢集由一個主節點(為了提供高可用性,通常還會有一個從主節點)和多個計算節點組成。主節點和每個計算節點都有自己獨立的CPU,記憶體和外部儲存。主節點負責接收客戶端的請求,生成查詢計劃,並將計劃下發到每個計算節點,協調查詢計劃的完成,最後彙總查詢結果返回給客戶端。計算節點負責資料的儲存以及查詢計劃的執行。計算節點之間是沒有任何共享依賴的(shared nothing)。查詢在每個計算節點上面並行執行,大大提升了查詢的效率。
我們接下來要講的開源Greenplum Database就是基於PostgreSQL的MPP資料庫。對應到這個架構圖,每個節點上面的資料庫例項可以簡單的認為是一個PostgreSQL例項。
我們首先通過一條簡單的查詢,感性地認識一下Greenplum Database是如何執行一條查詢的。
這是一條簡單的兩表等值連線語句。其中,customer表是維度表,表資料以cust_id作為hash分佈的key;sales表是事實表,在這個例子中,我們可以認為它的表資料是round-robin的方式隨機分佈的,不影響查詢的執行。
每個查詢執行是一個由操作符組成的樹。只看其中一個節點的話(如前面所說,每個計算節點就是一個PostgreSQL的例項),為了執行兩表的等值連線,我們首先會將兩表的資料分別掃描出來,然後基於維度表customer建立hash桶。對於每一條從sales表掃描出來的紀錄,我們都會到hash桶去查。如果滿足匹配條件,資料連線結果;否則,直接pass。
如前面提到的,在Greenplum Database中,每張表的資料按照hash分佈或者隨機分佈打散到每個計算節點上面。在這個例子中,由於sales表是隨機分佈的,為了正確執行基於cust_id的等值連線,生成的執行計劃會在table scan上面新增一個Redistribution motion節點。這個motion節點根據cust_id的hash值對資料作重分佈,類似MapReduce中的shuffling。由於hash join操作是在每個節點上面分散式執行的,在將結果返回給客戶端的時候,需要在主節點上面執行彙總操作。Gather motion的作用就在於將每個節點上面的中間結果集中到主節點上面。
對於這樣一個並行的查詢計劃,我們會根據資料重分佈的操作將整棵查詢執行樹切割成不同的子樹。每個子樹對應查詢計劃的一個階段,我們稱為slice。查詢計劃和slice是邏輯上的概念。
在物理層面,對應的是並行執行計劃和gang。gang指的是執行同一個slice操作的一組程序。MPP資料庫的一個重要特徵是,計算和儲存是緊耦合的。每一張表的資料打散儲存到每個計算節點上面。為了確保查詢結果的正確性,每個計算節點都需要參與每條查詢的執行中。在Greenplum Database的架構設計中,對於每個slice執行子樹,在每個計算節點中會啟動一個相應的Postgres Server程序(這裡稱為QE程序)來執行對應的操作。執行同一個slice的一組QE程序我們稱為gang。對應於查詢計劃中的三個slice,在執行計劃中,相應有三組gang。其中底下的兩個gang,我們稱之為N-gang,因為這種型別的gang中,包含了每個計算節點上面啟動的一個QE程序。頂上的gang,我們稱之為1-gang,因為它只包含了一個程序。
一般來說,對於N張表的關聯操作,執行計劃中會包含2N個gang,其中1個1-gang,對應主節點上面的程序;2N-1個N-gang,對應每個計算節點上面啟動的2N-1個QE程序。在這2N-1個gang中,其中N個用於掃描N張表,中間N-1個gang用於兩表關聯。也就是說,對於一條涉及到N表關聯操作的查詢語句,我們需要在每個計算節點上面啟動2N-1個QE程序。
很多使用者在評估Greenplum Database的併發數,也就是支援的最大同時執行的查詢數量,首先會擔心主節點會成為瓶頸,直觀原因是所有使用者連線請求都首先會到主節點。其實,從資源使用的角度看,計算節點會首先成為瓶頸。因為在執行涉及多表關聯的複雜查詢時,計算節點上面啟動的程序數量會遠多於主節點。所以,Greenplum Database系統架構決定了它不能支援非常高的併發訪問。
前面我們簡單闡述了MPP分散式資料庫的架構,並通過一條簡單的查詢語句解釋了分散式的執行計劃。接下來我們深入討論一下Greenplum Database的重要元件。
首先是解析器。從使用者的角度看,Greenplum Database跟PostgreSQL沒有明顯差別。主節點作為整個分散式系統叢集的大腦,負責接收客戶連線,處理請求。跟PostgreSQL一樣,對於每一個連線請求,Greenplum Database都會在主節點上面fork一個Postgres Server(我們稱之為QD)程序出來,負責處理這個連線提交的查詢語句。對於每一條進來的查詢語句,QD程序中的解析器執行語法分析和詞法分析,生成解析樹。雖然在一些DDL語句上面,Greenplum Database跟PostgreSQL會有一些語法上的小不同,例如建表語句可以指定資料進行hash分佈的key,但總體上,在解析器這個元件上面,兩者的差別不大。
優化器根據解析器生成的解析樹,生成查詢計劃。查詢計劃描述瞭如何執行查詢。查詢計劃的優劣直接影響查詢的執行效率。對於同樣一條查詢語句,一個好的查詢執行效率比一個次好的查詢計劃快上100倍,也是一個很正常的事情。從PostgreSQL到MPP架構的Greenplum Database,優化器做了重大改動。雖然兩者都是基於代價來生成最優的查詢計劃,但是Greenplum Database除了需要常規的表掃描代價、連線和聚合的執行方式外,還需要考慮資料的分散式狀態、資料重分佈的代價,以及叢集計算節點數量對執行效率的影響,因為它最終是要生成一個分散式的查詢計劃。
排程器是Greenplum Database在PostgreSQL上新增的一個元件,負責分配處理查詢需要的計算資源,將查詢計劃傳送到每個計算節點。在Greenplum Database中,我們稱計算節點為Segment節點。前面也提過,每一個Segment例項實際上就是一個PostgreSQL例項。排程器根據優化器生成的查詢計劃確定執行計劃需要的計算資源,然後通過libpg(修改過的libpg協議)協議給每個Segment例項傳送連線請求,通過Segment例項上的PostMaster程序fork出前面提到過的QE程序。排程器同時負責這些fork出來的QE程序的整個生命週期。
每個QE程序接收到從排程器傳送過來的查詢計劃之後,通過執行器執行分配給自己的任務。除了增加一個新的稱謂Motion的操作節點(負責不同QE程序間的資料交換)之外,總體上看,Greenplum Database的執行器跟PostgreSQL的執行器差別不大。
MPP資料庫在執行查詢語句的時候,跟單機資料庫的一個重要差別在於,它會涉及到不同計算節點間的資料交換。在Greenplum Database系統架構中,我們引入了Interconnect元件負責資料交換,作用類似於MapReduce中的shuffling階段。不過與MapReduce基於HTTP協議不一樣,Greenplum Database出於資料傳輸效率和系統擴充套件性方面的考慮,實現了基於UDP協議的資料交換元件。前面在解析執行器的時候提到,Greenplum Database引入了一個叫Motion的操作節點。Motion操作節點就是通過Interconnect元件在不同的計算節點之間實現資料的重分佈。
前面講到的解析器、優化器、排程器、執行器和Interconnect都是跟計算相關的元件,屬於無狀態元件。下面我們再看一下跟系統狀態相關的元件。首先是,系統表。系統表負責儲存和管理資料庫、表、欄位等元資料。主節點上面的系統表是全域性資料庫物件的元資料,稱為全域性系統表;每個Segment例項上也有一份本地資料庫物件的元資料,稱為本地系統表。解析器、優化器、排程器、執行器和Interconenct等無狀態元件在執行過程中需要訪問系統表資訊,決定執行的邏輯。由於系統表分散式地儲存在不同的節點中,如何保持系統表中資訊的一致性是極具挑戰的任務。一旦出現系統表不一致的情況,整個分散式資料庫系統是無法正常工作的。
跟很多分散式系統一樣,Greenplum Database是通過分散式事務來確保系統資訊一致的,更確切地說,通過兩階段提交來確保系統元資料的一致性。主節點上的分散式事務管理器協調Segment節點上的提交和回滾操作。每個Segment例項有自己的事務日誌,確定何時提交和回滾自己的事務。本地事務狀態儲存在本地的事務日誌中。
介紹完Greenplum Database的查詢元件和系統狀態元件後,我們再看看它是如何提供高可用性的。首先是管理節點的高可用。我們採取的方式是,啟動一個稱為Standby的從主節點作為主節點的備份,通過同步程序同步主節點和Standby節點兩者的事務日誌,在Standby節點上重做系統表的更新操作,從而實現兩者在全域性系統表上面的資訊同步。當主節點出故障的時候,我們能夠切換到Standby節點,系統繼續正常工作,從而實現管理節點的高可用。
計算節點高可用性的實現類似於管理節點,但是細節上有些小不同。每個Segment例項都會有另外一個Segment例項作為備份。處於正常工作狀態的Segment例項我們稱為Primary,它的備份稱為Mirror。不同於管理節點日誌重放方式,計算節點的高可用是通過檔案複製。對於每一個Segment例項,它的狀態以檔案的形式儲存在本地儲存介質中。這些本地狀態可以分成三大類:本地系統表、本地事務日誌和本地表分割槽資料。通過以檔案複製的方式保證Primary和Mirror之間的狀態一致,我們能夠實現計算節點的高可用。
HAWQ
Hadoop出現之前,MPP資料庫是為數不多的大資料處理技術之一。隨著Hadoop的興起,特別是HDFS的成熟,越來越多的資料被儲存在HDFS上面。一個自然的問題出現了:我們怎樣才能高效地分析儲存在HDFS上面的資料,挖掘其中的價值。4,5年前,SQL-on-Hadoop遠沒有現在這麼火,市場上的解決方案也只有耶魯大學團隊做的Hadapt和Facebook做的Hive,像Impala,Drill,Presto,SparkSQL等都是後來才出現的。而Hadapt和Hive兩個產品,在當時無論是易用性還是查詢效能方面都差強人意。
我們當時的想法是將Greenplum Database跟HDFS結合起來。與其他基於connector聯結器的方式不同,我們希望讓HDFS,而不是本地儲存,成為MPP資料庫的資料持久層。這就是後來的Apache HAWQ專案。但在當時,我們把它叫做Greenplum on Hadoop,其實更準確的說法應該是,Greenplum on HDFS。當時的想法非常簡單,就是將Greenplum Database和HDFS部署在同一個物理機器叢集中,同時將Greenplum Database中的Append-only表的資料放到HDFS上面。Append-only表指的是隻能追加,不能更新和刪除的表,這是因為HDFS本身只能Append的屬性決定的。
除了Append-only表之外,Greenplum Database還支援Heap表,這是一種能夠支援增刪改查的表型別。結合前面提到的Segment例項的本地狀態,我們可以將本地儲存分成四大類:系統表、日誌、Append-only表分割槽資料和非Append-only表分割槽資料。我們將其中的Append-only表分割槽資料放到了HDFS上面。每個Segment例項對應一個HDFS的目錄,非常直觀。其它三類資料還是儲存在本地的磁碟中。
總體上說,相對於傳統的Greenplum Database, Greenplum on HDFS架構上並沒有太多的改動,只是將一部分資料從本地儲存放到了HDFS上面,但是每個Segment例項還是需要通過本地儲存儲存本地狀態資料。所以,從高可用性的角度看,我們還是需要為每個例項提供備份,只是需要備份的資料少了,因為Append-only表的資料現在我們是通過HDFS本身的高可用性提供的。
Greenplum on HDFS作為一個原型系統,驗證了MPP資料庫和HDFS是可以很好地整合起來工作的。基於這個原型系統,我們開始將它當成一個真正的產品來打造,也就是後來的HAWQ。
從Greenplum on HDFS到HAWQ,我們主要針對本地儲存做了系統架構上的調整。我們希望將計算節點的本地狀態徹底去掉。本地狀態除了前面提到的系統表(系統表又可以細分成只讀系統表(系統完成初始化後不會再發生更改的元資料,主要是資料庫內建的資料型別和函式)和可寫系統表(主要是通過DDL語句對元資料的修改,如建立新的資料庫和表))、事務日誌、Append-only表分割槽資料和非Append-only表分割槽資料,同時還有系統在執行查詢過程中產生的臨時資料,如外部排序時用到的臨時檔案。其中臨時資料和本地只讀系統表的資料都是不需要持久化的。我們需要考慮的是如何在Segment節點上面移除另外四類狀態資料。
Append-only表分割槽資料前面已經提到過,交給HDFS處理。為了提高訪問HDFS的效率,我們沒有采用Hadoop自動的HDFS訪問介面,而是用C++實現了原生的HDFS訪問庫,libhdfs3。針對非Append-only表資料的問題,我們的解決方案就比較簡單粗暴了:通過修改DDL,我們徹底禁止使用者建立Heap表,因為Heap表支援更新和刪除。所以,從那時起到現在最新的Apache HAWQ,都只支援表資料的追加,不支援更新和刪除。沒有了表資料的更新和刪除,分散式事務就變得非常簡單了。通過為每個Append-only表文件對應的元資料增加一列,邏輯EoF,即有效的檔案結尾。只要能夠保證EoF的正確性,我們就能夠保證事務的正確性。而且Append-only表文件的邏輯EoF資訊是儲存在主節點的全域性系統表中的,它的正確性通過主節點的本地事務保證。為了清理Append-only表文件在追加新資料時事務abort造成的髒資料,我們實現了HDFS Truncate功能。
對於本地可寫系統表,我們的做法是將Segment例項上面的本地可寫系統表放到主節點的全域性系統表中。這樣主節點就擁有了全域性唯一的一份系統表資料。查詢執行過程中需要用到的系統元資料,我們通過Metadata Dispatch的方式和查詢計劃一起分發給每個Segment例項。
通過上述的一系列策略,我們徹底擺脫了Segment節點的本地狀態,也就是實現了無狀態Segment。整個系統的高可用性策略就簡單了很多,而且也不需要再為Segment節點提供Mirror了,系統的利用率大大提升。
資料的高可用交給了HDFS來保證。當一個Segment節點出故障後,我們可以在任意一臺有空閒資源的機器上重新創始化一個新的Segment節點,加入到叢集中替代原來出故障的節點,整個叢集就能夠恢復正常工作。
我們也做到了計算和儲存物理上的解耦合,往徹底擺脫傳統MPP資料庫(例如Greenplum Database)計算和儲存緊耦合的目標邁出了有著實質意義的一步。
雖然在HAWQ 1.x的階段,我們做到了計算和儲存物理上的分離,但是邏輯上兩者還是整合的。原因是,在將本地表分割槽資料往HDFS上面遷移的時候,為了不改變原來Segment例項的執行邏輯流程,我們為每個Segment指定了一個其專有的HDFS目錄,以便跟原來本地資料目錄一一對應。每個Segment負責儲存和管理的資料都放在其對應的目錄的底下,而且該目錄底下的檔案,也只有它自身能夠訪問。這種HDFS資料跟計算節點邏輯上的整合關係,使得HAWQ 1.x版本依然沒有擺脫傳統MPP資料庫剛性的併發執行策略:無論查詢的複雜度如何,所有的計算節點都需要參與到每條查詢的執行中。這意味著,系統執行一條單行插入語句所使用的計算資源,和執行一條對幾TB資料進行復雜多表連線和聚合的語句所使用的資源是一樣的。這種剛性的並行執行策略,極大地約束了系統的擴充套件性和吞吐量,同時與Hadoop基於查詢複雜度來排程計算資源的彈性策略也是相違背的。
我們決心對HAWQ的系統架構做一次大的調整,使其更加地Hadoop Native,Hadoop原生,而不僅僅是簡單地將資料放到HDFS上面。當時,我們內部成為HAWQ 2.0,也就是大家現在在github上面看到的Apache HAWQ。
其中最重要的一步是,我們希望計算和儲存不僅物理上分離,邏輯上也是分離。資料庫中的使用者表資料在HDFS上不再按照每個Segment單獨來組織,而是按照全域性的資料庫物件來組織。舉個例子,我們將一張使用者表對應的多個數據檔案(因為當往該表插入資料的時候,為了提高資料插入的速度,系統會啟動了多個QE程序同時往HDFS寫資料,每個QE寫一個單獨檔案)放到同一個目錄底下,而不是像原來那樣,每個QE程序將檔案寫到自己對應的Segment目錄底下。這種改變帶來的一個直觀結果就是,由於所有檔案的資料檔案都放一起了,查詢執行的時候,根據需要掃描的資料量不同,我們既可以使用一個Segment例項去完成表掃描操作,也可以使用多個Segment例項去做,徹底擺脫了原來只能使用固定個Segment例項來執行查詢的剛性並行執行策略。
當然,HDFS資料目錄組織的改變只是實現HAWQ 2.0彈性執行引擎的一步,但是卻是最重要的一步。計算和儲存的徹底分離,使得HAWQ可以像MapReduce一樣根據查詢的複雜度靈活地排程計算資源,極大地提升了系統的擴充套件性和吞吐量。
我們簡單比較一下HAWQ 1.x和HAWQ 2.0的資源排程。
左邊展現的是HAWQ 1.x在同時處理三個查詢(分別來自三個不同的會話)時的資源排程情況。與傳統的MPP資料庫一樣,無論查詢的複雜度怎樣,每個Segment例項都會參與到這條查詢的執行中。換句話說,每個Segment例項都會啟動一個QE程序處理分配給它的任務。在這種情況下,系統能夠支援的併發查詢數量,跟叢集的計算節點數沒有任何關係,完全由一個計算節點決定(這裡,我們先不考慮主節點成為瓶頸的問題)。一個4個節點的HAWQ叢集能夠支援的併發查詢數量和一個400個節點的叢集是一樣的。
右邊展現的是HAWQ 2.0在同樣併發查詢下的資源排程情況。和Hadoop的MapReduce一樣,我們能夠根據查詢的複雜度決定需要排程多少計算資源參與到每條查詢的執行中。為了簡化闡述,我們這裡假設每條查詢只需要兩個計算資源單元。而且,執行單元可以根據資源管理器的排程演算法分配到不同的物理計算節點上面。這兩點靈活性:計算資源的數量可變和計算資源的位置可變,正是HAWQ 2.0彈性執行引擎的核心。在這種情況下,系統能夠支援的併發查詢數量,跟叢集的計算節點數量呈線性關係:計算節點越多,系統能夠支援的併發查詢數量越多(再次提醒,這裡,我們先不考慮主節點成為瓶頸的問題)。
所以,可以說,HAWQ 2.0成功解決了傳統MPP資料倉庫中計算節點首先成為吞吐量瓶頸的問題。同時,由於並不是所有計算節點都需要參與到每條查詢的執行中,HAWQ 2.0同時也解決了傳統MPP資料庫由於單個計算節點效能下降直接影響整個叢集效能的問題(這導致MPP叢集不能包含太多的計算節點,因為根據概率,叢集節點到達一定值後,出現單個計算節點效能下降的概率將會非常高),從而也很大程度上解決了擴充套件性問題。
雲端資料倉庫
通過將計算和儲存徹底分離成功解決了計算節點成為系統吞吐量瓶頸的問題後,現在系統的唯一瓶頸就剩下主節點。
如前面提到,主節點的功能主要分成兩類:元資料管理,包括系統表儲存和管理、鎖管理和分散式事務等等,和計算資源排程管理和執行。前者我們可以看成是狀態管理,後者是沒有狀態的元件。通過將狀態管理提取出來成為單獨一個功能層,我們讓主節點跟計算節點一樣變得沒有狀態。這樣,我們能夠根據系統併發查詢的變化,動態增加或者減少主節點的數量。這個設計借鑑了Hadoop YARN的設計,將原來的Job Manager的功能分成了Resource Manager和Application Manager,從而解決Hadoop叢集吞吐量的問題。
這是一個雲端資料倉庫的架構圖。其實,我們在HashData希望通過雲端資料倉庫解決企業使用者使用資料倉庫時碰到的多種難題,包括商業上和技術上。在這裡,我們只關注技術上的。
在這個系統架構中,我們將管理即元資料、計算和儲存三者分離了,每一層都能單獨動態伸縮,在解決系統吞吐量和擴充套件性問題的同時,提供了多維度的彈性。
我們利用雲平臺的物件儲存服務,如AWS的S3和青雲QingCloud的QingStor,作為系統資料的持久層。除了按需付費的經濟特性外,雲平臺的物件儲存服務在可擴充套件性、穩定性和高可用性等方面遠勝於我們自己維護的分散式檔案系統(如HDFS)。雖然物件儲存的訪問延遲遠高於本地磁碟訪問,但是我們可以通過本地快取的策略很大程度減輕延遲問題。
同樣的,我們利用雲平臺提供的虛擬機器作為我們的計算資源,也能夠一定程度上實現資源的隔離,從而保證不同的工作負載之間沒有相互影響。
雲平臺提供的近乎無限的計算和儲存資源(相對於資料倉庫應用來說),使得雲端資料倉庫能夠儲存和處理的資料達到一個全新的高度。
總結
最後,我們做一個簡單的總結。從PostgreSQL到Greenplum Database,我們通過大規模並行處理(MPP)技術,實現了處理海量資料時的低延遲目標。從Greenplum Database到Apache HAWQ,通過計算和儲存分析的策略,我們提升了系統的併發處理能力和擴充套件性。從Apache HAWQ到Cloud Data Warehouse,我們藉助雲平臺近乎無限的計算資源和儲存資源,以及管理、計算和資料三者分離,還有計算資源嚴格隔離,我們能夠取得近乎無限的併發處理能力和擴充套件性。
MPP資料庫採取的是流水式的執行引擎,中間的每個階段是不帶檢查點的。這意味著,只有有一個參與到查詢執行的QE程序出錯,整條查詢將會失敗,只能從頭開始重新執行這條查詢。而我們知道,當參與到查詢執行的QE程序達到一定數量的時候,QE程序出錯將是必然的,特別是在一個資源共享的環境中。這時候,即使是重新提交查詢重跑,失敗還是必然的。換句話說,我們幾乎無法成功執行需要排程大量計算資源的查詢。
展望未來,我們希望實現帶檢查點的流水式執行引擎,從而使得系統能夠處理任意大的查詢(單個查詢需要同時排程成千上萬的計算資源)。