Aurora是Amazon為雲計算而專門定製的一款關係型數據庫。其目標主要是最小化網絡IO,提升系統的可擴展性與可用性。Aurora的設計哲學是log is database,對數據的更改只寫日誌,也即write-once。Aurora系統設計人員認為傳統的數據庫不論如何擴展都在複製整個系統棧,在不同的層面做耦合;為了更好地適應雲計算,他們認為應該將數據庫系統這個“盒子”打開,在不同的層面進行擴展。Aurora將恢復子系統委託給底層可靠的存儲系統,依賴這個來保障系統服務層級(Service Level Agreement, SLA)。
針對Amazon雲生態環境做了相應優化以後,在某些工作負載下,Aurora的性能可以比MySQL5.7高出10倍以上。下面我們從不同方面深入解讀Aurora的設計理念。
一、前言
關係數據庫系統中,處理事務的過程通常被視為一種分層的行為。系統在頂層對SQL語句進行解析,然後將得到的語法樹傳遞給查詢優化器層。查詢優化器利用啟發式規則和統計信息為每個關係操作符選取最優的策略。這個階段產生的物理執行計劃與邏輯存儲層交互,完成相應的操作。
在本文中,將事務處理引擎簡化為兩層模型:查詢解析、查詢執行以及查詢優化視為查詢處理引擎(Process Engine,PE);邏輯層存儲和物理層存儲統稱存儲引擎(Storage Engine,SE)。這對應MySQL可插拔存儲的兩層架構。
定義數據庫服務器集群的架構決策的關鍵點在於集群共享發生的程度,它定義協調動作發生在什麼層以及哪個層(PE和SE)將被複制或者共享。這不僅確定了系統在可擴展性和靈活性上的權衡,而且關係到每一種架構在現成的數據庫服務器上的適用性。以下是四個具有代表性的架構:
圖1. 不同的數據庫系統架構
Shared Disk Failover (SDF)
集群管理軟件確保DBMS服務器只在連接到共享磁盤上的一個節點上運行,節點之間通常使用存儲區域網絡(SAN)互聯。如果當前活動節點崩潰,強制卸載該服務,並在不同的節點上啟動服務器。標準日誌恢復過程可確保在磁盤上的數據的一致性,因此SDF適用任何DBMS。這種方法的一個變體可以在沒有物理共享磁盤的情況下通過使用諸如DRBD的卷複製器達到相同效果。否則,磁盤冗餘由RAID配置提供。
這種架構專門為解決服務器崩潰問題而設計,它通常部署在一個簡單的兩服務器配置。如在圖1(b)所示,協調僅發生在DBMS外部,確保僅有一個服務器加載共享卷。如果文件安裝在多個節點上,它甚至不能分發只讀負載到備用節點,因為緩存一致性的問題會出現。由於複製是在裸盤上執行,在面對更新的時候,無論是PE或SE都不需要被複制,該架構在系統崩潰的時候會導致短暫的不可用情況發生。
Shared Disk Parallel (SDP)
允許多個節點同時訪問共享存儲需要保證緩存一致。具體地說,每個數據塊的所有權隨著時間的變化而不同,特別是當集群應用觸發一個寫操作。分佈式併發控制機制負責將頁面所有權移交到對應實例。在這過程中,即使頁面變髒,也無需進行I/O操作。讀操作是共享操作,每當應用發送讀請求,數據頁的所有者複製該頁面。任何時候數據塊的寫回操作僅由一個副本進行。
如在圖1(c)所示,協調動作在存儲引擎層內進行。這種體系結構的一個例子是Oracle的RAC,這是基於Oracle並行服務器(OPS)和Cache Fusion技術。
這種架構主要是針對服務器可用的CPU和內存帶寬上進行擴展。它提供與SDF等同的容錯能力,因為大多數服務堆棧在面對更新事務的時候仍然沒有被複制。
Shared Nothing Active(SNA)
通過完全隔離後端服務器,中間件層攔截所有客戶機請求並將其轉發到獨立副本。因為只讀請求在可用節點之間得到平衡從而達到系統可擴展。因此,只有更新事務需要積極地在所有副本進行復制。控制器扮演包裝器的角色。對於請求者而言,它充當服務器,提供相同的客戶端接口。對於原始的服務器而言,該中間層作為客戶端。群集節點之間沒有直接的溝通,因為協調發生在服務器之外,如圖1(d)所示。一個流行的實現是由Sequoia提供的,之前叫做C-JDBC,可以移植到多種後端服務器。
可擴展性上的主要缺點就是更新語句必須是完全確定的,並且需要小心調度以避免衝突從而導致非確定的執行結果和數據庫不一致。 實際上,這通常意味著不允許併發執行更新事務。這個架構的目標主要還是增強服務器在面對大多以讀為主的工作負載能力。通過完全隔離後端服務器,在處理更新事務的時候,它在所有數據庫系統的軟件層重複執行,從而容許PE和SE的故障情況。事實上,可移植的實現,例如Sequoia,甚至支持多樣的DBMS。它甚至可以在崩潰的狀態上進行投票,以掩蓋錯誤的備份。
Shared Nothing Certification-Based (SNCB)
在無共享集群,可以使用基於認證協議避免主動複製更新事務。每個事務直接在副本上執行,而沒有任何先驗協調。因此,事務僅需要根據本地的併發控制協議在本地進行同步。僅在提交之前,協調的過程才開始。這個時候,發起協調的副本採用全序組通信原語廣播更新。這將導致所有節點採用完全相同的更新序列,然後通過測試可能的衝突獲取認證。PostgreSQL-R,Group Replication,Garela等屬於這類架構的範疇。由於協調發生在PE與SE之間如圖1(e),它能夠隨著更新密集型工作負載的擴展而執行更細粒度的同步。這方法能夠接受存儲引擎和磁盤層的物理損壞。
Aurora給人眼前一亮的是它的架構。其體系架構更類似SDP,但是它將更新侷限在一個DB服務器上,避免了分佈式併發控制協議。Aurora設計者們認為,傳統的數據庫實現可擴展不管是做Sharding、還是分佈式、或者共享存儲(Oracle RAC),本質上都是在數據庫的不同層面耦合(SNA在應用層,分佈式是在SQL層,SDP是在緩存層),擴展後的每個實例的程序棧仍然是原來的多層結構。Aurora認為從成本、部署靈活性及可用性等因素考慮,應該考慮把數據庫的各層打開,然後在每個層單獨做擴展。傳統的數據庫系統,例如MySQL、PostgreSQL以及Oracle,將所有的功能模塊封裝成一個整體,而Aurora則是將數據庫的緩衝區管理、恢復子系統從這個整體剝離出來,單獨定製擴展。
Amazon聲稱Aurora完全兼容MySQL,具備商業數據庫的性能與穩定以及開源項目的低成本和易用性。下面詳細介紹Aurora的設計理念。
二、Aurora系統設計
相比傳統的數據庫系統架構,Aurora具有以下三個明顯的優點。
首先,在跨數據中心環境將存儲作為一個獨立的、具有容錯以及自修復能力的服務模塊,使得數據庫系統免受性能抖動和存儲或者網絡故障的干擾。Aurora設計人員觀察到持久層的故障可以認為是系統長時間不可用事件,而系統不可用事件又可以建模為長時間的系統性能抖動。一個設計良好的系統可以無差別地處理這些問題。也就是,Aurora通過底層可靠的存儲系統保證數據庫系統的服務層級(Service Level Agreement, SLA)。
第二,Aurora的設計理念是日誌即數據。通過只將重做日誌記錄寫入存儲層,系統可以將網絡的IOPS減少一個數據量級。一旦移除了網絡I/O瓶頸,在MySQL的代碼基礎上可以針對各種資源競爭進行大量優化,獲得大幅的性能提升。
第三,將數據庫系統中一度被認為最複雜與關鍵的功能(例如重做、備份等)委託給底層的分佈式存儲。存儲系統以異步方式持續在後臺並行構造最新版本的數據。這使得Aurora可以達到即時恢復的效果。
存儲與計算分離,對於Aurora來說,並不是一個選擇題。在Amazon生態下,存儲本來就是和計算分離的,從邏輯上看,可以認為系統有一個巨大的共享存儲(或許使用的共享存儲就是計算節點的本地存儲)。Aurora能夠做的事情,就是儘可能減少計算與存儲之間的帶寬需求,這是整個架構的關鍵所在。
Aurora通過只傳輸重做日誌記錄以消除不必要的IO操作,降低成本,並確保資源可服務於讀/寫流量。與傳統的數據庫引擎不同,Amazon Aurora不會將修改後的數據庫頁面推送到存儲層,進一步節省了IO消耗。
在開始介紹實現細節之前,我們先給出如下術語定義。
表一 Aurora術語定義
術語 | 相應解釋 |
AZ(Availability Zone) | 可用數據中心,Aurora中所有AZ位於同一個region |
LSN(Log Sequence Number) | 日誌序列號,數據庫系統為每個日誌記錄生成的唯一記錄ID。傳統的數據庫系統採用文件偏移量表示,在Aurora中利用時間戳標記。 |
VCL(Volume Complete LSN) | 存儲節點接收到的最大連續日誌ID,這些日誌可能還未提交成功。 |
SCL (Segment Complete LSN) | Aurora中數據分片對應的最大連續日誌ID,節點利用該變量與其它節點交互,填補丟失的日誌記錄 |
CPL (consistency Point LSN) | 在數據庫層面,事務被分成多個MTRs(Min-Transactions)。每個MTR產生的最後一個LSN為一個CPL |
VDL (Volumn Durable LSN) | VDL為最大的CPL,其中CPL ≤ VCL;在系統恢復階段需要通過多數派讀確定VDL |
下面具體介紹Aurora事務的正常讀寫、提交以及恢復過程。
1、Aurora寫流程
圖2. Aurora I/O流程
如圖2所示,系統將數據庫文件切分成大小均等的存儲塊(一個存儲塊的大小為10GB),這些存儲塊分佈在不同的存儲設備上。每個存儲塊都有專屬的redo日誌。更新操作只寫日誌而不寫數據頁。在適當的時機底層的存儲將日誌合併成數據頁。也就是,計算與存儲之間只傳遞日誌,而不傳遞髒頁,頁面的合併由存儲端來完成(不要把存儲端看做是一組單純的硬盤,它也包含了計算節點,可以把他看成是一組磁盤服務器,類似Oracle ExaData),注意到Aurora的存儲格式是基於日誌結構的,所以這是一個整體的設計;系統對每個數據塊複製六次,分散在三個不同的可用區域AZs。Aurora認為寫操作已經持久化,僅當數據(redo日誌)至少寫入六個備份數據的其中四份。Aurora不支持跨region複製。
系統設計人員採用如此數據副本放置策略的原因主要如下:
在大規模的雲計算環境下,底層的磁盤、數據節點以及網絡的故障持續發生。每種故障有不同的持續時間以及波及範圍,例如,可能節點網絡的暫時不可用,重新啟動帶來的短暫的停機,或磁盤、節點、機架、網絡交換機等的永久性故障,甚至是整個數據中心的不可用。在備份系統裡面一個常用的容錯方法是多數派投票協議。複製的數據項的每個副本都關聯一個投票,讀寫分別對應副本數Vr以及Vw。
為了滿足一致性,必須遵循以下兩條規則:
1)為了讀到最新的數據,必須滿足Vr + Vw > V。
這條法則確保寫操作涉及的副本與讀操作涉及的副本有交集,讀操作可以讀到最新版本的數據。
2)為了避免衝突,感知最新的寫入操作,寫操作涉及的副本數Vw 必須滿足 Vw > V/2。
通常的做法是容忍一個節點不能正常工作,設置V=3,讀多數派為Vr = 2,寫多數派為Vw = 2。Aurora的設計人員認為2/3的多數派是不夠的。他們將副本數提升為6個。每個AZ上兩個數據副本。這樣子的話,為了使得讀寫條件成立,那麼Vr = 3,Vw = 4。這樣的配置使得系統可以容忍:
某個機房垮掉,外加一個副本所在的機器不可用(AZ+1)而不會影響讀;
損失兩個副本,即使這個兩個副本位於同一個機房,不會影響寫。
那這樣子的配置是否足夠健壯去容錯呢?
Aurora的設計者認為很難去降低兩個不相關事件的故障概率(兩個副本不可用),於是轉而限制平均修復時間,使得在平均修復時間內發生故障的概率幾乎不可能。他們將數據庫分片限制在10GB大小,在萬兆以太網下修復的時間低於10s,而在這個時間段內,一個機房不可用外加一個數據副本不可用的概率幾乎為0。這就是他們選擇10GB大小的數據分片以及每個分片需要複製6個副本的原因。
在介紹Aurora的更新事務流程之前,我們先看看傳統的數據庫系統的更新步驟。在類似MySQL的系統中需要將髒數據頁寫回堆文件或者b樹等對象中(延遲寫)。此外,還需要將WAL日誌寫入持久層存儲。通過重放WAL日誌可以產生數據頁修改後鏡像。實際上,需要寫回的數據遠不止這些。Aurora設計人員給出了一個MySQL同步鏡像的例子。在這個例子中,寫回的數據除了數據頁、redo日誌還包括binlog、避免數據頁損壞的雙寫文件以及元數據文件。寫回這些數據會帶來的巨大網絡I/O, 還有就是同步這些文件帶來的延遲太大了。
Aurora另闢蹊徑,採用並行寫多個副本保證可靠性,以及利用“log is database”的思想減少傳輸的數據。在Aurora中,一個更新事務的完整操作流程如下:
同時寫多個副本,執行更新操作,但不修改緩衝區中的數據頁,僅僅是構造對應的redo日誌。
存儲節點收到主實例發送的redo日誌,負責持久化工作。
具體來講存儲節點執行如下工作:
存儲節點將主實例發送的redo日誌放入內存隊列,然後將日誌從隊列移出,持久化到磁盤(這個過程是批量操作)。
存儲節點給主節點發送一個ACK,告訴主節點數據持久化過程已經完成。這個步驟完成以後,主實例與存儲節點的交互就已經完成。從Aurora的角度看來,這兩個步驟是執行路徑上的關鍵路徑,影響系統的吞吐量以及相應時間。此後的步驟與主實例的通信可以獨立,異步的方式進行。
一旦存儲節點生成日誌文件,它就立刻開始整理這些日誌記錄以便發現它遺漏了某些日誌記錄。
運行點對點的Gossip協議,將遺漏的日誌記錄補上。(通過Gossip協議,它們可以知道集群中有哪些節點,以及這些節點的狀態如何?每一條Gossip消息上都有一個版本號,節點可以對接收到的消息進行版本比對,確保二者得到的信息相同)。這個階段過後,每個節點上的數據是相同的拷貝。
將日誌記錄合併,生成最新的版本的數據,轉換成數據庫需要的數據塊。
以很高的頻率將生成的數據塊備份到S3。這個Point-in-time快照技術保證故障恢復的時候可以將數據庫恢復到之前特定時間點的一致性狀態。通常有兩種方法保證Point-in-time快照捕捉到最近的更新。一種方法是指針重定位。當最新版本的Point-in-time快照被創建的時候,它維護一個指針指向原來的快照。另外一種方法是增量維護,只是拷貝被更改的數據。
運行垃圾回收機制,清理過時的數據塊與日誌文件。
定期掃描數據塊,進行校驗。如果發現損壞數據塊,與相鄰節點進行通信獲取完好的數據塊。這是Aurora實現可自主修復損壞數據塊的關鍵技術。
以上8個步驟具體見圖3。
圖3 主實例與存儲節點的交互
當主實例收到4個以上的日誌持久化ACK以後就完成事務提交,可以將執行結果返回給客戶端。
從Aurora的寫步驟看來,系統只需要等待寫入存儲節點的日誌返回ACK即可,這是寫操作的唯一需要同步執行的地方。系統將大部分計算下推到底層的存儲層。因為只需寫入redo日誌,Aurora也可以極大地減少網絡IO。從這個角度看來,評價Aurora是一款優秀的專為雲計算而設計的DBMS,一點都不為過。
2、Aurora讀操作
雖然Aurora對寫操作進行了優化,僅僅寫入delta更新(redo日誌);但是系統的讀操作還是以塊為單位。如同傳統的數據庫系統的做法,讀事務首先在緩衝區裡查找所需的數據塊。如果存在,直接讀取即可;否則將請求下發到存儲系統。如果系統緩衝區已滿,那麼需要淘汰一個頁面來裝下新讀入的數據頁。在已有的系統中,如果被淘汰的數據頁是髒頁,那麼需要寫回磁盤。這可以確保隨後的事務都可以讀到最新版本的數據。
上面我們已經介紹了,Aurora並沒有將數據頁寫回存儲系統,只是簡單地將相應內存空間標記為free。但是,Aurora滿足類似的條件:緩衝區裡的數據都是最新的。這個條件的保證通過將緩衝區裡頁面LSN(關聯該頁面最新修改操作的日誌記錄的LSN)大於文件持久化位點VDL的數據頁置換掉。
這個協議確保了:
1)頁面的所有改動都已經持久化到日誌
2)在緩衝沒有該數據頁的情況下,可以構造出當前VDL的頁面。
隨著系統提交事務的不斷確定,VDL會不斷往前推移。最終VDL會大於修改頁面的PageLSN。此時,為了讓讀操作能夠讀到最新的數據,系統有兩種選擇:
其一是,重放操作組件將LSN ≤ VDL的日誌記錄應用到相應的數據頁產生最新版本的數據;
其二是,在讀操作的時候將舊版本數據頁與delta更新(日誌記錄)進行合併(amazon採用何種方式將緩衝區中的頁面變成最新版本的數據並沒有說明,但是Log structure結構的數據組織方式決定了Aurora只能採用上述兩種處理方式)。
系統只有在故障恢復重啟的時候會採用多數派讀的方式來確定系統的VDL。正常情況下,讀操作並沒有採用多數派讀的方法。Aurora給讀操作指派一個讀位點(read point),代表著讀請求產生時刻的VDL。系統維護著對應存儲節點的SCL,知道哪些節點可以滿足當前的讀操作。
3、事務提交
Aurora採用的是異步成組提交技術。在傳統的數據庫中,例如MySQL,為了減少磁盤IO採用成組提交技術。第一個寫日誌緩衝區的線程需要等待預先設定的時間,然後再執行磁盤IO操作;或者等到日誌緩衝區頁寫滿以後再執行IO操作。這樣子做的結果是第一個寫日誌緩衝區的線程需要掛起等待,耗費時間。這是一個同步操作,在持久層存儲的ACK返回之前不能進行其它工作。
在Aurora中,寫操作不僅僅依賴Linux文件系統,還需要跟網絡交互。第一個寫日誌緩衝區的線程可以馬上開始執行IO操作。每個提交事務都無需等待。線程將事務移動到提交列表,寫下該事務的commit LSN,轉而執行其他工作。在某個時刻,後臺進程負責將這些日誌記錄收集起來,批量發送到存儲節點。當主實例收到對應某批次的日誌記錄的4個ACK,VDL向前推移。系統有一個專門的線程不斷檢查提交事務隊列中commit LSN ≤ VDL的事務,然後回覆客戶端。這等價於WAL協議。
在圖3中,提交隊列裡面有3個掛起的提交請求,分別是Pending commit group1,Pending commit group2,以及Pending commit group3。主實例收到針對Pending group commit1的4個以上的日誌持久化ACK以後,將系統VDL前移至22,第一組的狀態從pending變成committed。此時後臺線程檢查提交隊列,然後成批提交LSN小於等於22的事務T1,T2,T3。
注意,即使後面Pending commit group3先於Pending commit group2收集4個以上的存儲節點返回的持久化ACK,那麼也不能移動數據庫持久化位點。因為,這個數據庫持久化位點是Aurora崩潰恢復以後決定開始重做的位點。跳過前面的成組提交的LSN會導致數據庫丟失某些數據。在系統崩潰恢復的時候,系統檢索最新的數據快照與相應的日誌記錄(其LSN大於數據庫持久化位點),即可將數據庫恢復到最新的一致性狀態。
圖4. Aurora成組提交細節
4、恢復
大多數據庫系統的恢復協議採用類ARIES算法,依賴WAL日誌將數據庫系統向前滾動到最新狀態。系統週期性地建立檢查點,將髒頁寫回磁盤,同時將檢查點記錄寫入日誌。系統重啟的時候,頁面遇到丟失提交數據或者包含未提交數據。此時,恢復子系統將最新檢查點以後的redo日誌進行重放,然後利用undo日誌撤銷未提交事務的修改。通過這兩個步驟就可以將數據庫恢復到最新一致性狀態。災難恢復是一個很昂貴的操作,增加系統建立檢查點的頻率可以減少恢復時間。但是,建立檢查點會干擾正常執行的事務。Aurora並不需要做這樣子折中。
Aurora將恢復子系統的功能完全從事務的執行路徑上剝離出來,交給下面的存儲層。存儲節點持續以並行、異步、分佈式的方式進行redo操作無需在性能與恢復時間上進行權衡。圖5給出了傳統DBMS與Aurora的恢復方式對比。
Aurora將數據庫文件切分成10GB大小的塊,每個塊都有專屬的日誌記錄。在崩潰恢復的時候,系統利用多數派讀,確定運行時的一致性狀態。恢復模塊首先確定最大VCL(最大的順序LSN),截斷此後日誌。進一步,可以將需要重放的日誌限制在其LSN ≤ CPL。VDL取最大的CPL,LSN大於VDL的日誌記錄都可以安全地截斷。例如,最大已完成的日誌記錄的LSN為1007(尚未提交),但是系統的CPL為990,1000, 1100。系統可以確定LSN大於1000的日誌記錄都可以忽略。確定完重放日誌的LSN最大值以後,redo操作就可以並行在不同segment上執行了。
根據官方記載,Aurora完成崩潰恢復所耗費的時間大概是60S~120S左右。如果有其它副本存在,用戶可以指定主實例發生故障以後各副本提升為主實例的優先級,不會出現單點故障的情況。對比MySQL,重放操作只是單線程的模式進行,在系統更新負載較大的情況下,MySQL的故障恢復時間通常會比較長。為了及時更新備機上的數據,主實例需要將redo日誌發送備機。備機收到redo日誌以後,查找數據緩衝區。如果存在對應的數據塊,則將根據redo日誌描述的操作應用在該數據塊上(日誌LSN需要滿足小於等於VDL)。否則,簡單地拋棄對應的日誌記錄即可。數據頁的讀入操作要等到對該數據頁的請求到來的時候。
圖5. 傳統DBMS與Aurora的恢復方式對比
三、總結
Aurora的設計讓我想起PBXT DBMS。PBXT作為MySQL的可插拔存儲引擎,定位在支持高併發場景。它沒有采用update-in-place的做法,而是利用append的方式進行更新,這減少了維護高速緩存一致性的開銷。同時,最近的工業界與學術界也都大致認同append更新方式對於Flash比較友好。最主要的是PBXT的設計哲學也是“log is database,write-once”,可以看見Aurora的影子。有興趣的同學可以看看研究下PBXT的源碼,或許更能加深對Aurora的理解。
總而言之,Aurora是針對雲將MySQL進行深層定製的數據庫。Amazon通過緊密集成數據庫引擎和基於SSD的虛擬化存儲層(專為數據庫工作負載而開發),其性能和可用性相較於MySQL有大幅提升。通過降低了存儲系統的寫入次數使之更好的適應雲環境的特點。Aurora在自動拓展存儲容量、自動修復數據、服務宕掉或者重啟時對緩存持久化的處理方式都是很有創新性的。最後是Aurora在RPO,RTO,兼容性以及擴展性方面的總結。
根據amazon官方文檔提供的參數[1],Aurora支持Point-In-Time Recovery將數據庫還原到指定時間點,通常能夠在60s左右的時間完成故障恢復,不會高於120s。在2017年數據庫頂級會議SIGMOD上[2],Amazon公佈了Aurora與MySQL5.6&5.7的性能對比:r3.8xlarge實例規格,sysbench壓測,Aurora的性能要比MySQL5.6與5.7優10倍以上。
表二 Aurora特性描述
RPO | RTO | 兼容性 | 性能 |
支持將數據庫恢復到特定時間點 | 一般小於60s-120s的故障恢復時間 | 完全兼容MySQL5.6 | 在Amazon的r3.8xlarge機型上,性能達到MySQL的10倍以上 |