一文看懂金融級分布式數(shù)據(jù)庫架構(gòu)設計
原創(chuàng)【51CTO.com原創(chuàng)稿件】行業(yè)背景
銀行業(yè)從最初的手工記賬到會計電算化,到金融電子化,再到現(xiàn)在的金融科技,可以看到金融與科技的結(jié)合越來越緊密,人工智能、大數(shù)據(jù)、物聯(lián)網(wǎng)、區(qū)塊鏈等新興技術(shù)改變了金融的交易方式,為金融行業(yè)的創(chuàng)新前行提供了源源不斷的動力。同時互聯(lián)網(wǎng)金融的興起是一把雙刃劍,帶來了機遇的同時也帶來了挑戰(zhàn)。
普惠金融使得金融的門檻降低,更多的普通大眾參與到金融活動中,這讓金融信息系統(tǒng)承受了越來越大的壓力。于是我們可以看到大型商業(yè)銀行、保險公司、證券公司、交易所等核心交易系統(tǒng)都在紛紛進行分布式改造,其中數(shù)據(jù)庫作為有狀態(tài)的應用,成為了信息系統(tǒng)中唯一的單點,承擔了所有來自上層應用的壓力。隨著數(shù)據(jù)庫瓶頸的凸顯,進行分布式改造迫在眉睫。
數(shù)據(jù)庫分布式改造的途徑
數(shù)據(jù)庫進行分布式改造主要有三種途徑:分布式訪問客戶端、分布式訪問中間件、分布式數(shù)據(jù)庫。由于其分布式能力實現(xiàn)在不同的層次(應用層、中間層、數(shù)據(jù)庫層),對應用程序有不同的侵入程度,其中分布式訪問客戶端對應用侵入性最大,改造難度最大,而分布式數(shù)據(jù)庫方案對應用侵入性最小,但是架構(gòu)設計及研發(fā)難度最大。
分布式數(shù)據(jù)庫總體架構(gòu)
其實當前市面上的分布式數(shù)據(jù)庫總體架構(gòu)都是類似的,由必不可少的三個組件組成:接入節(jié)點、數(shù)據(jù)節(jié)點、全局事務管理器??傮w架構(gòu)如下,接入節(jié)點負責sql解析,生成分布式執(zhí)行計劃,sql轉(zhuǎn)發(fā),數(shù)據(jù)匯總等;數(shù)據(jù)節(jié)點負責數(shù)據(jù)存儲與運算;全局事務管理器負責全局事務號的生成,保證事務的全局一致性。這個架構(gòu)或多或少都受到了Google Spanner F1論文的影響,這篇文章主要分析了這幾個組件在實現(xiàn)上有什么難點,該如何進行架構(gòu)設計。
兩階段提交的問題
我們知道兩階段提交是阻塞性協(xié)議,這也是它最大的問題。下圖以pgxc架構(gòu)下的兩階段提交為例,主要分為幾個階段:
①:CN prepare ->②:所有DN prepare ->③:CN commit->④:所有DN commit
試想一下如果在cn commit階段發(fā)生cn/dn宕機會發(fā)生什么?
如果在cn下發(fā)完cn commit命令后宕機,這時dn收到commit命令后會進行提交,但是返回commit ok時發(fā)生cn宕機,事務進入阻塞狀態(tài)。如果cn下發(fā)commit之后某個dn發(fā)生宕機,則會造成某些dn commit成功,某些dn commit失敗,造成不一致,但是如果dn重新啟動后會繼續(xù)去cn上拿事務提交信息,發(fā)現(xiàn)是commit狀態(tài),則會繼續(xù)執(zhí)行commit操作,提交之前的事務。
在這個地方我們可以探討一個更極端的情況,如果此時cn也宕機了,那么失敗的dn重啟后去cn拿狀態(tài)發(fā)現(xiàn)拿不到,這時這個失敗dn上的事務就處于一個未決態(tài),不知道是應該提交還是回滾,這時候應該有一個進程能夠從其他dn上發(fā)現(xiàn)該狀態(tài)并報告給故障dn,通知它進行提交。這個角色就是pgxc_clean進程,其實之前幾種情況下的事務的回滾也是該進程的工作。那我們再深入一下,如果該dn是事務的唯一參與者,那么此時pgxc_clean就無法從其他dn以及cn獲取狀態(tài),這時該dn就是真正的未決態(tài)了。
為了解決兩階段提交的阻塞問題,出現(xiàn)了三階段提交,三階段提交在commit之前引入了cancommit的過程,同時加入超時機制。因為如果協(xié)調(diào)者發(fā)生宕機,參與者無法得知協(xié)調(diào)者到底發(fā)出的是commit還是abort,三階段提交cancommit過程就是告知參與者我發(fā)送的是commit或者abort命令,這時如果協(xié)調(diào)者發(fā)生失敗,參與者等待超時時間后可以選出新的協(xié)調(diào)者,而該協(xié)調(diào)者是知道應該發(fā)出什么命令。
雖然三階段提交解決了阻塞問題,但是無法解決性能問題,分布式系統(tǒng)中為了保證事務一致性需要跟每個參與者通信,一個事務的提交和參與需要分布式系統(tǒng)中每個節(jié)點的參與,必然帶來延時,不過在萬兆、infiniband、roce高速網(wǎng)絡的支持下已經(jīng)不再是問題了。
CAP與BASE的抉擇
我們知道分布式系統(tǒng)無法戰(zhàn)勝CAP。那么在設計分布式系統(tǒng)的時候該如何進行取舍?首先P(分區(qū)容錯性)是必須保證的,因為分布式系統(tǒng)必然是多個節(jié)點(分區(qū))通過網(wǎng)絡進行互聯(lián),而網(wǎng)絡是不可靠的,分布式系統(tǒng)是為了避免單點故障,如果因為網(wǎng)絡問題或者某些節(jié)點失敗造成整體系統(tǒng)不可用,那么也不符合分布式系統(tǒng)的設計初衷。如果保證A(可用性),那么當網(wǎng)絡失敗時,網(wǎng)絡隔離的不同區(qū)域就要繼續(xù)提供服務,那么就會造成不同分區(qū)的數(shù)據(jù)不一致(腦裂);如果保證C(一致性),那么網(wǎng)絡失敗時,就需要等待不同網(wǎng)絡分區(qū)的節(jié)點同步完數(shù)據(jù),如果網(wǎng)絡一直失敗,那么系統(tǒng)就會因為無法同步而一直不可用。
2PC就是典型的犧牲可用性保證一致性的例子,而BASE(basically available,soft state,eventual consistency)就是犧牲一致性保證可用性的例子,因為做到實時的強一致要犧牲的代價太大了,它允許數(shù)據(jù)在某些時間窗口內(nèi)的不一致,通過記錄窗口內(nèi)的每一個臨時狀態(tài)日志做到在系統(tǒng)故障時,通過日志繼續(xù)完成未完成的工作或者取消已經(jīng)完成的工作回退到初始狀態(tài),這種方式保證了最終一致性。BASE與傳統(tǒng)ACID理論其實是背離的,滿足BASE理論的事務也叫柔性事務,在遭遇失敗時需要有相應的補償機制,與業(yè)務耦合性較高,其實我并不是很贊同BASE的做法,因為它已經(jīng)背離了數(shù)據(jù)庫最基本的設計理念。
raft的優(yōu)勢
不管是上面的XA還是BASE都無法徹底解決一致性問題,真正意義上的強一致一定是基于強一致協(xié)議的。paxos和raft是目前主流的兩種共識算法。Paxos誕生于學院派,是分布式環(huán)境下基于消息傳遞的共識算法,它設計之初是考慮一個通用的模型,并沒有過多的考慮實際的應用,而且paxos考慮了多個節(jié)點同時寫入的情況,這就使得paxos的狀態(tài)機異常復雜,所以難以理解,不同的人可能理解出不同的意思,這一點一直遭人詬病,比如MGR引入write set的概念來處理多點寫入沖突的問題,這在高并發(fā)熱點數(shù)據(jù)的場景下是不可接受的。因為paxos的難以理解,斯坦福的兩名大學生設計了raft算法,相比來說,raft是工業(yè)派,同一時刻leader只有一個,follower通過日志復制實現(xiàn)一致性,相比paxos來說raft的狀態(tài)機更加簡單易懂,實現(xiàn)起來也更加簡單,因此在分布式環(huán)境上有著廣泛的應用,例如TiDB、RadonDB、etcd、kubernetes等。
Raft協(xié)議將共識問題分解為三個子問題分別解決:leader選舉、日志復制、安全性。
Leader選舉:
服務器節(jié)點有三種狀態(tài):領(lǐng)導者、跟隨者和候選者。正常情況下,系統(tǒng)中只有一個領(lǐng)導者,其他的節(jié)點全部都是跟隨者,領(lǐng)導者處理全部客戶端請求,跟隨者不會主動發(fā)送任何請求,只是簡單的響應來自領(lǐng)導者或者候選者的請求。如果跟隨者接收不到消息(選舉超時),那么他就會變成候選者并發(fā)起一次選舉。獲得集群中大多數(shù)選票的候選者將成為領(lǐng)導者,領(lǐng)導者一直都會是領(lǐng)導者直到自己宕機了。
Raft 算法把時間分割成任意長度的任期(term),每一段任期從一次選舉開始,一個或者多個候選者嘗試成為領(lǐng)導者。如果一個候選者贏得選舉,然后他就在這個的任期內(nèi)充當領(lǐng)導者。要開始一次選舉過程,跟隨者先要增加自己的當前任期號并且轉(zhuǎn)換到候選者狀態(tài),然后他會并行的向集群中的其他服務器節(jié)點發(fā)送請求投票的 RPCs 來給自己投票,候選者會繼續(xù)保持著當前狀態(tài)直到以下三件事情之一發(fā)生:(a) 他贏得了這次的選舉,(b) 其他服務器成為領(lǐng)導者,(c) 沒有任何一個候選者贏得選舉。當一個候選者獲得了集群大多數(shù)節(jié)點針對同一個任期號的選票,那么他就贏得了選舉并成為領(lǐng)導者。然后他會向其他的服務器發(fā)送心跳消息來建立自己的權(quán)威并且阻止新的領(lǐng)導人的產(chǎn)生。下圖為三種角色的轉(zhuǎn)換狀態(tài)機。
日志復制:
當leader被選舉出來,他就作為服務器處理客戶端請求。客戶端的每一個請求都被看成復制狀態(tài)機所需要執(zhí)行的指令。領(lǐng)導者把這條指令作為一條新的日志條目附加到日志中去,然后并行的發(fā)起附加條目 RPCs 給其他的服務器,讓他們復制這條日志條目。當這條日志條目被安全的復制,領(lǐng)導者會應用這條日志條目到它的狀態(tài)機中然后把執(zhí)行的結(jié)果返回給客戶端。如果跟隨者崩潰或者網(wǎng)絡丟包,領(lǐng)導者會不斷的重復嘗試附加日志條目 RPCs (盡管已經(jīng)回復了客戶端)直到所有的跟隨者都最終存儲了所有的日志條目。下圖為復制狀態(tài)機模型。
安全性:
安全性指的是每臺復制狀態(tài)機都需要按照同樣的順序執(zhí)行相同的指令,以保證每臺服務器數(shù)據(jù)的一致性。假想一臺跟隨者在某段時間處于不可用狀態(tài),后來可能被選為領(lǐng)導者,這時就會造成之前的日志被覆蓋。Raft算法通過在leader選舉時增加一些限制來避免這個問題,這一限制保證所有領(lǐng)導者對于給定的任期號,都擁有了之前任期的所有被提交的日志條目。日志條目只會從領(lǐng)導者傳給跟隨者,不會出現(xiàn)因為新領(lǐng)導者缺日志而需要跟隨者向領(lǐng)導者傳日志的情況,并且領(lǐng)導者從不會覆蓋本地日志中已經(jīng)存在的條目。Raft 算法使得在投票時投票者拒絕掉那些日志沒有自己新的投票請求,從而阻止該候選者贏得選票。
CN的設計
接入節(jié)點的設計可能看起來很簡單,但是里面有些地方內(nèi)容還是有些玄機的。設計cn需要重點考量的地方主要是cn到底是做重還是做輕。這是把雙刃劍,主要有下面兩方面問題。
1.如何做到sql語法兼容性?
接入節(jié)點主要負責sql的解析、執(zhí)行計劃的生成與下發(fā),這些東西其實是sql解析器做的事情,我們可以直接將MySQL或者pg的解析器甚至server層拿過來做sql解析和執(zhí)行計劃生成,而且就天然的兼容了MySQL或者pg的語法。
2.如何處理元數(shù)據(jù)的問題?
上面的方案看似很完美的事情,但是有個問題:如果直接將MySQL或者pg的server層搬過來的話,元數(shù)據(jù)怎么辦?cn上到底放不放元數(shù)據(jù)?如果不放元數(shù)據(jù),那么就需要一個統(tǒng)一的存放和管理元數(shù)據(jù)的地方,我在cn上建的表需要到某個固定地方更新元數(shù)據(jù)信息,查詢也是一樣。如果cn上存放元數(shù)據(jù),那么元數(shù)據(jù)的更新就需要在各個cn之間進行同步,如果發(fā)生某個cn宕機,則任何ddl操作都會hang住,這時就需要有一個機制:在cn超時無響應后將cn剔除出集群。
DN的設計
數(shù)據(jù)節(jié)點的設計主要考慮下面幾個方面問題。
1.數(shù)據(jù)節(jié)點如何做高可用?
數(shù)據(jù)庫的數(shù)據(jù)當然是最寶貴的,任何數(shù)據(jù)庫都要有數(shù)據(jù)冗余方案,數(shù)據(jù)節(jié)點一定要有高可用,在保證rpo=0的基礎(chǔ)上盡量縮短rto。細想一下,其實每個dn都是一個數(shù)據(jù)庫實例,這里以MySQL或者pg為例,MySQL和pg本身是有高可用方案的,不管是基于主從半同步還是流復制,都可以在dn層面作為數(shù)據(jù)的冗余和切換方案。當然還有些數(shù)據(jù)庫在dn層面引入了paxos、raft、quorum等的強一致方案,這也是在分布式數(shù)據(jù)庫中很常見的設計。
2.如何做到在線擴容?
在線擴容是分布式數(shù)據(jù)庫的一項巨大優(yōu)點,而擴容數(shù)據(jù)節(jié)點必然涉及到數(shù)據(jù)向新節(jié)點的遷移,目前市面上的分布式數(shù)據(jù)庫基本上都做到了自動的數(shù)據(jù)重分布。但是做到數(shù)據(jù)庫自動重分布還不夠,如何做到只遷移少部分數(shù)據(jù)以降低服務器IO壓力成為關(guān)鍵問題。
傳統(tǒng)的散列方式是根據(jù)分區(qū)鍵哈希值對分區(qū)數(shù)量進行取模操作,得到的結(jié)果就是數(shù)據(jù)應該落入的分區(qū),但是這種分布方法在增加刪除節(jié)點時會造成大量的數(shù)據(jù)重分布,而一致性哈希的核心思想是每個分區(qū)不再是對應一個數(shù)字,而是對應一個范圍,對計算的散列值進行范圍的匹配,大體思路是將數(shù)據(jù)節(jié)點和鍵的hash值都映射到0~2^32的圓環(huán)上,然后從映射值的位置開始順時針查找,將數(shù)據(jù)保存到找到的第一個節(jié)點上。如果超過2^32仍然找不到服務節(jié)點,就會保存到第一個節(jié)點上。一致性哈希最大程度解決了數(shù)據(jù)重分布問題,但是可能會造成節(jié)點數(shù)據(jù)分布不均勻的問題,當然針對這個問題還有一些改進,比如增加虛擬節(jié)點。
GTM的設計
GTM顧名思義是一個全局概念,分布式數(shù)據(jù)庫本來就是為了可擴展、提升性能、降低全局風險,然而GTM這個東西打破了這一切。
1.為什么需要GTM?
簡單一句話總結(jié)就是:GTM是為了保證全局讀一致性,而兩階段提交是為了保證寫一致性。這里我們可能有個誤區(qū),如果沒有GTM那么會不會造成數(shù)據(jù)不一致?會,但是只是某個時間點讀的不一致,這個不一致也是暫時的,但是不會造成數(shù)據(jù)寫的不一致,寫的一致性通過兩階段提交來保證。
我們知道postgresql通過快照(snapshot)來實現(xiàn)MVCC與事務可見性判斷。對于read commit隔離級別,要求每個事務中的查詢僅能看到在該事務啟動前已經(jīng)提交的更改,以及當前事務中該查詢之前所做的更改,這都要通過快照來實現(xiàn)。快照的數(shù)據(jù)結(jié)構(gòu)中會包含事務的xmin(插入tuple的事務號)、xmax(更新或者刪除事務的事務號)、正在運行的事務列表等相關(guān)信息。pg的每條元組(tuple)頭信息中也會記錄事務的xmin和xmax信息。Pg取得snapshot后會進行事務可見性判斷,對于所有id小于xmin的tuple對當前快照可見,同時id大于xmax的tuple對當前事務可見。當前擴展到分布式集群后,每臺機器上都存在pg的實例,為了保證全局的讀一致性,需要一個全局的組件來負責snapshot的分配,使得快照信息在各個節(jié)點之間共享,這就是GTM的工作。
2.GTM高可用的問題?
GTM作為分配全局快照和事務id的唯一組件,只能有一個,當然GTM可以做主備高可用,但是同一時刻只能有一個GTM在工作,gxid信息在主備之間進行同步,這樣就造成一個問題,雖然其他節(jié)點都分布式了,但是GTM始終是一個單點,單點故障時就會涉及到切換,切換過程是影響全局的,而且為了保證切換后gxid信息不丟失,GTM之間必須做到gxid的同步。
針對高可用這塊問題,可以將GTM的事務號存儲信息剝離,將事務號信息存在第三方存儲中,例如etcd就是個很好的選擇,etcd是個強一致高可用的分布式存儲集群,etcd比較輕量,適合用來存儲事務號信息,同時它自身保證了高可用與強一致,這時GTM就不需要在主備之間同步gxid,如果發(fā)生主備切換,新主GTM只需要再去從etcd中取得最新事務號,寫事務號也同理,主GTM會向主etcd節(jié)點寫入事務號信息,通過etcd自身的raft復制協(xié)議保證一致性。這樣的設計使得GTM的壓力減輕很多。
3.GTM性能的問題?
GTM是大部分分布式數(shù)據(jù)庫的性能瓶頸,它使得一套集群的整體性能甚至不如一臺單機。也很好理解,任何一個事務開啟都要先通過cn到GTM取事務號和快照信息,然后結(jié)果解析后下發(fā)到dn執(zhí)行,然后cn進行匯總再返回給應用,路徑很明顯變長了,那么效率肯定變低,目前優(yōu)勢在于可以利用多臺機器的組合能力進行計算,計算資源得到了擴展。
針對GTM的瓶頸問題當然也有解決方案,比如華為GaussDB就提出GTM-Free和GTM-Lite,gtm-free是在那種強一致讀要求不高的場景下關(guān)閉GTM的功能,所有事物都不走GTM,這種情況下性能基本能夠得到線性提升,該功能已經(jīng)實現(xiàn);gtm-lite是將事務分類,全局事務就走GTM,本地事務就直接下發(fā),因為大多數(shù)情況下都是本地事務,所有性能提升也很明顯,該功能還在研發(fā)階段。
分布式數(shù)據(jù)庫如何實現(xiàn)PITR
數(shù)據(jù)庫的PITR一般都是通過一個基礎(chǔ)備份加上持續(xù)不間斷的wal歸檔來做到的,這個基礎(chǔ)備份可以是在線的,因為它并不需要數(shù)據(jù)庫當時處于一致性狀態(tài),一致性可以通過replay redo來實現(xiàn),所以基礎(chǔ)備份可以是文件系統(tǒng)tar命令而不需要文件系統(tǒng)級別的快照。
PITR是通過基礎(chǔ)備份加上redo日志能夠恢復到任意時間點,這個任意時間點不同數(shù)據(jù)庫有不同定義,可能是某個lsn,可能是某個snapshot,可能是某個timestamp。Postgresql數(shù)據(jù)庫中能夠基于redo恢復到任意的timestamp。
分布式數(shù)據(jù)庫的PITR理論上和單機區(qū)別不大,每個節(jié)點備份自己的基礎(chǔ)數(shù)據(jù),這個數(shù)據(jù)不需要一致性,但是要考慮到分布式事務的問題,在做基礎(chǔ)備份的時候必須保證之前的分布式事務(如果存在)已經(jīng)全部完成,因為分布式事務是走兩階段提交協(xié)議,2PC在提交階段不同的機器commit肯定有時間差,如果在這個時間差做了備份,會發(fā)現(xiàn)最后一臺機器有這個事務的redo,另一臺沒有,這樣恢復的話就會造成數(shù)據(jù)不一致。這個問題可以通過pg中一個barries的概念實現(xiàn),在分布式事務結(jié)束后打一個barrier,獲得一致性點,然后再進行基礎(chǔ)備份。對于redo的前滾來說,只需要將所有節(jié)點的redo前滾到一個一致性位點即可。
作者介紹:
張小海,就職于某大型商業(yè)銀行,目前主要負責數(shù)據(jù)庫管理及新技術(shù)研究,PostgreSQL技術(shù)推廣者,個人公眾號:數(shù)據(jù)庫架構(gòu)之美。
【51CTO原創(chuàng)稿件,合作站點轉(zhuǎn)載請注明原文作者和出處為51CTO.com】