PolarDB PostgreSQL 架構原理解讀


背景

PolarDB PostgreSQL(以下簡稱PolarDB)是一款阿里雲自主研發的企業級數據庫產品,采用計算存儲分離架構,兼容PostgreSQL與Oracle。PolarDB 的存儲與計算能力均可橫向擴展,具有高可靠、高可用、彈性擴展等企業級數據庫特性。同時,PolarDB 具有大規模並行計算能力,可以應對OLTP與OLAP混合負載;還具有時空、向量、搜索、圖譜等多模創新特性,可以滿足企業對數據處理日新月異的新需求。
PolarDB 支持多種部署形態:存儲計算分離部署、X-Paxos三節點部署、本地盤部署。

傳統數據庫的問題

隨着用戶業務數據量越來越大,業務越來越復雜,傳統數據庫系統面臨巨大挑戰,如:

  1. 存儲空間無法超過單機上限。
  2. 通過只讀實例進行讀擴展,每個只讀實例獨享一份存儲,成本增加。
  3. 隨着數據量增加,創建只讀實例的耗時增加。
  4. 主備延遲高。

PolarDB雲原生數據庫的優勢

針對上述傳統數據庫的問題,阿里雲研發了PolarDB雲原生數據庫。采用了自主研發的計算集群和存儲集群分離的架構。具備如下優勢:

  1. 擴展性:存儲計算分離,極致彈性。
  2. 成本:共享一份數據,存儲成本低。
  3. 易用性:一寫多讀,透明讀寫分離。
  4. 可靠性:三副本、秒級備份。

PolarDB整體架構簡介

下面會從兩個方面來解讀PolarDB的架構,分別是:存儲計算分離架構、HTAP架構。

存儲計算分離架構

PolarDB是存儲計算分離的設計,存儲集群和計算集群可以分別獨立擴展:

  1. 當計算能力不夠時,可以單獨擴展計算集群。
  2. 當存儲容量不夠時,可以單獨擴展存儲集群。

基於Shared-Storage后,主節點和多個只讀節點共享一份存儲數據,主節點刷臟不能再像傳統的刷臟方式了,否則:

  1. 只讀節點去存儲中讀取的頁面,可能是比較老的版本,不符合他自己的狀態。
  2. 只讀節點指讀取到的頁面比自身內存中想要的數據要超前。
  3. 主節點切換到只讀節點時,只讀節點接管數據更新時,存儲中的頁面可能是舊的,需要讀取日志重新對臟頁的恢復。

對於第一個問題,我們需要有頁面多版本能力;對於第二個問題,我們需要主庫控制臟頁的刷臟速度。

HTAP架構

讀寫分離后,單個計算節點無法發揮出存儲側大IO帶寬的優勢,也無法通過增加計算資源來加速大的查詢。我們研發了基於Shared-Storage的MPP分布式並行執行,來加速在OLTP場景下OLAP查詢。 PolarDB支持一套OLTP場景型的數據在如下兩種計算引擎下使用:

  • 單機執行引擎:處理高並發的OLTP型負載。
  • 分布式執行引擎:處理大查詢的OLAP型負載。

在使用相同的硬件資源時性能達到了傳統Greenplum的90%,同時具備了SQL級別的彈性:在計算能力不足時,可隨時增加參與OLAP分析查詢的CPU,而數據無需重分布。

PolarDB - 存儲計算分離架構

Shared-Storage帶來的挑戰

基於Shared-Storage之后,數據庫由傳統的share nothing,轉變成了shared storage架構。需要解決如下問題:

  • 數據一致性:由原來的N份計算+N份存儲,轉變成了N份計算+1份存儲。
  • 讀寫分離:如何基於新架構做到低延遲的復制。
  • 高可用:如何Recovery和Failover。
  • IO模型:如何從Buffer-IO向Direct-IO優化。

架構原理

首先來看下基於Shared-Storage的PolarDB的架構原理。

  • 主節點為可讀可寫節點(RW),只讀節點為只讀(RO)。
  • Shared-Storage層,只有主節點能寫入,因此主節點和只讀節點能看到一致的落盤的數據。
  • 只讀節點的內存狀態是通過回放WAL保持和主節點同步的。
  • 主節點的WAL日志寫到Shared-Storage,僅復制WAL的meta給只讀節點。
  • 只讀節點從Shared-Storage上讀取WAL並回放。

數據一致性

傳統數據庫的內存狀態同步

傳統share nothing的數據庫,主節點和只讀節點都有自己的內存和存儲,只需要從主節點復制WAL日志到只讀節點,並在只讀節點上依次回放日志即可,這也是復制狀態機的基本原理。

基於Shared-Storage的內存狀態同步

前面講到過存儲計算分離后,Shared-Storage上讀取到的頁面是一致的,內存狀態是通過從Shared-Storage上讀取最新的WAL並回放得來,如下圖:

 

  1. 主節點通過刷臟把版本200寫入到Shared-Storage。
  2. 只讀節點基於版本100,並回放日志得到200。

基於Shared-Storage的“過去頁面”

上述流程中,只讀節點中基於日志回放出來的頁面會被淘汰掉,此后需要再次從存儲上讀取頁面,會出現讀取的頁面是之前的老頁面,稱為“過去頁面”。如下圖:

 

  1. T1時刻,主節點在T1時刻寫入日志LSN=200,把頁面P1的內容從500更新到600;
  2. 只讀節點此時頁面P1的內容是500;
  3. T2時刻,主節點將日志200的meta信息發送給只讀節點,只讀節點得知存在新的日志;
  4. T3時刻,此時在只讀節點上讀取頁面P1,需要讀取頁面P1和LSN=200的日志,進行一次回放,得到P1的最新內容為600;
  5. T4時刻,只讀節點上由於BufferPool不足,將回放出來的最新頁面P1淘汰掉;
  6. 主節點沒有將最新的頁面P1為600的最新內容刷臟到Shared-Storage上;
  7. T5時刻,再次從只讀節點上發起讀取P1操作,由於內存中已把P1淘汰掉了,因此從Shared-Storage上讀取,此時讀取到了“過去頁面”的內容;

“過去頁面” 的解法

只讀節點在任意時刻讀取頁面時,需要找到對應的Base頁面和對應起點的日志,依次回放。如下圖:

 

  1. 在只讀節點內存中維護每個Page對應的日志meta。
  2. 在讀取時一個Page時,按需逐個應用日志直到期望的Page版本。
  3. 應用日志時,通過日志的meta從Shared-Storage上讀取。

通過上述分析,需要維護每個Page到日志的“倒排”索引,而只讀節點的內存是有限的,因此這個Page到日志的索引需要持久化,PolarDB設計了一個可持久化的索引結構 - LogIndex。LogIndex本質是一個可持久化的hash數據結構。

  1. 只讀節點通過WAL receiver接收從主節點過來的WAL meta信息。
  2. WAL meta記錄該條日志修改了哪些Page。
  3. 將該條WAL meta插入到LogIndex中,key是PageID,value是LSN。
  4. 一條WAL日志可能更新了多個Page(索引分裂),在LogIndex對有多條記錄。
  5. 同時在BufferPool中給該該Page打上outdate標記,以便使得下次讀取的時候從LogIndex重回放對應的日志。
  6. 當內存達到一定閾值時,LogIndex異步將內存中的hash刷到盤上。

通過LogIndex解決了刷臟依賴“過去頁面”的問題,也是得只讀節點的回放轉變成了Lazy的回放:只需要回放日志的meta信息即可。

基於Shared-Storage的“未來頁面”

在存儲計算分離后,刷臟依賴還存在“未來頁面”的問題。如下圖所示:

 

  1. T1時刻,主節點對P1更新了2次,產生了2條日志,此時主節點和只讀節點上頁面P1的內容都是500。
  2. T2時刻, 發送日志LSN=200給只讀節點。
  3. T3時刻,只讀節點回放LSN=200的日志,得到P1的內容為600,此時只讀節點日志回放到了200,后面的LSN=300的日志對他來說還不存在。
  4. T4時刻,主節點刷臟,將P1最新的內容700刷到了Shared-Storage上,同時只讀節點上BufferPool淘汰掉了頁面P1。
  5. T5時刻,只讀節點再次讀取頁面P1,由於BufferPool中不存在P1,因此從共享內存上讀取了最新的P1,但是只讀節點並沒有回放LSN=300的日志,讀取到了一個對他來說超前的“未來頁面”。
  6. “未來頁面”的問題是:部分頁面是未來頁面,部分頁面是正常的頁面,會到時數據不一致,比如索引分裂成2個Page后,一個讀取到了正常的Page,另一個讀取到了“未來頁面”,B+Tree的索引結構會被破壞。

“未來頁面”的解法

“未來頁面”的原因是主節點刷臟的速度超過了任一只讀節點的回放速度(雖然只讀節點的Lazy回放已經很快了)。因此,解法就是對主節點刷臟進度時做控制:不能超過最慢的只讀節點的回放位點。如下圖所示:

 

  1. 只讀節點回放到T4位點。
  2. 主節點在刷臟時,對所有臟頁按照LSN排序,僅刷在T4之前的臟頁(包括T4),之后的臟頁不刷。
  3. 其中,T4的LSN位點稱為“一致性位點”。

低延遲復制

傳統流復制的問題

  1. 同步鏈路:日志同步路徑IO多,網絡傳輸量大。
  2. 頁面回放:讀取和Buffer修改慢(IO密集型 + CPU密集型)。
  3. DDL回放:修改文件時需要對修改的文件加鎖,而加鎖的過程容易被阻塞,導致DDL慢。
  4. 快照更新:RO高並發引起事務快照更新慢。

如下圖所示:

 

  1. 主節點寫入WAL日志到本地文件系統中。
  2. WAL Sender進程讀取,並發送。
  3. 只讀節點的WAL Receiver進程接收寫入到本地文件系統中。
  4. 回放進程讀取WAL日志,讀取對應的Page到BufferPool中,並在內存中回放。
  5. 主節點刷臟頁到Shared Storage。

可以看到,整個鏈路是很長的,只讀節點延遲高,影響用戶業務讀寫分離負載均衡。

優化1 - 只復制Meta

因為底層是Shared-Storage,只讀節點可直接從Shared-Storage上讀取所需要的WAL數據。因此主節點只把WAL日志的元數據(去掉Payload)復制到只讀節點,這樣網絡傳輸量小,減少關鍵路徑上的IO。如下圖所示:

 

  1. WAL Record是由:Header,PageID,Payload組成。
  2. 由於只讀節點可以直接讀取Shared-Storage上的WAL文件,因此主節點只把 WAL 日志的元數據發送(復制)到只讀節點,包括:Header,PageID。
  3. 在只讀節點上,通過WAL的元數據直接讀取Shared-Storage上完整的WAL文件。

通過上述優化,能顯著減少主節點和只讀節點間的網絡傳輸量。從下圖可以看到網絡傳輸量減少了98%。

 

優化2 - 頁面回放優化

在傳統DB中日志回放的過程中會讀取大量的Page並逐個日志Apply,然后落盤。該流程在用戶讀IO的關鍵路徑上,借助存儲計算分離可以做到:如果只讀節點上Page不在BufferPool中,不產生任何IO,僅僅記錄LogIndex即可。
可以將回放進程中的如下IO操作offload到session進程中:

  1. 數據頁IO開銷。
  2. 日志apply開銷。
  3. 基於LogIndex頁面的多版本回放。

如下圖所示,在只讀節點上的回放進程中,在Apply一條WAL的meta時:

 

  1. 如果對應Page不在內存中,僅僅記錄LogIndex。
  2. 如果對應的Page在內存中,則標記為Outdate,並記錄LogIndex,回放過程完成。
  3. 用戶session進程在讀取Page時,讀取正確的Page到BufferPool中,並通過LogIndex來回放相應的日志。
  4. 可以看到,主要的IO操作有原來的單個回放進程offload到了多個用戶進程。

通過上述優化,能顯著減少回放的延遲,比AWS Aurora快30倍。

 

優化3 - DDL鎖回放優化

在主節點執行DDL時,比如:drop table,需要在所有節點上都對表上排他鎖,這樣能保證表文件不會在只讀節點上讀取時被主節點刪除掉了(因為文件在Shared-Storage上只有一份)。在所有只讀節點上對表上排他鎖是通過WAL復制到所有的只讀節點,只讀節點回放DDL鎖來完成。
而回放進程在回放DDL鎖時,對表上鎖可能會阻塞很久,因此可以通過把DDL鎖也offload到其他進程上來優化回訪進程的關鍵路徑。

通過上述優化,能夠回放進程一直處於平滑的狀態,不會因為去等DDL而阻塞了回放的關鍵路徑。

上述3個優化之后,極大的降低了復制延遲,能夠帶來如下優勢:

  • 讀寫分離:負載均衡,更接近Oracle RAC使用體驗。
  • 高可用:加速HA流程。
  • 穩定性:最小化未來頁的數量,可以寫更少或者無需寫頁面快照。

Recovery優化

背景

數據庫OOM、Crash等場景恢復時間長,本質上是日志回放慢,在共享存儲Direct-IO模型下問題更加突出。

 

Lazy Recovery

前面講到過通過LogIndex我們在只讀節點上做到了Lazy的回放,那么在主節點重啟后的recovery過程中,本質也是在回放日志,那么我們可以借助Lazy回放來加速recovery的過程:

 

  1. 從checkpoint點開始逐條去讀WAL日志。
  2. 回放完LogIndex日志后,即認為回放完成。
  3. recovery完成,開始提供服務。
  4. 真正的回放被offload到了重啟之后進來的session進程中。

優化之后(回放500MB日志量):

 

Persistent BufferPool

上述方案優化了在recovery的重啟速度,但是在重啟之后,session進程通過讀取WAL日志來回放想要的page。表現就是在recovery之后會有短暫的響應慢的問題。優化的辦法為在數據庫重啟時BufferPool並不銷毀,如下圖所示:crash和restart期間BufferPool不銷毀。

內核中的共享內存分成2部分:

  1. 全局結構,ProcArray等。
  2. BufferPool結構;其中BufferPool通過具名共享內存來分配,在進程重啟后仍然有效。而全局結構在進程重啟后需要重新初始化。

而BufferPool中並不是所有的Page都是可以復用的,比如:在重啟前,某進程對Page上X鎖,隨后crash了,該X鎖就沒有進程來釋放了。因此,在crash和restart之后需要把所有的BufferPool遍歷一遍,剔除掉不能被復用的Page。另外,BufferPool的回收依賴k8s。
該優化之后,使得重啟前后性能平穩。

 

PolarDB - HTAP架構

PolaDB讀寫分離后,由於底層是存儲池,理論上IO吞吐是無限大的。而大查詢只能在單個計算節點上執行,單個計算節點的CPU/MEM/IO是有限的,因此單個計算節點無法發揮出存儲側的大IO帶寬的優勢,也無法通過增加計算資源來加速大的查詢。我們研發了基於Shared-Storage的MPP分布式並行執行,來加速在OLTP場景下OLAP查詢。

HTAP架構原理

PolarDB底層存儲在不同節點上是共享的,因此不能直接像傳統MPP一樣去掃描表。我們在原來單機執行引擎上支持了MPP分布式並行執行,同時對Shared-Storage進行了優化。 基於Shared-Storage的MPP是業界首創,它的原理是:

  1. Shuffle算子屏蔽數據分布。
  2. ParallelScan算子屏蔽共享存儲。

如圖所示:

  1. 表A和表B做join,並做聚合。
  2. 共享存儲中的表仍然是單個表,並沒有做物理上的分區。
  3. 重新設計4類掃描算子,使之在掃描共享存儲上的表時能夠分片掃描,形成virtual partition。

分布式優化器

基於社區的GPORCA優化器擴展了能感知共享存儲特性的Transformation Rules。使得能夠探索共享存儲下特有的Plan空間,比如:對於一個表在PolarDB中既可以全量的掃描,也可以分區域掃描,這個是和傳統MPP的本質區別。
圖中,上面灰色部分是PolarDB內核與GPORCA優化器的適配部分。
下半部分是ORCA內核,灰色模塊是我們在ORCA內核中對共享存儲特性所做的擴展。

 

算子並行化

PolarDB中有4類算子需要並行化,下面介紹一個具有代表性的Seqscan的算子的並行化。為了最大限度的利用存儲的大IO帶寬,在順序掃描時,按照4MB為單位做邏輯切分,將IO盡量打散到不同的盤上,達到所有的盤同時提供讀服務的效果。這樣做還有一個優勢,就是每個只讀節點只掃描部分表文件,那么最終能緩存的表大小是所有只讀節點的BufferPool總和。

下面的圖表中:

  1. 增加只讀節點,掃描性能線性提升30倍。
  2. 打開Buffer時,掃描從37分鍾降到3.75秒。

 

消除數據傾斜問題

傾斜是傳統MPP固有的問題:

  1. 在PolarDB中,大對象的是通過heap表關聯TOAST​表,無論對哪個表切分都無法達到均衡。
  2. 另外,不同只讀節點的事務、buffer、網絡、IO負載抖動。

以上兩點會導致分布執行時存在長尾進程。

 

  1. 協調節點內部分成DataThread和ControlThread。
  2. DataThread負責收集匯總元組。
  3. ControlThread負責控制每個掃描算子的掃描進度。
  4. 掃描快的工作進程能多掃描邏輯的數據切片。
  5. 過程中需要考慮Buffer的親和性。

需要注意的是:盡管是動態分配,盡量維護buffer的親和性;另外,每個算子的上下文存儲在worker的私有內存中,Coordinator不存儲具體表的信息;

下面表格中,當出現大對象時,靜態切分出現數據傾斜,而動態掃描仍然能夠線性提升。

 

SQL級別彈性擴展

那我們利用數據共享的特點,還可以支持雲原生下極致彈性的要求:把Coordinator全鏈路上各個模塊所需要的外部依賴存在共享存儲上,同時worker全鏈路上需要的運行時參數通過控制鏈路從Coordinator同步過來,使Coordinator和worker無狀態化。

因此:

  1. SQL連接的任意只讀節點都可以成為Coordinator節點,這解決了Coordinator單點問題。
  2. 一個SQL能在任意節點上啟動任意worker數目,達到算力能SQL級別彈性擴展,也允許業務有更多的調度策略:不同業務域同時跑在不同的節點集合上。

 

事務一致性

多個計算節點數據一致性通過等待回放和globalsnapshot機制來完成。等待回放保證所有worker能看到所需要的數據版本,而globalsnapshot保證了選出一個統一的版本。

 

TPCH性能 - 加速比

我們使用1TB的TPCH進行了測試,首先對比了PolarDB新的分布式並行和單機並行的性能:有3個SQL提速60倍,19個SQL提速10倍以上;

另外,使用分布式執行引擎測,試增加CPU時的性能,可以看到,從16核和128核時性能線性提升; 單看22條SQL,通過該增加CPU,每個條SQL性能線性提升。

TPCH性能 - 和Greenplum的對比

和傳統MPP的Greenplum的對比,同樣使用16個節點,PolarDB的性能是Greenplum的90%。

前面講到我們給PolarDB的分布式引擎做到了彈性擴展,數據不需要充分重分布,當dop=8時,性能是Greenplum的5.6倍。

分布式執行加速索引創建

OLTP業務中會建大量的索引,經分析建索引過程中:80%是在排序和構建索引頁,20%在寫索引頁。通過使用分布式並行來加速排序過程,同時流水化批量寫入。

上述優化能夠使得創建索引有4~5倍的提升。

 

分布式並行執行加速多模 - 時空數據庫

PolarDB是對多模數據庫,支持時空數據。時空數據庫是計算密集型和IO密集型,可以借助分布式執行來加速。我們針對共享存儲開發了掃描共享RTREE索引的功能。

 

  • 數據量:40000萬,500 GB
  • 規格:5個只讀節點,每個節點規格為16 核CPU、128 GB 內存
  • 性能:
    • 隨CPU數目線性提升
    • 共80核CPU時,提升71倍

 

 

總結

本文從架構層面分析了PolarDB的技術要點:

  • 存儲計算分離架構。
  • HTAP架構。

后續文章將具體討論更多的技術細節,比如:如何基於Shared-Storage的查詢優化器,LogIndex如何做到高性能,如何閃回到任意時間點,如何在Shared-Storage上支持MPP,如何和X-Paxos結合構建高可用等等,敬請期待。​

企業級分布式開源數據庫 PolarDB for PostgreSQL-阿里雲開發者社區


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM