Amazon Aurora 論文筆記


前言

小小的吐槽一下,這篇論文實在是太難讀了,細節太多,需要了解的背景知識不少(2PC,Quorum,數據庫)。即使看了視頻之后,仍然讀的不是很懂,所以我用最笨的方法來讀:按照論文的標題,逐節摘抄、翻譯。我們開始吧。


ABSTRACT

Amazon Aurora 是 Amazon 提供的關系型數據庫服務,用於 OLTP。這篇論文主要講了 Aurora 的架構以及架構設計的出發點:我們相信高吞吐數據處理的瓶頸已經不再是計算和存儲了,而是網絡。因此 Aurora 帶來了船新的架構來解決這個瓶頸,主要的方法是將重做處理下推到一個為 Aurora 專門設計的存儲服務上。這個架構能夠做到:減少網絡流量,故障的快速恢復,Primary 失效時能夠無損啟用 replicas,容錯和自愈的存儲。這篇論文還講了 Aurora 存儲節點保持一致的策略。最后,分享了現在的雲應用對數據庫層的期望需求。


OLTP 中文譯為“聯機事務處理”。OLTP(OnLine Transaction Processing) 和 OLAP(OnLine Analytical Processing) 相對,前者的特點有,增刪查改,ACID,大量用戶,涉及數據量相對小,要求可用性,低延時[1]。后者需要對數據進行分析,需要涉及大量數據,甚至整個數據庫。Aurora 用於 OLTP,所以要求要有高可用、低延時。因此,在看論文的時候,可以多關注可用性和延時。


1. INTRODUCTION

在現代的分布式雲服務中,彈性和可擴展性通過“計算、存儲分離”和“在多個節點復制存儲”來實現。順便說一下,彈性指的是雲端的可用資源能夠隨着用戶的需求而靈活變化[2]。


問題

在分布式環境下,數據庫面臨如下三個問題:

  1. IO 可以在多個節點負載均衡,所以 IO 不再是瓶頸。通過“計算、存儲分離”,數據庫層和存儲層之間的網絡通信成為瓶頸。此外,“在多個節點復制存儲”,將會加劇這一瓶頸。網絡成為新的瓶頸
  2. 數據庫的大部分操作可以並行,但是在某些情況下仍然需要同步操作。cache miss 的時候,線程需要等待硬盤讀取完畢。cache miss 可能還需要彈出頁面,將 dirty cache page(修改過的中間數據)寫入存儲。
  3. 事務的提交是另一個干擾源。一個事務 commit 延遲了,別的事務也會相應延遲。在分布式環境下,使用 multi-phase synchronization 協議(比如 2-phase commit)來提交事務是有挑戰性的。這些協議不能容錯,然而分布式系統環境下錯誤是常見的。

注意 Two-phase commit[3] 和 Two-phase locking[4],前者是一個特殊的分布式共識協議,后者是並發控制模型。2PC 的兩階段為,第一階段為請求 commit 階段,讓所有的參與者執行一個事務,參與者執行過程沒出問題,就返回 yes,否則 no。第二階段,根據參與者返回的情況,決定是否 commit,如果全部都是 yes,那么 commit。不管 commit 還是 abort,都需要再次通知參與者,參與者根據 commit 還是 abort 來處理。2PL 的兩階段為:伸展階段,獲取鎖;收縮階段,釋放鎖。


解決

通過更加充分利用重做日志來解決這個問題。我們提出了一個船新的架構,將計算和存儲分離。每個數據庫實例仍然包含大部分傳統數據庫的組件,比如 query processor, transactions, locking, buffer cache, accsss methods, undo management。將部分函數下移到存儲服務中,比如,redo logging, durable storage, crash recovery, backup/restore。

架構的優點:

  1. 通過構建獨立的、容錯自愈的存儲服務,讓數據庫不受網絡層、存儲層的性能差異和錯誤的影響。
  2. 只寫 redo log 到存儲層,大大減少了網絡通信。
  3. 將數據庫中復雜關鍵的函數,從一次耗時的操作,變成連續異步的操作。因而,我們夠做到故障的快速恢復,對前台處理影響小的備份操作。


2. DURABILITY AT SCALE


2.1 Replication and Correlated Failures

“計算和存儲”分離之后,存儲節點也可能故障,因此復制存儲節點來容錯。Aurora 采用了 Quorum 協議,一般來說 Quorum 采用 3 個節點,讀寫都要得到兩個節點的投票。

我們認為這個配置是不夠的。首先先來講講 AZ(Availability Zone),AZ 是一個 region 的一個部分,和這個 region 的其他 AZ 通過低延時鏈路連接起來,你可以理解為一個 data center 就是一個 AZ。AZ 級別的一些 failures 是不相關的,比如斷電、洪水。在一個 AZ 內部,硬盤或節點的 failures 是會擴散的。(原話: These failures may be spread independently across nodes in each of AZ A, B and C)。AZ 級別的 failures 是 uncorrelated,比如斷電洪水;AZ 內部的 failures 是 correlated,比如硬盤節點的錯誤。在“一個 AZ 出問題了,同時有一個節點故障”的情況下,不再能讀寫,也不能確定第三個節點是否最新數據。一般來說,出現這種故障,我們可以通過復制第三個節點來恢復,但是現在的情況是丟失了兩個節點,我們不能確定第三個節點是否最新,所以 3 個節點這個配置是不夠的。

為此,Aurora 采用了 6 個節點,寫入需要 4 個節點的投票,讀取需要 3 個節點的投票,使用 3 個 AZ,每個 AZ 用兩個節點。有了這樣的配置,我們可以做到:

  1. 一個 AZ 的故障 + 一個節點的故障,還能讀到最新數據,重新復制一個存儲節點就能寫了。
  2. 丟失任意兩個節點,還能寫。

Quorum 機制,這里我們來舉例說明一下 Quorum 的一致性。Quorum 要求讀操作需要讀取 \(V_r\) 個節點,寫操作需要寫入到 \(V_w\) 個節點,集群一共有 \(V\) 個節點。此外還有兩條規則:1, \(V_r + V_w > V\);2, \(V_w > V/2\)。假如現在有六個節點,我們有 \(V=6\)\(V_w = 4\)\(V_r = 3\),初始情況下數據為:\((V_1, V_1, V_1, V_1, V_1, V_1)\)。現在要寫入數據 \(V_2\),我們必須寫入 4 個節點。數據更新為:\((V_2, V_2, V_2, V_2, V_1, V_1)\)。當我們要讀數據的時候,根據抽屜原理和第一條規則,我們一定可以讀到最新的數據,所以讀取 3 個節點之后,不管讀取的數據是什么,總是包含最新數據,讀取后返回最新的即可。第一條規則保證了總是可以讀取到最新的數據[5],加上第二條規則就能保證一致性。此外,Quorum 能夠容錯。如果有兩個節點奔潰了,還有四個節點,因而還是能夠寫入成功的。


2.2 Segmented Storage

為了保證 durability,需要在出現 uncorrelated failures(AZ 級別)的時候,不能同時有兩個節點或硬盤的錯誤。這樣我們就丟失了 4 個節點,因此導致了不能確保讀取到最新數據。節點的 MTTF(Mean Time to Failure),要低於修復節點的 MTTR(Mean Time to Repair),否則將很有可能發生兩個節點都出故障。如果 MTTF 低於 MTTR,那么一個節點故障了,去修理,還沒修理完,有可能別的節點又出現故障。

為了解決這個問題,我們可以考慮降低 MTTF,或者降低 MTTR。但是,MTTF 降到一定程度之后,想要再降低的話是比較困難的,所以我們考慮降低 MTTR。將存儲進行分片,每個分片 10GB,六個分片組成一個 PG(Protection Group)。Aurora 會監視分片是否出現故障,在出現故障的時候,會自動恢復分片。當有一個分片出現故障,可以通過高速網絡快速復制一個分片進行恢復,這樣就大大降低了 MTTR,使到連續兩個節點發生故障的概率大大減少了。


2.3 Operational Advantages of Resilience

我們可以利用這個容錯機制來做一些騷操作。比如,heat management(我的理解是溫度管理),將熱的硬盤和節點標注為故障,這樣容錯機制會自動復制存儲節點。再比如,軟件升級,我們可以逐一標注節點為故障節點,並進行升級。


3. THE LOG IS THE DATABASE


3.1 The Burden of Amplified Writes

一句話總結: 在分布式的配置下,需要將數據發送給其他節點,輸入輸出(IO)變得很多,導致了延時過大。

Aurora 使用 6 個節點做存儲數據,3 個節點做數據庫的實例。任何一個寫操作需要進行的 IO 將被放大,IO 量隨着節點大大增加。先來看看在傳統數據庫下,寫操作需要做的事情;再來看看一個有鏡像的 MySQL 的結構下,寫操作如何放大了 IO。

傳統數據庫

一個寫操作,一般需要將數據頁寫入到對應數據結構(比如B樹)中,同時還要寫入 log。

鏡像 MySQL

看結構圖,我們需要兩個數據庫實例,一個 Primary,一個 Replica,分別放在兩個 AZ。我們將 MySQL 的數據存儲到 EBS 中,這個 EBS 也需要搞一個鏡像。

在這個結構下,一個寫入操作,需要發送的數據由:LOG, BINLOG, DATA, DOUBLE-WRITE, FRMFILES。結構圖中標注有順序和需要發送的內容。Primary 需要等待所有的內容寫入完畢之后,才能回復客戶端,因此大大增加了延遲。此外,需要寫入的數據這么多,對延遲也會有影響。下一節,講 Aurora 如何解決這個問題。


double write:在寫入數據頁之前,先將數據寫入到 doublewrite buffer,確保在發生故障的時候,數據頁能從 doublewrite buffer 中找到。官方文檔的內容為[6],The doublewrite buffer is a storage area where InnoDB writes pages flushed from the buffer pool before writing the pages to their proper positions in the InnoDB data files. If there is an operating system, storage subsystem, or mysqld process crash in the middle of a page write, InnoDB can find a good copy of the page from the doublewrite buffer during crash recovery.


3.2 Offloading Redo Processing to Storage

一句話總結: 將重做處理下移到存儲層。

在傳統的數據庫中,修改一個數據頁,會產生一個 redo log。如果我們保存了前鏡像(before-image),我們在上面應用 redo log,我們可以得到后鏡像(after-image)。對於事務的提交,我們要求先寫 log,數據頁的寫入可以推遲。

在 Aurora 中,我們采用如下的結構。將重做操作下移到存儲層。首先,我們在存儲層里面,在內存中維護一份數據庫的數據,通過不斷應用 redo log,我們可以得到最新的數據。為了避免從頭應用 log,我們將數據頁定期保存到硬盤。(這里 checkpoint 和 materialize 的區別在於,粒度不同。checkpoint 由整個 log 來控制,而 materialize 由一個特定數據頁的 log 來控制)。在結構圖中,采用了 3 個數據庫實例,6 個存儲節點。一個數據庫實例掛了,我們可以啟用實例的副本。Primary 發送 log 到 6 個存儲節點,同時使用 chain replication 發送數據給數據庫副本實例。(鏈式復制如何進行具體看 CRAQ 那個論文)

對比鏡像 MySQL,在 30 分鍾內,Aurora 比鏡像 MySQL 能處理的事務多了 35 倍,並且每個事務的 IO 量少了 7.7 倍。


3.3 Storage Service Design Points

存儲服務的核心設計原則是:盡可能減少前台寫入請求的延時。

將存儲節點上的操作分解成多個步驟的、異步進行的操作,如下圖所示。

  1. 接收 log,加入到隊列中
  2. 保存到硬盤上,並回復數據庫實例。
  3. 整理 log,看看缺了哪些 log
  4. 和其他存儲節點交互,填充丟失的日志
  5. 合並 log,應用到數據頁上。
  6. 定期將 log 和新的數據頁推送到 S3 備份
  7. 垃圾回收,處理舊數據頁
  8. 定期 CRC 檢測,看看數據頁是否損壞。

所有的這些操作都能夠異步進行,並且只有 1 和 2 會影響反應速度,我們可以讓其他操作為 1 和 2 讓步,在系統請求比較繁忙的時候,垃圾回收可以不進行或降低頻率,除非到達了存儲的極限。


4. THE LOG MARCHES FORWARD

在這節,我們描述了 log 是如何產生的,從而保證了持久化狀態、運行時狀態、副本狀態三個狀態總是一致 (durable state, runtime state, replica state)。


4.1 Solution sketch: Asynchronous Processing

每一條 log,我們可以標個號 LSN(Log Sequence Number),LSN 是單調遞增的。有了標號之后,我們可以簡化共識協議,不使用 2PC 這樣的協議。存儲節點可以檢查缺少哪些 log,通過 LSN 向其他存儲節點獲取 log。

在正常情況下,數據庫實例維護的運行時狀態,讓我們可以僅僅讀取一個 segment(因為我們有了 LSN,所以我們可以確定最新的數據);在故障恢復的時候,我們才需要使用 Quorum 協議來讀取 segment 來重建。

接下來,分數據庫層和存儲層來看故障處理。數據庫層中可以有多個未完成的事務,當故障發生的時候,由數據庫層來決定每個事務是否回滾,我們只是將 redo 下移到存儲層,但是我們 undo 還是在數據庫層的。在存儲層恢復的時候,要保證數據庫層看到的是同一個視圖。不管數據庫層決定回滾還是不回滾,存儲層都是要一致的。

借助 LSN,我們可以定義幾種日志點:VCL、CPL、VDL。

在 VCL 前所有的 log 都是有的;CPL 定義為某個特定的點,比如 100、200、300 這些整數節點;VDL 是最大的 CPL。VCL 保證了 log 的完整性,CPL 保證了 log 的一致性。在實踐中, CPL 可以這樣定義。對於數據庫級別的事務,我們可以將它分解為 MTR(mini-transaction),MTR 是有次序的原子操作。每個 MTR 由多個連續的 log 構成,我們可以將一個 MTR 中最后的一個 log 標記為 CPL。

數據庫層通過和多個節點通信,最終確定好 VDL,然后每個存儲節點將 VDL 之后的 log 拋棄,由此存儲層就統一了。


4.2 Normal Operation


4.2.1 Writes

寫入的時候使用 Quorum,當達到要求(比如 4 個節點投票),數據庫實例就推進 VDL 並且將事務標記為 committed。因為有可能數據庫層太快了,所以我們分配 LSN 的時候,給它加一個限制:“LSN < VDL + LAL”,LAL 是 LSN Allocation Limit,給 LSN 分配加一個上限。從而讓存儲層能跟上數據庫層。

注意到,在存儲節點中,我們將數據分成了 segment,如果一個事務太長了,需要橫跨多個 segment。此時,我們可以給 log 加一個 backlink,讓它能夠找到前一個 log,在我們需要補全 log 的時候,可以通過 backlink 來看需要哪些 log。這里有一個 SCL(Segment Complete LSN) 的概念,顧名思義就是到這個 LSN 為止, Segment 有所有的 log。在交換 log 的時候,可以使用 SCL 來確定是缺這個 log,還是有這個 log.


4.2.2 Commits

事務的提交異步完成,客戶端提交一個事務的時候,專門有個線程處理,處理完畢之后,將這個事務放入等待隊列,等待提交。在 VDL 推進的時候,如果在隊列中有事務的 LSN 比當前 VDL 小,那么提交事務。這個事情專門由一個線程完成,提交之后還要回復客戶端已經提交事務。


4.2.3 Reads

Aurora 和傳統的數據庫一樣,從緩存中讀取數據,如果緩存中沒有才讀取硬盤。

當緩存滿了,傳統的數據庫使用頁面置換算法來替換緩存頁,如果緩存頁是一個 dirty page,那么還需要將緩存結果寫回到硬盤上。在 Aurora 中,直接丟棄頁面,根據頁面上的 LSN 決定丟棄哪些,Aurora 保證在緩存中的數據頁總是最新的。[7] 中認為論文中有誤,應該丟棄的是 page-LSN < VDL 的頁面。我認為確實如此,假如我們長期運行 Aurora,如果每次都丟棄新的 page-LSN,那么最終留下來的不就是舊的數據頁了嗎?從而違反了 Aurora 的保證。

數據庫在讀取數據的時候,不用 Quorum 來讀取。在發起讀取的時候,Aurora 將當前的 VDL 作為 read-point,選擇一個擁有 read-point 的存儲節點來讀取數據。因為將數據分段了,Aurora 保存了每個 Segment 的 SCL,所以可以根據 SCL 確定讀取哪一個 Segment,讀取那個 Segment 所在存儲節點。

每一個 PG 可以計算一個最小可讀的 LSN(Minimum Read Point LSN),所有的節點之間通過 gossip 來確定 PGMRPL。存儲節點不允許讀取 LSN 低於 PGMRPL,因此可以推進數據頁的版本,對在 PGMRPL 之前的舊 log 進行垃圾回收。


4.2.4 Replicas

前面我們看到 Aurora 的架構,一個 Primary,兩個 Replica,這些 Replica 除了容錯之外還能做什么呢?答案是作為讀副本。雖然 Replica 不能請求寫入,但是可以請求讀取,這樣可以分擔一些讀請求。為了降低 lag,writer 向存儲節點寫數據的同時,也向 replica 寫數據。replica 接收到 log 的時候,看 log 是否和自己緩存中的數據頁相關,如果相關 apply;如果不相關 discard。之后想要讀取,可以讀取存儲節點。


4.3 Recovery

傳統數據庫中的故障恢復使用檢查點來完成。故障發生的時候,恢復到檢查點,然后先 redo,再 undo。傳統的數據庫恢復的時候,數據庫處於離線狀態。

對於 Aurora,“計算和存儲分離”,在存儲故障恢復的時候,數據庫仍然能在線進行服務。對於數據庫掛掉的情況,需要恢復它的運行時狀態,可以通過 Quorum 讀取到最新的數據來恢復,重新計算 VDL,截取 VDL 之后的日志。


5. PUTTING IT ALL TOGETHER

為了安全使用 Amazon Virtual Private Cloud (VPC,論文里為自家打廣告呢),有三類 VPC: Customer VPC,RDS VPC,Storage VPC。Aurora 數據庫的控制台使用 Amzon Relational Database Service (RDS,又一波廣告) 來提供服務。最后還需要用 Amazon Simple Storage Service (S3,再來一條廣告!) 來做整個系統的備份和恢復。

總結

Performance 和 Lessons 這里就不寫了。總的來說,Aurora 通過“計算和存儲分離”提高了彈性和可擴展性,隨之帶來的網絡瓶頸問題,Aurora 通過將 redo 處理過程下推到存儲節點,不傳數據頁只傳 log 解決了網絡 IO 的問題。論文還討論了在分布式的架構下,數據庫的常規操作以及一致性問題。

參考鏈接

[1] https://database.guide/what-is-oltp/
[2] https://cloud.tencent.com/developer/article/1470169
[3] https://en.wikipedia.org/wiki/Two-phase_commit_protocol
[4] https://en.wikipedia.org/wiki/Two-phase_locking
[5] https://www.cnblogs.com/hapjin/p/5626889.html
[6] https://dev.mysql.com/doc/refman/5.7/en/innodb-doublewrite-buffer.html
[7] https://www.cnblogs.com/cchust/p/7476876.html


免責聲明!

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



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