《數據庫故障恢復機制的前世今生》[1]一文中介紹過,由于磁盤的的順序訪問性能遠好于隨機訪問,數據庫設計中通常都會采用WAL的方式,將隨機訪問的數據庫請求轉換為順序的日志IO,并通過Buffer Pool盡量的合并并推遲真正的數據修改落盤。如果發生故障,可以通過日志的重放恢復故障發生前未刷盤的修改信息。也就是說Log中包含數據庫恢復所需要的全部信息。
現代數據庫為了追求更高的事務并發度,會顯式地區分用戶可見的邏輯內容和維護內部的物理結構,在并發控制上支持了Lock和Latch的分層,同時也在故障恢復上區分了User Transaction和System Transaction。在這種設計下, 保證數據庫D(Durable)的Redo Log需要能在Crash Recovery的過程中,在完全不感知用戶事務的情況下,恢復未提交的System Transaction。因此,Redo Log的設計上天然就是Page Oriented的,也就是說每條Redo Log都被限制在單個Page中,其重放過程不需要感知用戶事務的存在,也不需要關心其他的Page。在《B+樹數據庫故障恢復概述》[2]中詳細的討論過這個過程,也提到這樣做的好處使得在Crash Recovery的過程中Page的恢復過程可以實現充分的并發。到這里我們就可以引出本文想要討論的主要內容:
已知:
特性1(完備):Log中包含數據庫恢復所需要的全部信息;
特性2(Page Oriented):Page通過Log的恢復過程只需要關心當前的Page本身;
那么,通過這兩個特性,數據庫設計能實現哪些實用和有趣的功能呢?
Single Page Recovery
最直接的就是讓Single Page Recovery成為可能,當硬件故障導致某些Page的數據損壞或錯誤時,通常數據庫都是將這類Page異常直接當做介質損壞來處理的,比如需要從最近的備份做還原。這樣會由于多余的,不必要的數據還原導致較長的恢復時間和空間開銷。而Single Page Recovery可以更精準、更快的只恢復損壞的Page。如果再搭配上一些Page Corruption檢測機制,比如磁盤、操作系統、DB對數據的Checksum檢查,甚至是一些自檢的數據結構,就可以實現更可靠的數據庫服務。
為Log建立按Page索引(Log Index)
發現損壞的Page后,下一步就是對該Page的恢復,即Single Page Recovery過程本身。這個時候最主要的是需要有根據Page號查找所有需要的Log的能力。而正常的Log寫入是Append Only的,所有Page的Log會穿插在一起,因此這里需要Log有按照Page的索引存在。比如,可以通過在Log中添加信息維護如下Page的Log鏈,當需要恢復Page時,可以沿著這個Log鏈一路找到所有需要的Log Record:
More Than Single Page Recovery
Single Page Recovery其實是讓DB擁有了:在任何時候,通過較老的Page版本及之后的Log,在線獲得Page的最新版本的能力。 那么,利用和這個能力,DB能做的就不只是單個Page的故障恢復。甚至轉變一下思路,如果通過主動的引入Page故障,并在需要的時候再做這個Page的Recovery是否能獲得更多的東西呢?本文將從這個角度出發介紹幾個DB可能的工作,其中很多其實已經在慢慢變成包括PolarDB在內的現代數據庫的標配。
寫省略(Write Elision):
省略部分Page刷盤操作,主動讓Page成為“Corrupt Page”, 并在需要的使用再通過Single Page Recovery來拿到需要的Page。
快速重啟(Instant Recovery):
當發生System Failure后,DB重啟過程中,跳過需要的Log應用,主動保持這些Page的“Corrupt”狀態,在后續需要訪問的時候再通過Single Page Recovery恢復。
快速還原(Instant Restore):
當發生Media Failure后,DB還原的過程中,跳過Page的修復過程,同樣在后續需要時再恢復。
寫省略(Write Elision)
一次很小的Page修改通常會對應一條很短的Log Record,但卻會導致整個Page在Buffer Pool中變成臟頁,在《庖丁解Innodb之Buffer Pool》[3]中介紹過,Buffer Pool本身大小是受限的,當沒有空閑的Page空間時,為了承載新的請求,就需要通過例如LRU算法來選擇Page換出,如果換出的Page本身是臟頁,就需要先將這個臟頁刷盤,也就是觸發一次Page大小的寫IO。當總數據量遠大于Buffer Pool的場景中,這種現象頻繁的發生:每個Page被換入Buffer Pool后做了很少的修改,又很快因為被換出而刷盤。很小的修改導致很大的寫IO,而IO的帶寬資源又是很寶貴的,這種場景中很容易就變成了整個DB的性能瓶頸,這也就是所謂的IO-Bound場景。
Single Page Recovery給了這種場景一種新的選擇,當臟頁被換出時,直接跳過刷臟過程,從而完全避免了Page大小的IO。當下次該Page再次被換入時,這個Page會被看做是一個“Corrupt Page”,通過Log的按Page索引找到需要的Log Record,并完成應用。這種實現由于避免了大量的Page IO,可以顯著的提升這種IO-Bound場景下的DB性能。這種實現中,可以在寫入過程中,在內存中維護Log的按Page索引。
快速重啟(Instant Recovery)
數據庫發生故障或運維操作需要重啟時,中斷服務的時間直接影響數據庫的可用性,因此這個階段是希望盡可能短暫的。《數據庫故障恢復機制的前世今生》[1]中介紹過,ARIES實現的數據庫在恢復過程中會經歷Log Analysis、Redo Phase以及Undo Phase三個階段,其中回滾未提交事務的Undo階段可以在數據庫提供服務之后,在后臺進行。
因此主要影響不可服務時間的,就是Log Analysis階段及Redo Phase階段,其中應用Log恢復Page的Redo Phase的時間占比又顯著高于僅僅掃描Redo的Log Analysis階段。實踐中,Redo Phase的時間可能因為Active Redo的量及Buffer Pool的大小限制變得不可接受。Redo Phase主要的任務是要將所有的未刷盤的Page通過重放Redo恢復到最新的狀態,如果我們暫時接受這種未恢復Page的“Corrupt”狀態,并利用Single Page Recovery的能力,在需要的時候再在后臺完成,那么,我們就可以將數據庫提供服務的時間提前到Redo Phase開始之前,如下圖所示:
重啟階段
為了實現快速重啟,在ARIES的基礎上,Log Analysis階段需要額外維護一些必要的信息,主要包括Register Pages及Log Index。其中Register Pages中記錄所有Checkpoint之后的Active Redo所涉及到的Page,這些是所有需要恢復的Page。DB提供服務后,在后臺異步完成Redo Phase之前,如果有用戶請求訪問到Register Pages中的Page才需要觸發Single Page Recovery的恢復流程,并在Page恢復完成后從Register Pages中刪除。另一個需要的信息就是Single Page Recovery過程中需要的Log index,參考ARIES中為Undo Phase維護的Per-Transaction Log鏈,這里也可以維護出Per-Page Log鏈,如下圖所示:
需要做Recovery的Page可以沿著這個鏈表,一路找到當前Page LSN的位置或者找到Page Initial位置為止,并順序應用所有需要的Page。
后臺Redo Phase
DB完成Log Analysis并開始提供服務之后,后臺的Redo Phase會比之前同步的Redo Phase有更多的選擇。比如,是立刻恢復所有的Resister Pages,還是等待這些Page真正被用戶請求使用時再恢復,亦或是二者結合。又比如,是按照Log中Page的排列順序恢復,還是按照Page的某種優先級恢復(大/小事務優先、大/小表優先,或者某種用戶定義的優先級),亦或是多種策略相結合。同時后臺Redo Phase過程由于Page恢復之間相互獨立,也天然更容易實現并發。因此,更高的靈活度和可能更高的并發度,也是Instant Recovery除了快速恢復服務之外新增優勢。
不過需要注意的是,由于在恢復同時接受用戶新的請求,完整的恢復過程可能會拉長,而在這個過程中,用戶請求的性能也是會有下降的,如下圖所示是一個傳統ARIES Restart和Instant Restart的DB可用性及性能效果:
快速還原(Instant Restore)
備份還原通常是數據庫應對磁盤故障的保底手段。為了實現這一點,正常運行過程中,數據庫就需要周期性的對數據和日志進行備份,權衡恢復時間和對正常運行的影響,其中數據備份又包括全量備份和增量備份。當遇到磁盤故障需要做備份還原時,會先從全量備份和增量備份在新的磁盤上還原一份數據,之后應用備份時間點之后的日志,全部完成后切換這個新的數據庫實例提供服務。整個過程如下圖所示:
可以看出,備份還原有非常長的周期,包括拷貝全量備份即增量備份的Full Restore及Incremental Restore階段,以及跟重啟類似的Log Analysis、Redo Phase和Undo Pass階段,其恢復時間跟數據及日志總量相關,并受網絡帶寬及磁盤IO的限制。因此,盡可能讓DB提前提供服務是非常必要的,類似于上面講的快速重啟,ARIES通過事務Lock的方式,讓數據庫可以在Undo Phase階段完成前就提供服務。同樣的,我們可以通過暫時接受“Corrupt Page”,將真正的Page Recovery推遲到需要的時候,從而將DB整體提供服務的時間提前,如下圖所示:
我們甚至可以將這個時間點提前到備份還原的一開始。這個時候,新的磁盤上甚至還沒有任何Page數據,當一個Page被訪問的時候,會先從最近的全量及增量備份中去找到該Page的歷史版本,再從Log備份中找到這個Page之后的所有Log完成應用。因此,能夠快速找到需要Page對應的備份數據位置及需要的增量Logs非常重要,也就是在正常的備份過程中,為備份和Log維護按Page的索引。
為數據備份及Log備份建立按Page的索引
正常數據庫寫入過程中會以Append Only的方式寫WAL Log,為可能發生的Crash做Recovery準備,這里稱為Recovery Log。隨著Checkpoint的推進,陳舊的Recovery Log不再被Recovery需要,可以被清理。但為了可能的備份還原,這部分Log還需要被保留,可能是在成本更低的存儲介質上,這部分Log這里稱為Archive Log。在轉存Archive Log的過程中,便可以為Archive Log建立按Page索引。最理想的情況是需要還原的時候,有全局的Archive Log索引,但因為Log本身流式產生的特性,這樣顯然是不可能的。因此,分區排序的Archive Log就成為非常好的選擇,如下圖所示:
后臺的Archive任務會在內存中對順序生成的Recovery Log中的一段按Page排序,這部分內存超過某個大小時,已經排序的部分會落盤生成一個按Page號有序的Archive Log分片。重復這個過程就有了許許多多的排序分區。根據分片號及Page號就可以訪問到需要Page的所有Archive Log。除了Log以外,為了支持Instant Restore的按需Page恢復,全量備份及增量備份也需要按照Page號建立索引。
最終,如上圖所示,無論是數據備份還是日志部分都存在一個索引方式可以方便的按照Page號查找。當一個Page需要真正做Restore時,就可以利用這些索引快速找到其對應的備份Page版本及后續Log,完成真正的Page重建。
后臺Restore過程
類似于快速重啟,實踐上通常也會搭配一個后臺運行的Restore任務,即使沒有用戶請求訪問,所有的Page也會在一個有限的時間窗口全部完成Restore。下圖展示的就是從全量備份,增量備份和日志備份中不斷還原Page到目標新磁盤的過程。
有了上面所說的數據備份及日志備份的按Page索引,后臺的這個Restore過程也可以拋棄之前按Log順序進行的限制,有了更多的選擇和靈活性。舉個很實用的例子,Backup相對于最新的DB位置,受Backup周期及寫入壓力的影響,中間的日志量可能非常多,而這些日志通常又會反復修改同一個Page,按照Log順序的還原策略,會導致同一個Page可能會不斷的讀寫,造成很大的IO浪費,也因此受到IO帶寬限制。而采用Single Page Recovery方式的后臺Restore過程,天然的可以按照Page的順序進行還原,每個Page的一次讀寫都可以完成全部的日志應用,這樣就可能很大程度的提升Page的IO效率和還原的速度。
應用 - Aurora
Aurora作為共享存儲數據庫的佼佼者,其設計和實現中大量的利用了日志即數據庫的思路。Aurora認為計算節點與存儲節點分離后,整個DB系統的IO瓶頸會轉移到計算節點與存儲節點及存儲節點副本間的網絡上,為此Aurora的設計中,計算節點和存儲節點,及存儲節點之間只傳遞日志,如下圖所示:
Aurora也把這種設計設計稱為日志即數據(The Log is The Database)[4] 。這種實現會在日志量遠小于Page修改量的場景下非常的有效,可以看出網絡上傳遞的只有Log而沒有任何Page數據。每個存儲節點在收到流式的Log之后,會獨立完成一遍Page的修改過程。下圖所示的是其存儲節點的工作流程:
存儲節點不斷的從計算節點接受Log寫入并持久化90(1),完成日志在Update Queue中的持久化,并返回計算節點確認(2),同其他存儲節點通信補全Log(3),當某些Page收到的連續日志量超過某個閾值,或者整體的Update Queue中的日志量達到某個閾值后,這些Page會在后臺周期性的應用Page的修改并寫入Data Pages,這個過程稱為日志到Page的合并(COALESCE)(5)。最后歷史的日志和數據會備份到S3為后續可能的按時間點還原做準備。
寫省略(Write Elision)
可以看出,當某個Page的收到的日志量比較少的情況下,Aurora的存儲節點并不基于將其真正的修改到Data Pages中,這個時候,如果計算節點需要讀這個Page,存儲節點會應用Update Queue中的Log到需要的Page上并返回用戶。這個熟悉的過程其實就是本文提到的寫省略(WRITE ELISION)。理想情況下,一個Page的多次修改對應的Log會一直積累在Update Queue中而沒有真正產生Page,直到足夠的Log量觸發Page 的COALESCE。
快速重啟(Instant Recovery)
由于Aurora的計算節點不維護Log也不負責Page的更新,其重啟過程可以非常迅速,不需要傳統數據庫的Log Analysis,Redo Phase及Undo Phase,而這部分需求其實是下放到了存儲節點上的。Aurora存儲節點的重啟恢復過程,第一步要確定VDL or the Volume Durable LSN[5],這個位置可以認為對應單機數據庫的有效Log結尾,之后這個存儲節點就可以恢復向計算節點提供服務。這其實就是天然的Instant Restart實現,真正的Page Apply被推遲到用戶請求,或收到更多的Log出發Page的Coalesce。
快速還原(Instant Restore)
云數據庫通常都會會提供諸如按時間點還原的功能,來還原一個新的指定歷史時間點的實例。這個過程其實就是典型的備份還原場景,需要結合歷史的Page版本和后續增量Log完成。除此之外,Aurora還提供了Backtrack的能力,可以讓當前實例分鐘級的快速還原到Backtrack Window內的任意時間點,并且這個動作還可以向前向后反復重復執行。這部分的具體實現細節沒有太多的公開資料,不過測試中Aurora在這些功能良好的表現以及合理的實現推測,也讓我們相信其受益于其日志即數據庫的設計。
總結
本文從AIRES及現代數據庫的邏輯、物理分層實現,推導出數據庫的Redo Log會具有,完備及Page Oriented兩個特性,有了這兩個特性,就可以很好的支持精準的Single Page Recovery。而更廣泛一點的是讓數據庫擁有了:按需在線恢復單個Page的能力。而利用這個能力并主動引入”Page Corrupt”就可以實現更廣義上的Single Page Recovery。本文從 寫省略(Write Elision)、快速重啟(Instant Recovery)以及快速還原(Instant Restore)三個方向介紹了利用Single Page Recovery可以讓數據庫獲得的如性能提升、可用性提高、靈活性提高等優勢。最后,通過介紹Amazon的共享存儲數據庫Aurora是如何在設計中應用這些優化的。最后的最后,同樣作為共享存儲數據庫,后起之秀PolarDB與他的前輩Aurora有很多相似的地方,但也有著大不相同的硬件環境和架構設計,后續有機會會詳細介紹PolarDB中是如何利用Single Page Recovery能力獲得這些優勢的。
審核編輯:湯梓紅
-
內核
+關注
關注
3文章
1401瀏覽量
40855 -
數據庫
+關注
關注
7文章
3875瀏覽量
65430 -
日志
+關注
關注
0文章
140瀏覽量
10779
原文標題:參考
文章出處:【微信號:inf_storage,微信公眾號:數據庫和存儲】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
提高Oracle的數據庫性能
基于數據庫日志復制和故障恢復

數據庫教程之如何進行數據庫設計

數據庫教程之SQL Server數據庫管理的詳細資料說明

盡可能限制NVM寫操作的數據庫日志方案NVRC

華為云數據庫穩定可靠-即開即用
python讀取數據庫數據 python查詢數據庫 python數據庫連接
SQL Server數據庫備份方法
Oracle數據恢復—異常斷電后Oracle數據庫啟庫報錯的數據恢復案例

評論