本文由雲+社區發表算法
本文做者:許中清,騰訊雲自研數據庫CynosDB的分佈式存儲CynosStore負責人。從事數據庫內核開發、數據庫產品架構和規劃。曾就任於華爲,2015年加入騰訊,參與過TBase(PGXZ)、CynosDB等數據庫產品研發。專一於關係數據庫、數據庫集羣、新型數據庫架構等領域。目前擔任CynosDB的分佈式存儲CynosStore負責人。數據庫
企業IT系統遷移到公有云上已然是正在發生的趨勢。數據庫服務,做爲公有云上提供的關鍵組件,是企業客戶是否願意將本身運行多年的系統搬到雲上的關鍵考量之一。另外一方面,自從System R開始,關係數據庫系統已經大約四十年的歷史了。尤爲是隨着互聯網的發展,業務對數據庫實例的吞吐量要求愈來愈高。對於不少業務來講,單個物理機器所能提供的最大吞吐量已經不能知足業務的高速發展。所以,數據庫集羣是不少IT系統繞不過去的坎。緩存
CynosDB for PostgreSQL是騰訊雲自研的一款雲原生數據庫,其主要核心思想來自於亞馬遜的雲數據庫服務Aurora。這種核心思想就是「基於日誌的存儲」和「存儲計算分離」。同時,CynosDB在架構和工程實現上確實有不少和Aurora不同的地方。CynosDB相比傳統的單機數據庫,主要解決以下問題:網絡
存算分離架構
存算分離是雲數據庫區別於傳統數據庫的主要特色之一,主要是爲了1)提高資源利用效率,用戶用多少資源就給多少資源;2)計算節點無狀態更有利於數據庫服務的高可用性和集羣管理(故障恢復、實例遷移)的便利性。併發
存儲自動擴縮容app
傳統關係型數據庫會受到單個物理機器資源的限制,包括單機上存儲空間的限制和計算能力的限制。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基於CynosStore之上,CynosStore是一個分佈式存儲,爲CynosDB提供堅實的底座。CynosStore由多個Store Node和CynosStore Client組成。CynosStore Client以二進制包的形式與DB(PostgreSQL)一塊兒編譯,爲DB提供訪問接口,以及負責主從DB之間的日誌流傳輸。除此以外,每一個Store Node會自動將數據和日誌持續地備份到騰訊雲對象存儲服務COS上,用來實現PITR(即時恢復)功能。
CynosStore會爲每個數據庫分配一段存儲空間,咱們稱之爲Pool,一個數據庫對應一個Pool。數據庫存儲空間的擴縮容是經過Pool的擴縮容來實現的。一個Pool會分紅多個Segment Group(SG),每一個SG固定大小爲10G。咱們也把每一個SG叫作一個邏輯分片。一個Segment Group(SG)由多個物理的Segment組成,一個Segment對應一個物理副本,多個副本經過RAFT協議來實現一致性。Segment是CynosStore中最小的數據遷移和備份單位。每一個SG保存屬於它的數據以及對這部分數據最近一段時間的寫日誌。
圖二 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)完成相似數據庫日誌的功能,好比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
只要是連續日誌根據數據分片路由,就會有日誌亂序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一寫多讀
圖七描述了一寫一讀(一主一從)的基本框架,一寫多讀(一主多從)就是一寫一讀的疊加。CynosStore Client(CSClient)運行態區分主從,主CSClient源源不斷地將CynosStore Journal(CSJ)從主實例發送到從實例,與開源PostgreSQL主備模式不一樣的是,只要這些連續的日誌到達從實例,不用等到這些日誌所有apply,DB engine就能夠讀到這些日誌所修改的最新版本。從而下降主從之間的時延。這裏體現「基於日誌的存儲」的優點:只要主實例將日誌持久化到Store Node,從實例便可讀到這些日誌所修改的最新版本數據頁。
CynosStore是一個徹底從零打造、適應雲數據庫的分佈式存儲。CynosStore在架構上具有一些自然優點:1)存儲計算分離,而且把存儲計算的網絡流量降到最低; 2)提高資源利用率,下降雲成本,3)更加有利於數據庫實例實現一寫多讀,4)相比一主兩從的傳統RDS集羣具有更高的性能。除此以外,後續咱們會在性能、高可用、資源隔離等方面對CynosStore進行進一步的加強。
此文已由做者受權騰訊雲+社區發佈