騰訊計費平台部托管着公司90%以上的虛擬賬戶。如QB、Q點、包月服務、游戲的二級賬戶等,為了保證能順暢支撐公司各大業務的實時在線交易。而且在各種災難場景下數據是一致而且可用的,對系統的可用性、一致性切換要求很高,因此計費團隊歷來都很重視高一致性存儲系統的建設。
到眼下為止,計費高一致性存儲層的解決方式大致經過了3個階段,本文將分享最新的基於MySQL的分布式解決方式。
隨着業務的發展,基於內存的NoSQL解決方式HOLD平台在高峰期一天支撐3000億讀寫,證明了分布式Cache的巨大價值。但隨着各種業務的接入,NoSQL方案的不足也逐步顯現出來了,例如以下所看到的。
- 適用的業務場景比較有限,僅提供get/set操作,有不少業務場景希望能通過記錄中的其它字段做索引來查詢,比方流水類業務。
- 不是全部的數據都是熱點,一台64GB內存機器提供的有效內存空間大概在50GB左右,而採用Fusion卡的機型容量一般在1TB以上。對照起來。假設全部數據放入分布式Cache明顯是一種極大的浪費。最合理的當然是熱點在HOLD。冷數據採用基於磁盤的存儲。
- 計費平台部多年來在支付領域有了相當多的技術積累,HOLD作為NoSQL系統功能有限,因此建造一套更加強大通用的高一致性存儲系統將整個支付領域的實時數據(重點是賬戶數據、用戶訂單數據。以及海量的流水數據)統一管理起來很有價值。
基於上面的分析。結合我們在MySQL領域多年的應用和優化經驗,終於決定在MySQL存儲引擎基礎之上,打造一套分布式的SQL系統。
- 保持原來的MySQL協議,這樣曾經訪問MySQL系統的C++、Java各類系統都不須要改動。DBA能繼續保持原來大部分使用習慣。
- 自己主動的跨IDC容災切換,同一時候保證數據一致性,對於提交成功的事務保證一筆不丟,達到銀行級對容災的要求。
- 靈活的容量伸縮機制,對業務透明,解決MySQL本身擴容不靈活的問題。
- 重點支持OLTP類型的在線業務。
總體架構
針對上面的需求,TDSQL終於的結構如圖1所看到的(與當前大部分中心化的分布式系統類似)。
圖1 TDSQL架構
系統由三個模塊組成:Scheduler、Agent、網關,三個模塊的交互都是通過ZooKeeper完畢,極大簡化了各個節點之間的通信機制。相對於第二代HOLD的開發簡單了非常多。
Scheduler作為集群的管理調度中心,主要功能包含:
- 管理set,提供創建、刪除set、set內節點替換等工作;
- 全部的DDL操作統一下發和調度;
- 監控set內各個節點的存活狀態,當set內主節點故障,發起高一致性主備切換流程;
- 監控各個set的CPU、磁盤容量、各個表的資源消耗情況。必要的時候自己主動發起擴容流程;
- Scheduler自身的容災通過ZooKeqzer的選舉機制完畢,保證中心控制節點無單點。
Agent模塊負責監控本機MySQL實例的執行情況,主要功能包含:
- 用短連接的方式周期性訪問本機的MySQL實例,檢測是否可讀、可寫,若發生異常,會將異常信息上報到ZooKeeper,終於會由上面描寫敘述的Scheduler模塊檢測到這個異常情況,從而發起容災切換。
- 檢測主備復制的運行情況。會定期上報主備復制的延時和延遲的事務數。若發生了主備切換,自己主動向新主機重建主備,因此MySQL的主備不須要DBA干預。對於新增的實例會自己主動採用xtrabackup通過主機自己主動重建數據。
- 檢測MySQL實例的CPU利用率和各個表的請求量、數據量、CPU利用率。上報到ZooKeeper,ZooKeeper通過全局的資源情況抉擇怎樣擴容、縮容;
- 監控是否有下發到自身的擴容任務,如有則會運行擴容流程(以下會有描寫敘述);
- 監控是否要發生容災切換。並按計划運行主備切換流程。
網關基於MySQL Proxy開發。在網絡層、連接管理、SQL解析、路由等方面做了大量優化。主要特點和功能例如以下:
- 解析SQL,將識別出的DDL語句直接存到ZooKeeper,讓Keeper來統一調度;
- Watch ZooKeeper的路由信息。拉取最新的路由表保存到本地文件和內存;
- 將SQL請求路由到相應的set,支持讀寫分離。
- 對接入的IP、username、password進行鑒權;
- 記錄完整的SQL運行信息。與秒級監控平台對接完畢實時的SQL請求的時耗,成功率等指標監控分析;
- 對count、distinct、sum、avg、max、min、order by、group by等聚合類SQL一般須要訪問后端的多個set,網關會分析結果並做合並再返回。暫不支持跨set join和分布式事務;
- 網關無狀態,既支持與業務部署到一起,也能夠獨立部署(可通過TGW或者LVS做容災)。
自己主動擴容機制
眼下。針對MySQL的擴容,一般有以下兩種策略。
- 垂直擴容。
一般通過升級硬件來實現,比方更換更好的CPU,將傳統的sas盤換成FusionIO卡這類,然后針對新硬件調整好參數。在硬件結構變化比較大的時候,性能甚至能達到上十倍的提升。但垂直擴容有比較大的局限,就是這樣的模式隨着業務的突增還是比較easy達到瓶頸,特別是面對互聯網海量用戶的時候。所以在互聯網應用場景下,一般僅將垂直擴容當做一個輔助的手段。
- 水平擴容。經常使用的有2種方法,一是不同的庫或者表部署到不同的實例,二是一張表須要依據某個字段拆分到不同的字表中(數據分片)。這樣的策略在互聯網系統中非經常見,非常多系統會將這2種水平擴容的方法結合起來使用。
通過上述2種擴容方法的比較。為了應對海量擴展的需求,應該是重點選用水平擴容的方法。但水平擴容的實現一般對業務是有感知的,比方採用什么規則來拆表。拆開的表放到哪些節點,假設某個子表還有瓶頸應該怎么擴容,擴容是否還須要業務配合等等這些事情假設所有交給業務會比較繁瑣。因此這些需求應該盡量所有交給TDSQL自身來完畢。對業務全然透明。
分表邏輯
在TDSQL中。每一個表(邏輯表)可能會拆分成多個子表(建表的時候通過在建表語句中嵌入凝視的方式提供一個shard字段名。最多會拆分出1W個子表),每一個子表在MySQL上都是一個真實的物理表,這里稱為一個shard,因此一張表的數據可能會按這種方式分布在多個Set中,如圖2所看到的
圖2 TDSQL的邏輯表
每一個SQL請求到達網關之后,網關會做詞法和語法解析。重點會解析出shard字段,假設帶了shard字段就能夠直接查詢路由表並發送到某個詳細的set中。
計費的OLTP類業務99%的請求都會帶上shard字段;假設某筆請求沒有shard字段,查詢路由之后會將請求發送到全部的shard相應的set中,並對全部返回的結果做一些聚合運算。
擴容流程
上面描寫敘述了shard的方式。可是這種shard結構不是固定不變的,當Scheduler檢測到某個set,某個表的CPU、磁盤超過閾值之后就會啟動擴容流程。
這里描寫敘述下詳細的擴容流程。
擴容過程中一般都要盡量避免影響業務,眼下來看存在2種比較成熟的策略。
策略1先切后搬:先改動路由,將須要遷走的數據的請求直接發送到新set,在新set交易過程中如發現本地的數據不存在。則去原set拉取數據,然后再通過一些離線的策略將要遷移的數據全量再搬遷一次。HOID平台就是採用這種策略。
策略2先搬后切:讓請求繼續在原set交易,擴容程序首先記錄一個binlog位置點,並將源set中符合遷移條件的數據所有遷移出去,最后再將搬遷過程中新增的binlog追完,最后改動路由規則,將請求發送到新set。
綜合來看,策略1最大的優點是假如是由於壓力大做的遷移。可能很快就能將部分請求發送新set了,實現對原set的壓力分擔。策略2實現上在最后的追路由階段須要很多其它的精細化控制。實現會略微復雜點。但策略2有個很大的優點就是擴容過程中回滾很方便,如有異常直接干掉擴容任務就可以。
對於TDSQL這類數據庫業務系統來說。策略1實現會很麻煩。由於請求到達新set之后可能須要去源set拉取數據,這個須要對MySQL本身進行改動;另外假如一個批量更新的update操作。可能要往新老set都發送一次請求,比較復雜。所以終於選擇了策略2。策略2會有更大的通用性。開發模式基本上能夠統一到全部類似的系統。
以下描寫敘述採用策略2詳細的擴容流程。假如要將Set1中的t_shard_1的數據遷移一半到Set4中的t_shard_4(1667-3333)。
圖3 策略2的擴容流程
Scheduler首先在Set4中創建好表t_shard_4。
后將擴容任務下發到Set1中的agent模塊。agent檢測到擴容任務之后會採用mysqldump+where條件的方式將t_shard_1中shard號段為1667-3333的記錄導出來並通過管道用並行的方式插入到Set4(不會在本地存文件。避免引起過多的IO)。用mysqldump導出鏡像的時候會有一個binlog位置。
從mysqldump記錄的binlog位置開始讀取binlog並插入到到Set4。追到全部binlog文件末尾的時候(這須要一個循環,每次循環記錄從開始追binlog截止到追到文件結尾消耗的時間,必須保證追單次循環要在幾秒之內完畢,避免遺留的binlog太多導致最后一次追binlog消耗太多的時間,從而影響業務過久)。對原來的表t_shard_1重命名t_shard_5,此時針對這個表不會再有新請求。若還有請求過來都會失敗,然后再追一次binlog到文件結尾(由於上面的循環保證了追binlog不會太耗時間了,所以此次會高速完畢),然后上報狀態到ZooKeeper,表明擴容任務完畢。
Scheduler收到擴容完畢的信息之后會改動路由表,最后由網關拉取到新路由完畢總體的擴容;從表重命名開始到網關拉取到新路由,這段時間這個原始shard不可用。從我們測試結果來看這個不可用的時間是200毫秒左右;假設某個網關異常,拉取不到新路由,繼續訪問老表t_shard_1會一直失敗。這樣就能夠保證數據的一致性。
容災機制
對於TDSQL來說,我們希望容災做到自己主動切換。自己主動恢復。主備一致性(保證業務提交的事務在切換過程不丟失),跨IDC容災。
【MySQL異步復制】
在MySQL發展的早期,就提供了異步復制的技術。僅僅要寫的壓力不是特別大,在網絡條件較好的情況下。發生主備切換基本上能將影響控制到秒級別。因此吸引了非常多開發人員的關注和使用。但這套方案提供的一致性保證。對於計費或者金融行業是不夠的。
圖4是異步復制的大致流程。非常顯然主機提交了binlog就會返回給業務成功,沒有保證binlog同步到了備機,這樣在切換的瞬間非常有可能丟失這部分事務。
圖4 異步復制
【MySQL半同步復制】
到了MySQL 5.5版本號的時候。Google提供了一個半同步半異步的插件。確保必須收到一個備機的應答才讓事務在主機中提交。當備機應答超時的情況下,強同步就會自己主動退化成異步模式(這也是半同步半異步名字的由來)。
圖5 半同步復制
這套方案相對異步復制,在數據的可靠性方面確實好非常多,在主機本身故障的情況下,基本能保證不丟失事務(由於最后一個事務,至少有一個備機上存在),但一旦退化成異步復制就回到過去了。TDSQL沒直接採用這套方案。是由於:在主備跨IDC(ping延遲2-3毫秒)時性能非常非常低。
【Cluster方案】
除了上面的方案外。開源社區還有三個Cluster解決方式,各自是Oracle的NDB引擎、Percona XtraDB Cluster和MariaDB Galera Cluster。從公開資料的性能對照上來看。后2者在性能和系統靈活性等方面都強於NDB(同一時候採用NDB意味着也放棄了InnoDB引擎,NDB主要是基於全內存的。而且須要快速網絡環境支持,所以不考慮了);Percona XtraDB Cluster和MariaDB Galera Cluster強同步機制的底層都是採用Galera這套強同步的架構。MariaDB Galera Cluster具有例如以下很吸引人的特性:
- MariaDB Galera Cluster 是一套在MySQL InnoDB存儲引擎上面實現multi-master及數據實時同步的系統架構,業務層面無需做讀寫分離工作,數據庫讀寫壓力都能依照既定的規則分發到各個節點上去。
- 同步復制Synchronous replication:保證節點間數據一致性;
- Active-active multi-master拓撲邏輯:多主的拓撲結構,能夠覺得沒有備機的概念;
- 可對集群中任一節點進行數據讀寫:假如一個set有3個節點,則3個節點能夠同一時候讀寫。上次全然不用關心主備切換和讀寫分離;
- 自己主動成員控制,故障節點自己主動從集群中移除;
- 自己主動節點增加。
- 真正並行的復制,基於行級:同一個表能夠在集群中不論什么節點更新。支持不帶where條件,但一次更新的記錄條數有限制。
- 每一個節點都包括完整的數據副本。
眼下來看,Galera是一套相當完美的方案。可是。在跨IDC的性能測試中。其性能下降比較大,另外,實現方案也比較復雜,眼下對它的代碼理解還不夠透徹,所以臨時沒有在計費領域大范圍推廣使用。
但我相信這個方向是對的,有吸引力的。隨着興許Galera越來越完好。我們對它研究得越透徹。或許有一天會採用這套方案。
【性能測試和分析】
上面的三種復制模式對照測試,數據如圖6所看到的。
圖6 三種復制模式的對照
從圖6的數據能夠看出,半同步和Galera模式對性能的損耗還是很大的。Galera的毛刺尤其嚴重,所以在跨IDC環境下還不是適合計費這樣對延遲要求很低的場景。
為什么性能損耗會這么嚴重呢?這個看明確MySQL的網絡模型就清楚了。外界可查的MySQL最早的公開版本號應該是1996年的3.1.1.1版本號。這么多年來,網絡模型基本上變化不大。與Apache有點類似,有點差別的是MySQL採用的是每一個連接一個線程的模型,這套模型最大的優點就是開發特別簡單,線程內部都是同步調用。僅僅要不訪問外部接口,支撐每秒幾百上千的請求量也基本夠用。由於大部分情況下IO是瓶頸。只是隨着當前硬件的發展,尤其是SSD、FusionIO的出現,IOPS從200+/s進化到幾十萬甚至百萬次/s。IO基本上不再是瓶頸,若再採用這套模型並採用堵塞的方式調用延遲較大的外部接口。則CPU都會堵塞在等網絡應答上了,性能自然上不去。
只是在MySQL5.6企業版和MariaDB、Percona中都引入了線程池。使得網絡模型靈活了非常多,圖7是簡化后的對照模型。
圖7 簡化的對照模型
TDSQL採用的強同步方案
從上面的分析可知,半同步半異步是比較輕量級的高一致性容災方案。但受限於已有的同步網絡模型,CPU利用不起來。我們假設在線程池基礎之上做一些改動,參考半同步的思路就能夠實現一個高性能的強同步方案。
眼下的做法是採用與Linux內核處理中斷的思路:將上面線程池模型的第三個環節(運行SQL的邏輯)拆成兩個部分:
- 上半部分:任務運行到寫binlog為止。然后將會話保存到session中。接着運行下一輪循環去處理其它請求了。這樣就避免讓線程堵塞等待應答了;
- 然后:MySQL自身負責主備同步的dump線程會將binlog馬上發送出去,備機的IO線程收到binlog並寫入到relay log之后。再通過UDP給主機一個應答;
- 在主機上,開一組線程來處理應答。收到應答之后找到相應的會話。運行下半部分的commit。send應答,綁定到epoll等操作。綁定到epoll之后這個連接又能夠被其它線程檢測到並運行了。
改造后性能提升明顯。如圖8所看到的。
圖8 改造后的性能
數據高可用性保障機制
除上述強同步機制外。TDSQL還做了下面增強,以提升數據的可用性。
- 推薦一個set最少配置3個跨IDC的節點。能夠按業務的要求對備機開放查詢服務。
- 支持靈活添加節點。比方認為3個節點還不夠。能夠很方便地添加節點。TDSQL會自己主動完畢數據的全量和增量復制,此處主要依賴Xtrabackup實現物理復制,性能測試數據表明:一個小時大概能夠拷貝500GB數據到新節點。那么對於Z3(1.1TB盤,一般最多用800GB左右)。新添加的節點大概1.5個小時左右就有了全量數據,此功能也能夠用在壞盤等情況下替換節點的時候使用。很方便。
- 細心的同學可能會發現上面的強同步還有點小缺陷:比方主機用kill -9殺掉。那么可能寫了binlog但沒有來得及發送到遠端,此時當然也不會返回給業務成功,備機上不存在這筆數據。但主機起來之后會多出來這筆事務。
我們的做法是對新增的事務依據row格式的binlog做閃回,當然回退不了的比方drop table之類的,就直接提醒運維手工確認是否清除數據庫。然后會由Xtrabakcup機制自己主動從新的備機全量拉取數據重構。
- 節點的監控通過跨IDC部署的ZooKeeper來保證。而且主備切換由一套自己主動化的嚴格流程來保證。
接下來的方向
- 當將高一致性容災、高可用性、自己主動容量伸縮做實后,隨着業務的接入,集群的規模會越來越大,TDSQL必將會更加依賴實時的資源調度、隔離框架,因此有必要研究怎樣將TDSQL與Docker結合起來。
- 如前所述,Galera集群是個非常好的發展方向,我們會持續研究並實踐。
- 眼下大部分MySQL還在使用單個連接單線程模型。線程池也剛起步。以后隨着大家對性能要求越來越高,這塊或許能夠繼續突破,比方結合線程池+協程或許是個非常好的方向。假設真能引入協程,或許為MySQL添加調用外部接口的結構會靈活非常多。
- TDSQL將數據拆是拆的徹底了,但作為完整的分布式數據庫、合也須要考慮,比方跨庫少量記錄的join,規模受限的分布式事務等。眼下的做法是數據按小時入TDW,在TDW上做OLAP分析。
作者簡單介紹:雷海林,2007年增加騰訊,10年以上的Linux后台Server開發經驗。眼下主要從事分布式Cache、實時大數據處理引擎、分布式MySQL(TDSQL)設計和開發工作。
本文選自程序猿電子版2015年6月A刊,該期很多其它文章請查看這里。2000年創刊至今全部文章文件夾請查看程序猿封面秀。歡迎訂閱程序猿電子版(含iPad版、Android版、PDF版)。
本文為CSDN原創文章,未經同意不得轉載,如需轉載請聯系market#csdn.net(#換成@)