一、背景
傳統的關系型資料庫有着悠久的曆史,從上世紀60年代開始就已經在航空領域發揮作用。因為其嚴謹的強一緻保證以及通用的關系型資料模型接口,獲得了越來越多的應用,大有一統天下的氣勢。
2000年以後,随着網際網路應用的出現,很多場景下,并不需要傳統關系型資料庫提供的強一緻性以及關系型資料模型。相反,由于快速膨脹和變化的業務場景,對可擴充性(Scalability)以及可靠性(Reliable)更加需要,而這個又正是傳統關系型資料庫的弱點。
自然地,新的适合這種業務特點的資料庫出現,就是我們常說的NoSQL。但是由于缺乏強一緻性及事務支援,很多業務場景被NoSQL拒之門外。同時,缺乏統一的進階的資料模型,通路接口,又讓業務代碼承擔了更多的負擔。資料庫的曆史就這樣經曆了否定之否定,又螺旋上升的過程。而這一次,魚和熊掌我們都要。
PolarDB就是在這種背景下出現的,由阿裡巴巴自主研發的下一代關系型分布式雲原生資料庫。在相容傳統資料庫生态的同時,突破了傳統單機硬體的限制,為使用者提供大容量,高性能,極緻彈性的資料庫服務。
二、核心技術之共享存儲

PolarDB采用了Share Storage的整體架構。采用RDMA高速網絡互連的衆多Chunk Server一起向上層計算節點提供塊裝置服務。一個叢集可以支援一個Primary和多個Secondary節點,分别以讀寫和隻讀的挂載模式通過RDMA挂載在Chunk Server上。PolarDB的計算節點通過libpfs挂載在PolarStores上,資料按照Chunk為機關拆分,再通過本機的PolarSwritch分發到對應的ChunkServer。每個ChunkServer維護一組Chunk副本,并通過ParallelRaft保證副本間的一緻性。PolarCtl則負責維護和更新整個叢集的元資訊。
- Bypass Kernel
PolarDB誕生于2015年,由于RDMA高速網絡的出現,使得網絡帶寬接近于總線帶寬。PoalrDB作出大膽的假設,那就是未來資料庫的瓶頸将由網絡轉向軟體棧自己。是以PolarStore中采用了大量的Bypass Kernel的設計。首先是新硬體的使用,NVME和RDMA的使用,擺脫了IO通路過程中的使用者态核心态互動。
軟體設計中,在綁定CPU,非阻塞IO的模式下, 通過狀态機代替作業系統的線程排程,達到Bypass Kernel的目的。
- ParallelRaft
PolarStore中采用三副本的方式來保證資料的高可用,需要保證副本間的一緻性。工業界有成熟的Raft協定及實作,但Raft由于對可了解的追求,要求順序确認以及順序送出。而副本的确認送出速度會直接影響整個PolarStore的性能。為了獲得更好的通路速度,PolarStore提出了ParallelRaft協定,在Raft協定的架構下,利用塊裝置通路模式中友善判定通路沖突的特點,允許一定程度的亂序确認和亂序送出,如下圖所示:在所有已經确認提案中,那些對前序通路有通路Range沖突的提案會被暫時Block,而沒有沖突的提案會進入Ready狀态并commit,commit以後的提案會繼續回報給目前的Scheduler,之前被Block的提案有可能會進入Ready狀态,進而繼續被送出。
三、核心技術之實體複制
采用了共享存儲的模式之後,Secondary上依然需要從Primary來的複制邏輯來重新整理記憶體結構,如果Buffer Pool以及各種Cache。但是,由于讀寫節點和隻讀節點通路的是同一份資料,傳統的基于binlog的邏輯複制方式不再可用,這時由于邏輯複制由于最終執行順序的變化,導緻主從之間不同的實體資料結構。是以DB層基于Redo Log的實體複制的支援是必不可少的:
不同于邏輯複制自上而下的複制方式,實體複制的複制方式是自下而上的,從共享存儲中讀取并重放REDO,重放過程會直接修改Buffer Pool中的Page,同步B+Tree及事務資訊,更新Secondary上的各種記憶體Cache。除了支援共享存儲外,實體複制還可以減少一份日志寫。同時,由于整個複制過程不需要等到事務送出才能開始,顯著地減少了複制延遲:
四、交易場景優化
針對雙十一峰值交易場景,PolarDB也做了大量優化。
- Blink Tree
在峰值交易場景中,會有大量涉及熱點page的更新及通路,會導緻大量關于這些熱點Page的SMO操作,
之前PolarDB在SMO場景下由于B+Tree實作有如下的加鎖限制:
- 同一時刻整個B+Tree 有且隻能有一個SMO在進行;
- 正在做SMO的B+Tree分支上的讀取操作會被阻塞直到整個smo完成。
針對這個問題PolarDB做了如下優化:
- 通過優化加鎖,支援同一時刻有多個SMO同時進行,這樣原本等待在其他分支做SMO的插入操作就無需等待,進而提高寫入性能;
- 引入Blink Tree來替換B+Tree并通過縮小SMO的加鎖粒度,将原本需要将所有涉及SMO的各層Page加鎖直到整個SMO完成後才釋放的邏輯,優化成Ladder Latch,即逐層加鎖,修改完一層即可放鎖然後去加上一層Page鎖繼續修改。這樣原本被SMO阻塞的讀操作會有機會在SMO中間進來:通過對每個節點增加一個後繼連結的方式,使得在Page Split的中間狀态也可以完成對Page安全的通路,如下圖所示,傳統的B+ Tree必須通過一把鎖來Block整個Page Split過程中對所影響的Page的通路。而Blink Tree則不需要,即使Split還在進行中,父節點到子節點的連結還沒有完成建立,依然可以通過前一個節點的後繼連結找到正确的子節點。并且通過特殊處理確定通路到正确的Page,進而提高讀取性能。
通過這些對B+ Tree的優化,可以實作交易場景PolarDB讀寫性能提升20%。
- Simulated AIO
InnoDB中有simulated AIO的邏輯,用于支援運作在不包含AIO的系統下,PolarDB下的共享存儲檔案系統就是沒有AIO的,是以采用的是simulated AIO的邏輯。
但是原版中的simulated AIO是基于本地存儲設計的,與分布式存儲的特性并不适配。為了進行IO合并,原版的simulated IO設計,将所有異步IO請求按照目标位址進行組織,存放在同一個IO數組中,友善将目标位址連續的小IO合并成大IO來操作,以提升IO的吞吐。
但是這個設計與分布式存儲是不相适配的,連續的大IO操作,會使得同一時刻,隻有一個或少量存儲節點處在服務狀态,浪費了其他存儲節點的作用;另外,分布式存儲的網絡延遲較大,高負載下,網絡中的Inflight IO會較多,IO組中的IO請求數量也會很多,而這種組織方式下,IO數組中的槽位狀态都無序的,往數組中添加IO請求和移除IO請求的開銷都很大。
是以,PolarDB在高負載下的性能比較差且不穩定,為此PolarDB專門對simulated AIO進行了重新的設計,主要包括:
a.合理地選擇IO合并和拆解,充分利分布式存儲的多節點優勢;
b.建立狀态有序的IO服務隊列,減少高負載下的IO服務開銷。
重新設計下,性能提升了很多
穩定性也有了很大的提升
- Partitioned Lock System
PolarDB采用的是2PL + MVCC的并發控制方式。也就是用多版本資料建構Snapshot來服務讀請求,進而避免讀寫之間的通路沖突。而讀寫之間的沖突需要通過兩階段鎖來保證,包括表鎖,記錄鎖,謂詞鎖等。每當需要加鎖的時候,之前的做法都需要去log_sys中先獲得一把全局的mutex保護。在峰值的交易場景中,大量的寫入會導緻這個地方的mutex成為瓶頸。是以PolarDB采取了Partition Lock System的方式,将log_sys改造成由多個LockSysShard組成,每個Shard中都有自己局部的mutex,進而将這個瓶頸打散。尤其是在這種大壓力的寫入場景下明顯的提升寫入性能。
- Lockless Transaction System
PolarDB中支援Snapshot Isolation的隔離級别,通過保留使用的Undo版本資訊來支援對不同版本的記錄的通路,即MVCC。而實作MVCC需要事務系統有能力跟蹤目前Active及已經Commit的事務資訊。在之前的實作中每當有寫事務開始時,需要配置設定一個事務ID,并将這個ID添加到Transaction System中的一個活躍事務清單中。當有讀請求需要通路資料時,會首先配置設定一個ReadView,其中包括目前已配置設定最大的事務ID,以及目前這個活躍事務清單的一個備份。每當讀請求通路資料時,會通過從Index開始的Roll ptr通路到這個記錄所有的曆史版本,通過對比某個曆史版本的事務ID和自己ReadView中的活躍事務清單,可以判斷是不是需要的版本。
然而,這就導緻每當有讀事務開始時,都需要在整個拷貝過程對這個活躍事務清單加鎖,進而阻塞了新的寫事務将自己的ID加入。同樣寫事務和寫事務之間也有通路活躍事務清單的沖突。進而活躍事務清單在這裡變成一個明顯的性能瓶頸,在雙十一這種大壓力的讀寫場景下尤為明顯。
對此,我們将Tansaction System中的這個活躍事務清單改造成無鎖Hash實作,寫事務添加ID以及讀事務拷貝到ReadView都可以并發進行。大大提升了性能。