本文由雲+社區發表 本文作者:許中清,騰訊雲自研資料庫CynosDB的分散式存儲CynosStore負責人。從事資料庫內核開發、資料庫產品架構和規劃。曾就職於華為,2015年加入騰訊,參與過TBase(PGXZ)、CynosDB等資料庫產品研發。專註於關係資料庫、資料庫集群、新型資料庫架構等領域。目 ...
本文由雲+社區發表
本文作者:許中清,騰訊雲自研資料庫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是一個基於共用存儲、支持一寫多讀的資料庫集群。
CynosDB 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保存屬於它的數據以及對這部分數據最近一段時間的寫日誌。
CynosStore 數據組織形式
圖二 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(寫日誌),便於提升資料庫性能。
基於日誌的存儲
圖三 基於日誌的存儲
圖三(左)極度抽象地描述了傳統資料庫寫數據的過程:每次修改數據的時候,必須保證日誌先持久化之後才可以對數據頁進行持久化。觸發日誌持久化的時機通常有
1)事務提交時,這個事務產生的最大日誌點之前的所有日誌必須持久化之後才能返回給客戶端事務提交成功;
2)當日誌緩存空間不夠用時,必須持久化之後才能釋放日誌緩存空間;
3)當數據頁緩存空間不夠用時,必須淘汰部分數據頁來釋放緩存空間。比如根據淘汰演算法必須要淘汰臟頁A,那麼最後修改A的日誌點之前的所有日誌必須先持久化,然後才可以持久化A到存儲,最後才能真正從數據緩存空間中將A淘汰。
從理論上來說,資料庫只需要持久化日誌就可以了。因為只要擁有從資料庫初始化時刻到當前的所有日誌,資料庫就能恢復出當前任何一個數據頁的內容。也就是說,資料庫只需要寫日誌,而不需要寫數據頁,就能保證數據的完整性和正確性。但是,實際上資料庫實現者不會這麼做,因為1)從頭到尾遍歷日誌恢復出每個數據頁將是非常耗時的;2)全量日誌比數據本身規模要大得多,需要更多的磁碟空間去存儲。
那麼,如果持久化日誌的存儲設備不僅僅具有存儲能力,還擁有計算能力,能夠自行將日誌重放到最新的頁的話,將會怎麼樣?是的,如果這樣的話,資料庫引擎就沒有必要將數據頁傳遞給存儲了,因為存儲可以自行計算出新頁並持久化。這就是CynosDB“採用基於日誌的存儲”的核心思想。圖三(右)極度抽象地描述了這種思想。圖中計算節點和存儲節點置於不同的物理機,存儲節點除了持久化日誌以外,還具備通過apply日誌生成最新數據頁面的能力。如此一來,計算節點只需要寫日誌到存儲節點即可,而不需要再將數據頁傳遞給存儲節點。
下圖描述了採用基於日誌存儲的CynosStore的結構。
基於日誌的存儲
圖四 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)。
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的下一條開始。
故障恢復時日誌恢復過程
圖六:故障恢復時日誌恢復過程
如果圖五剛好是某個資料庫實例故障發生的時間點,當重新啟動一個資料庫讀寫實例之後,圖六就是計算新的一致性點的過程。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的負擔。
CynosDB一寫多讀
圖七 CynosDB一寫多讀
圖七描述了一寫一讀(一主一從)的基本框架,一寫多讀(一主多從)就是一寫一讀的疊加。CynosStore Client(CSClient)運行態區分主從,主CSClient源源不斷地將CynosStore Journal(CSJ)從主實例發送到從實例,與開源PostgreSQL主備模式不同的是,只要這些連續的日誌到達從實例,不用等到這些日誌全部apply,DB engine就可以讀到這些日誌所修改的最新版本。從而降低主從之間的時延。這裡體現“基於日誌的存儲”的優勢:只要主實例將日誌持久化到Store Node,從實例即可讀到這些日誌所修改的最新版本數據頁。
七、結語
CynosStore是一個完全從零打造、適應雲資料庫的分散式存儲。CynosStore在架構上具備一些天然優勢:1)存儲計算分離,並且把存儲計算的網路流量降到最低; 2)提升資源利用率,降低雲成本,3)更加有利於資料庫實例實現一寫多讀,4)相比一主兩從的傳統RDS集群具備更高的性能。除此之外,後續我們會在性能、高可用、資源隔離等方面對CynosStore進行進一步的增強。
此文已由作者授權騰訊雲+社區發佈