一個資料庫儲存架構的獨白

qcloud發表於2018-12-28

本文由雲+社群發表

本文作者:許中清,騰訊雲自研資料庫CynosDB的分散式儲存CynosStore負責人。從事資料庫核心開發、資料庫產品架構和規劃。曾就職於華為,2015年加入騰訊,參與過TBase(PGXZ)、CynosDB等資料庫產品研發。專注於關聯式資料庫、資料庫叢集、新型資料庫架構等領域。目前擔任CynosDB的分散式儲存CynosStore負責人。

企業IT系統遷移到公有云上已然是正在發生的趨勢。資料庫服務,作為公有云上提供的關鍵元件,是企業客戶是否願意將自己執行多年的系統搬到雲上的關鍵考量之一。另一方面,自從System R開始,關聯式資料庫系統已經大約四十年的歷史了。尤其是隨著網際網路的發展,業務對資料庫例項的吞吐量要求越來越高。對於很多業務來說,單個物理機器所能提供的最大吞吐量已經不能滿足業務的高速發展。因此,資料庫叢集是很多IT系統繞不過去的坎。

CynosDB for PostgreSQL是騰訊雲自研的一款雲原生資料庫,其主要核心思想來自於亞馬遜的雲資料庫服務Aurora。這種核心思想就是“基於日誌的儲存”和“儲存計算分離”。同時,CynosDB在架構和工程實現上確實有很多和Aurora不一樣的地方。CynosDB相比傳統的單機資料庫,主要解決如下問題:

存算分離

存算分離是雲資料庫區別於傳統資料庫的主要特點之一,主要是為了1)提升資源利用效率,使用者用多少資源就給多少資源;2)計算節點無狀態更有利於資料庫服務的高可用性和叢集管理(故障恢復、例項遷移)的便利性。

儲存自動擴縮容

傳統關係型資料庫會受到單個物理機器資源的限制,包括單機上儲存空間的限制和計算能力的限制。CynosDB採用分散式儲存來突破單機儲存限制。另外,儲存支援多副本,通過RAFT協議來保證多副本的一致性。

更高的網路利用率

通過基於日誌的儲存設計思路,大幅度降低資料庫執行過程中的網路流量。

更高的吞吐量

傳統的資料庫叢集,面臨的一個關鍵問題是:分散式事務和叢集吞吐量線性擴充套件的矛盾。也就是說,很多資料庫叢集,要麼支援完整的ACID,要麼追求極好的線性擴充套件性,大部分時候魚和熊掌不可兼得。前者比如Oracle RAC,是目前市場上最成熟最完善的資料庫叢集,提供對業務完全透明的資料訪問服務。但是Oracle RAC的線性擴充套件性卻被市場證明還不夠,因此,更多使用者主要用RAC來構建高可用叢集,而不是高擴充套件的叢集。後者比如Proxy+開源DB的資料庫叢集方案,通常能提供很好的線性擴充套件性,但是因為不支援分散式事務,對資料庫使用者存在較大的限制。又或者可以支援分散式事務,但是當跨節點寫入比例很大時,反過來降低了線性擴充套件能力。CynosDB通過採用一寫多讀的方式,利用只讀節點的線性擴充套件來提升整個系統的最大吞吐量,對於絕大部份公有云使用者來說,這就已經足夠了。

儲存自動擴縮容

傳統關係型資料庫會受到單個物理機器資源的限制,包括單機上儲存空間的限制和計算能力的限制。CynosDB採用分散式儲存來突破單機儲存限制。另外,儲存支援多副本,通過RAFT協議來保證多副本的一致性。

更高的網路利用率

通過基於日誌的儲存設計思路,大幅度降低資料庫執行過程中的網路流量。

更高的吞吐量

傳統的資料庫叢集,面臨的一個關鍵問題是:分散式事務和叢集吞吐量線性擴充套件的矛盾。也就是說,很多資料庫叢集,要麼支援完整的ACID,要麼追求極好的線性擴充套件性,大部分時候魚和熊掌不可兼得。前者比如Oracle RAC,是目前市場上最成熟最完善的資料庫叢集,提供對業務完全透明的資料訪問服務。但是Oracle RAC的線性擴充套件性卻被市場證明還不夠,因此,更多使用者主要用RAC來構建高可用叢集,而不是高擴充套件的叢集。後者比如Proxy+開源DB的資料庫叢集方案,通常能提供很好的線性擴充套件性,但是因為不支援分散式事務,對資料庫使用者存在較大的限制。又或者可以支援分散式事務,但是當跨節點寫入比例很大時,反過來降低了線性擴充套件能力。CynosDB通過採用一寫多讀的方式,利用只讀節點的線性擴充套件來提升整個系統的最大吞吐量,對於絕大部份公有云使用者來說,這就已經足夠了。

下圖為CynosDB for PostgreSQL的產品架構圖,CynosDB是一個基於共享儲存、支援一寫多讀的資料庫叢集。

imgCynosDB for PostgreSQL產品架構圖

圖一CynosDB for PostgreSQL產品架構圖

CynosDB基於CynosStore之上,CynosStore是一個分散式儲存,為CynosDB提供堅實的底座。CynosStore由多個Store Node和CynosStore Client組成。CynosStore Client以二進位制包的形式與DB(PostgreSQL)一起編譯,為DB提供訪問介面,以及負責主從DB之間的日誌流傳輸。除此之外,每個Store Node會自動將資料和日誌持續地備份到騰訊雲物件儲存服務COS上,用來實現PITR(即時恢復)功能。

一、CynosStore資料組織形式

CynosStore會為每一個資料庫分配一段儲存空間,我們稱之為Pool,一個資料庫對應一個Pool。資料庫儲存空間的擴縮容是通過Pool的擴縮容來實現的。一個Pool會分成多個Segment Group(SG),每個SG固定大小為10G。我們也把每個SG叫做一個邏輯分片。一個Segment Group(SG)由多個物理的Segment組成,一個Segment對應一個物理副本,多個副本通過RAFT協議來實現一致性。Segment是CynosStore中最小的資料遷移和備份單位。每個SG儲存屬於它的資料以及對這部分資料最近一段時間的寫日誌。

imgCynosStore 資料組織形式

圖二 CynosStore 資料組織形式

圖二中CynosStore一共有3個Store Node,CynosStore中建立了一個Pool,這個Pool由3個SG組成,每個SG有3個副本。CynosStore還有空閒的副本,可以用來給當前Pool擴容,也可以建立另一個Pool,將這空閒的3個Segment組成一個SG並分配個這個新的Pool。

二、基於日誌非同步寫的分散式儲存

傳統的資料通常採用WAL(日誌先寫)來實現事務和故障恢復。這樣做最直觀的好處是1)資料庫down機後可以根據持久化的WAL來恢復資料頁。2)先寫日誌,而不是直接寫資料,可以在資料庫寫操作的關鍵路徑上將隨機IO(寫資料頁)變成順序IO(寫日誌),便於提升資料庫效能。

img基於日誌的儲存

圖三 基於日誌的儲存

圖三(左)極度抽象地描述了傳統資料庫寫資料的過程:每次修改資料的時候,必須保證日誌先持久化之後才可以對資料頁進行持久化。觸發日誌持久化的時機通常有

1)事務提交時,這個事務產生的最大日誌點之前的所有日誌必須持久化之後才能返回給客戶端事務提交成功;

2)當日志快取空間不夠用時,必須持久化之後才能釋放日誌快取空間;

3)當資料頁快取空間不夠用時,必須淘汰部分資料頁來釋放快取空間。比如根據淘汰演算法必須要淘汰髒頁A,那麼最後修改A的日誌點之前的所有日誌必須先持久化,然後才可以持久化A到儲存,最後才能真正從資料快取空間中將A淘汰。

從理論上來說,資料庫只需要持久化日誌就可以了。因為只要擁有從資料庫初始化時刻到當前的所有日誌,資料庫就能恢復出當前任何一個資料頁的內容。也就是說,資料庫只需要寫日誌,而不需要寫資料頁,就能保證資料的完整性和正確性。但是,實際上資料庫實現者不會這麼做,因為1)從頭到尾遍歷日誌恢復出每個資料頁將是非常耗時的;2)全量日誌比資料本身規模要大得多,需要更多的磁碟空間去儲存。

那麼,如果持久化日誌的儲存裝置不僅僅具有儲存能力,還擁有計算能力,能夠自行將日誌重放到最新的頁的話,將會怎麼樣?是的,如果這樣的話,資料庫引擎就沒有必要將資料頁傳遞給儲存了,因為儲存可以自行計算出新頁並持久化。這就是CynosDB“採用基於日誌的儲存”的核心思想。圖三(右)極度抽象地描述了這種思想。圖中計算節點和儲存節點置於不同的物理機,儲存節點除了持久化日誌以外,還具備通過apply日誌生成最新資料頁面的能力。如此一來,計算節點只需要寫日誌到儲存節點即可,而不需要再將資料頁傳遞給儲存節點。

下圖描述了採用基於日誌儲存的CynosStore的結構。

img基於日誌的儲存

圖四 CynosStore:基於日誌的儲存

此圖描述了資料庫引擎如何訪問CynosStore。資料庫引擎通過CynosStore Client來訪問CynosStore。最核心的兩個操作包括1)寫日誌;2)讀資料頁。

資料庫引擎將資料庫日誌傳遞給CynosStore,CynosStore Client負責將資料庫日誌轉換成CynosStore Journal,並且負責將這些併發寫入的Journal進行序列化,最後根據Journal修改的資料頁路由到不同的SG上去,併傳送給SG所屬Store Node。另外,CynosStore Client採用非同步的方式監聽各個Store Node的日誌持久化確認訊息,並將歸併之後的最新的持久化日誌點告訴資料庫引擎。

當資料庫引擎訪問的資料頁在快取中不命中時,需要向CynosStore讀取需要的頁(read block)。read block是同步操作。並且,CynosStore支援一定時間範圍的多版本頁讀取。因為各個Store Node在重放日誌時的步調不能完全做到一致,總會有先有後,因此需要讀請求發起者提供一致性點來保證資料庫引擎所要求的一致性,或者預設情況下由CynosStore用最新的一致性點(讀點)去讀資料頁。另外,在一寫多讀的場景下,只讀資料庫例項也需要用到CynosStore提供的多版本特性。

CynosStore提供兩個層面的訪問介面:一個是塊裝置層面的介面,另一個是基於塊裝置的檔案系統層面的介面。分別叫做CynosBS和CynosFS,他們都採用這種非同步寫日誌、同步讀資料的介面形式。那麼,CynosDB for PostgreSQL,採用基於日誌的儲存,相比一主多從PostgreSQL叢集來說,到底能帶來哪些好處?

1)減少網路流量。首先,只要存算分離就避免不了計算節點向儲存節點傳送資料。如果我們還是使用傳統資料庫+網路硬碟的方式來做存算分離(計算和儲存介質的分離),那麼網路中除了需要傳遞日誌以外,還需要傳遞資料,傳遞資料的大小由併發寫入量、資料庫快取大小、以及checkpoint頻率來決定。以CynosStore作為底座的CynosDB只需要將日誌傳遞給CynosStore就可以了,降低網路流量。

2)更加有利於基於共享儲存的叢集的實現:一個資料庫的多個例項(一寫多讀)訪問同一個Pool。基於日誌寫的CynosStore能夠保證只要DB主節點(讀寫節點)寫入日誌到CynosStore,就能讓從節點(只讀節點)能夠讀到被這部分日誌修改過的資料頁最新版本,而不需要等待主節點通過checkpoint等操作將資料頁持久化到儲存才能讓讀節點見到最新資料頁。這樣能夠大大降低主從資料庫例項之間的延時。不然,從節點需要等待主節點將資料頁持久化之後(checkpoint)才能推進讀點。如果這樣,對於主節點來說,checkpoint的間隔太久的話,就會導致主從延時加大,如果checkpoint間隔太小,又會導致主節點寫資料的網路流量增大。

當然,apply日誌之後的新資料頁的持久化,這部分工作總是要做的,不會憑空消失,只是從資料庫引擎下移到了CynosStore。但是正如前文所述,除了降低不必要的網路流量以外,CynosStore的各個SG是並行來做redo和持久化的。並且一個Pool的SG數量可以按需擴充套件,SG的宿主Store Node可以動態排程,因此可以用非常靈活和高效的方式來完成這部分工作。

三、CynosStore Journal(CSJ)

CynosStore Journal(CSJ)完成類似資料庫日誌的功能,比如PostgreSQL的WAL。CSJ與PostgreSQL WAL不同的地方在於:CSJ擁有自己的日誌格式,與資料庫語義解耦合。PostgreSQL WAL只有PostgreSQL引擎可以生成和解析,也就是說,當其他儲存引擎拿到PostgreSQL WAL片段和這部分片段所修改的基礎頁內容,也沒有辦法恢復出最新的頁內容。CSJ致力於定義一種與各種儲存引擎邏輯無關的日誌格式,便於建立一個通用的基於日誌的分散式儲存系統。CSJ定了5種Journal型別:

1.SetByte:用Journal中的內容覆蓋指定資料頁中、指定偏移位置、指定長度的連續儲存空間。

\2. SetBit:與SetByte類似,不同的是SetBit的最小粒度是Bit,例如PostgreSQL中hitbit資訊,可以轉換成SetBit日誌。

\3. ClearPage:當新分配Page時,需要將其初始化,此時新分配頁的原始內容並不重要,因此不需要將其從物理裝置中讀出來,而僅僅需要用一個全零頁寫入即可,ClearPage就是描述這種修改的日誌型別。

\4. DataMove:有一些寫入操作將頁面中一部分的內容移動到另一個地方,DataMove型別的日誌用來描述這種操作。比如PostgreSQL在Vacuum過程中對Page進行compact操作,此時用DataMove比用SetByte日誌量更小。

\5. UserDefined:資料庫引擎總會有一些操作並不會修改某個具體的頁面內容,但是需要存放在日誌中。比如PostgreSQL的最新的事務id(xid)就是儲存在WAL中,便於資料庫故障恢復時知道從那個xid開始分配。這種型別日誌跟資料庫引擎語義相關,不需要CynosStore去理解,但是又需要日誌將其持久化。UserDefined就是來描述這種日誌的。CynosStore針對這種日誌只負責持久化和提供查詢介面,apply CSJ時會忽略它。

以上5種型別的Journal是儲存最底層的日誌,只要對資料的寫是基於塊/頁的,都可以轉換成這5種日誌來描述。當然,也有一些引擎不太適合轉換成這種最底層的日誌格式,比如基於LSM的儲存引擎。

CSJ的另一個特點是亂序持久化,因為一個Pool的CSJ會路由到多個SG上,並且採用非同步寫入的方式。而每個SG返回的journal ack並不同步,並且相互穿插,因此CynosStore Client還需要將這些ack進行歸併並推進連續CSJ點(VDL)。

img CynosStore日誌路由和亂序ACK

圖五 CynosStore日誌路由和亂序ACK

只要是連續日誌根據資料分片路由,就會有日誌亂序ack的問題,從而必須對日誌ack進行歸併。Aurora有這個機制,CynosDB同樣有。為了便於理解,我們對Journal中的各個關鍵點的命名採用跟Aurora同樣的方式。

這裡需要重點描述的是MTR,MTR是CynosStore提供的原子寫單位,CSJ就是由一個MTR緊挨著一個MTR組成的,任意一個日誌必須屬於一個MTR,一個MTR中的多條日誌很有可能屬於不同的SG。針對PostgreSQL引擎,可以近似理解為:一個XLogRecord對應一個MTR,一個資料庫事務的日誌由一個或者多個MTR組成,多個資料庫併發事務的MTR可以相互穿插。但是CynosStore並不理解和感知資料庫引擎的事務邏輯,而只理解MTR。傳送給CynosStore的讀請求所提供的讀點必須不能在一個MTR的內部某個日誌點。簡而言之,MTR就是CynosStore的事務。

四、故障恢復

當主例項發生故障後,有可能這個主例項上Pool中各個SG持久化的日誌點在全域性範圍內並不連續,或者說有空洞。而這些空洞所對應的日誌內容已經無從得知。比如有3條連續的日誌j1, j2, j3分別路由到三個SG上,分別為sg1, sg2, sg3。在發生故障的那一刻,j1和j3已經成功傳送到sg1和sg3。但是j2還在CynosStore Client所在機器的網路緩衝區中,並且隨著主例項故障而丟失。那麼當新的主例項啟動後,這個Pool上就會有不連續的日誌j1, j3,而j2已經丟失。

當這種故障場景發生後,新啟動的主例項將會根據上次持久化的連續日誌VDL,在每個SG上查詢自從這個VDL之後的所有日誌,並將這些日誌進行歸併,計算出新的連續持久化的日誌號VDL。這就是新的一致性點。新例項通過CynosStore提供的Truncate介面將每個SG上大於VDL的日誌truncate掉,那麼新例項產生的第一條journal將從這個新的VDL的下一條開始。

img故障恢復時日誌恢復過程

圖六:故障恢復時日誌恢復過程

如果圖五剛好是某個資料庫例項故障發生的時間點,當重新啟動一個資料庫讀寫例項之後,圖六就是計算新的一致性點的過程。CynosStore Client會計算得出新的一致性點就是8,並且把大於8的日誌都Truncate掉。也就是把SG2上的9和10truncate掉。下一個產生的日誌將會從9開始。

五、多副本一致性

CynosStore採用Multi-RAFT來實現SG的多副本一致性, CynosStore採用批量和非同步流水線的方式來提升RAFT的吞吐量。我們採用CynosStore自定義的benchmark測得單個SG上日誌持久化的吞吐量為375萬條/每秒。CynosStore benchmark採用非同步寫入日誌的方式測試CynosStore的吞吐量,日誌型別包含SetByte和SetBit兩種,寫日誌執行緒持續不斷地寫入日誌,監聽執行緒負責處理ack回包並推進VDL,然後benchmark測量單位時間內VDL的推進速度。375萬條/秒意味著每秒鐘一個SG持久化375萬條SetByte和SetBit日誌。在一個SG的場景下,CynosStore Client到Store Node的平均網路流量171MB/每秒,這也是一個Leader到一個Follower的網路流量。

六、一寫多讀

CynosDB基於共享儲存CynosStore,提供對同一個Pool上的一寫多讀資料庫例項的支援,以提升資料庫的吞吐量。基於共享儲存的一寫多讀需要解決兩個問題:

\1. 主節點(讀寫節點)如何將對頁的修改通知給從節點(只讀節點)。因為從節點也是有Buffer的,當從節點快取的頁面在主節點中被修改時,從節點需要一種機制來得知這個被修改的訊息,從而在從節點Buffer中更新這個修改或者從CynosStore中重讀這個頁的新版本。

\2. 從節點上的讀請求如何讀到資料庫的一致性的快照。開源PostgreSQL的主備模式中,備機通過利用主機同步過來的快照資訊和事務資訊構造一個快照(活動事務列表)。CynosDB的從節點除了需要資料庫快照(活動事務列表)以外,還需要一個CynosStore的快照(一致性讀點)。因為分片的日誌時並行apply的。

如果一個一寫多讀的共享儲存資料庫叢集的儲存本身不具備日誌重做的能力,主從記憶體頁的同步有兩種備選方案:

第一種備選方案,主從之間只同步日誌。從例項將至少需要保留主例項自從上次checkpoint以來所有產生的日誌,一旦從例項產生cache miss,只能從儲存上讀取上次checkpoint的base頁,並在此基礎上重放日誌快取中自上次checkpoint以來的所有關於這個頁的修改。這種方法的關鍵問題在於如果主例項checkpoint之間的時間間隔太長,或者日誌量太大,會導致從例項在命中率不高的情況下在apply日誌上耗費非常多的時間。甚至,極端場景下,導致從例項對同一個頁會反覆多次apply同一段日誌,除了大幅增大查詢時延,還產生了很多沒必要的CPU開銷,同時也會導致主從之間的延時有可能大幅增加。

第二種備選方案,主例項向從例項提供讀取記憶體緩衝區資料頁的服務,主例項定期將被修改的頁號和日誌同步給從例項。當讀頁時,從例項首先根據主例項同步的被修改的頁號資訊來判斷是1)直接使用從例項自己的記憶體頁,還是2)根據記憶體頁和日誌重放新的記憶體頁,還是3)從主例項拉取最新的記憶體頁,還是4)從儲存讀取頁。這種方法有點類似Oracle RAC的簡化版。這種方案要解決兩個關鍵問題:1)不同的從例項從主例項獲取的頁可能是不同版本,主例項記憶體頁服務有可能需要提供多版本的能力。2)讀記憶體頁服務可能對主例項產生較大負擔,因為除了多個從例項的影響以外,還有一點就是每次主例項中的某個頁哪怕修改很小的一部分內容,從例項如果讀到此頁則必須拉取整頁內容。大致來說,主例項修改越頻繁,從例項拉取也會更頻繁。

相比較來說,CynosStore也需要同步髒頁,但是CynosStore的從例項獲取新頁的方式要靈活的多有兩種選擇1)從日誌重放記憶體頁;2)從StoreNode讀取。從例項對同步髒頁需要的最小資訊僅僅是到底哪些頁被主例項給修改過,主從同步日誌內容是為了讓從例項加速,以及降低Store Node的負擔。

imgCynosDB一寫多讀

圖七 CynosDB一寫多讀

圖七描述了一寫一讀(一主一從)的基本框架,一寫多讀(一主多從)就是一寫一讀的疊加。CynosStore Client(CSClient)執行態區分主從,主CSClient源源不斷地將CynosStore Journal(CSJ)從主例項傳送到從例項,與開源PostgreSQL主備模式不同的是,只要這些連續的日誌到達從例項,不用等到這些日誌全部apply,DB engine就可以讀到這些日誌所修改的最新版本。從而降低主從之間的時延。這裡體現“基於日誌的儲存”的優勢:只要主例項將日誌持久化到Store Node,從例項即可讀到這些日誌所修改的最新版本資料頁。

七、結語

CynosStore是一個完全從零打造、適應雲資料庫的分散式儲存。CynosStore在架構上具備一些天然優勢:1)儲存計算分離,並且把儲存計算的網路流量降到最低; 2)提升資源利用率,降低雲成本,3)更加有利於資料庫例項實現一寫多讀,4)相比一主兩從的傳統RDS叢集具備更高的效能。除此之外,後續我們會在效能、高可用、資源隔離等方面對CynosStore進行進一步的增強。

此文已由作者授權騰訊雲+社群釋出


相關文章