分佈式MySQL集群方案的探索與思考

本文整理自ArchSummit微信大講堂張成遠線上群分享內容


背景

數據庫作為一個非常基礎的系統,任何一家互聯網公司都會使用,數據庫產品也很多,有Oracle、SQL Server 、MySQL、PostgeSQL、MariaDB等,像SQLServer/Oracle 這類數據庫在初期可以幫業務搞定很多棘手的事情,我們可以花更多的精力在業務本身的發展上,但眾所周知也得交不少錢。

涉及到錢的事情在公司發展壯大以後總是會回來重新審視這個事情的,在京東早期發展的過程中確實有一些業務的數據就是直接存在oracle或者sqlserver中。

後來隨著業務的發展以及數據量訪問量的不斷增加及成本等方面的考慮,從長遠考慮需要把這些業務用免費的MySQL來存,但單機的MySQL往往無法直接抗住這些業務,自然而然的我們就需要考慮引入分佈式的MySQL解決方案幫助業務去SQLServer/Oracle以及支撐未來的發展。

方案選型對比及京東實現方案

說到分佈式MySQL的解決方案一般來說解決方案主要就兩種,客戶端的方案或者中間代理的方案,如下圖所示。

分佈式MySQL集群方案的探索與思考


分佈式MySQL集群方案的探索與思考


這兩種方案各有各的優缺點:客戶端的方案是指會給業務提供一個專門的客戶端的包,這種方案在實現上會更容易一點,如果公司需要快速出一個相對通用的解決方案,客戶端的方案可以優先考慮。

客戶端方案需要為不同的語言提供不同的客戶端的包,這點有所侷限。客戶端方案只需要走一段網絡,理論上性能會更好一點。

客戶端方案對業務有侵入,有一些系統部署及實現方面的可能可以控制得更好,但對業務本身不友好,客戶端包升級等方面比較麻煩。

中間代理的方案是指採用一個兼容MySQL協議的代理的方式,業務可以使用任何語言的MySQL客戶端的包,對業務本身無侵入的,這種方案相對來說是最友好的。

中間代理方案開發難度上來說門檻會更高一點,需要考慮前後端的東西,尤其是與MySQL端交互時自己解析協議的情況下會更復雜一些。中間代理方案多走一段TCP,對性能理論上會有一些影響。

上述兩種方案有一個非常重要的因素沒有提及,在實際生產環境中面臨一個非常現實的問題是MySQL能支持的連接數是有限的。以MySQL5.5來說假設一個MySQL實例配置1000個連接,業務應用實例部署了100個,每個應用實例的數據庫連接池配置20個,採用客戶端方案這個MySQL實例都沒法正常工作了。

大多數情況下並不是每個應用實例的每條連接都是活躍的,中間代理的方案可以很好的解決這個問題,應用實例可以有很多連接打到代理上,代理只需要維護較少的與MySQL的連接即可滿足需求,代理與MySQL之間的連接會被業務打過來的訪問重複使用。

另外關於多走一次TCP對性能的影響,從我們的實際經驗來看其實可以忽略不計,業務實例一多優先遇到的是MySQL連接數的問題,從這個角度來說中間代理的方案會更優。

分佈式MySQL集群方案的探索與思考


我們採用的就是中間代理的方案,京東的分佈式MySQL方案由很多部分組成,有JManager、 JProxy、 JTransfer、JMonitor、JConsole、MySQL,在實際部署的時候還涉及到LVS以及域名系統等。

JManager是中心管理節點,這個節點負責統一管理系統的元信息,元信息包括路由信息、權限管理信息、資源相關的信息等。

JProxy就是一個兼容MySQL協議的代理,負責把客戶端發送過來的SQL按照路由規則發送到相應的數據庫節點上,再把返回的結果進行合併並返回給客戶端。JProxy在啟動的時候會先去JManager中拉取相關的元信息,並在自己的內存中維護一份,平時使用的時候只用自身內存維護的這一份就可以了。

JProxy的內部實現原理如圖所示。

分佈式MySQL集群方案的探索與思考


JTransfer是在線遷移系統,我們針對業務的數據進行拆分以後,比如某個MySQL實例上有32個庫,等到業務數據量繼續增大以後在這個實例上就放不下了,我們就需要往整個集群中加MySQL實例,將之前的32個庫中一部分遷移到這個新增加的實例上,如何在不停業務的情況完成數據的在線遷移就是JTransfer這個系統來保證的。

JConsole系統可以理解為將多個業務的中心管理節點整合起來的一個後臺管理控制系統,這個系統可以與每個JManager交互。在具體使用的時候,業務方需要申請創建庫表、拆分規則、什麼權限、對哪些IP授權,我們會通過JConsole系統與JManager交互完成元數據的配置。

JMonitor系統會將各個業務的jproxy以及MySQL相關的信息採集起來,整合到一起形成一個統一的監控系統,完成對系統的全面詳盡的監控。

網絡模型

JProxy作為一個非常典型的代理服務,程序本身的性能非常關鍵,具體在實現的時候我們參考了Nginx的網絡模型。

大家都知道Nginx的性能非常高,根據機器核數配置相應的worker數就可以,每個worker可以理解為圍繞一個epoll把前後端的連接以完全基於事件驅動的方式串在一起,避免了上下文切換避免了鎖等待等各種可能阻塞或者耗時的操作。

同樣的網絡模型也可以參考一下Redis的實現,redis雖然不像nginx需要考慮前後端連接的處理,但redis的模型也是一種非常類似的經典的實現方式。

JProxy整個網絡模型如圖所示,採用一個全局的nioacceptor以及多個nioreactor,由nioacceptor統一accept連接,之後把連接分給某個nioreactor。

nioreactor可以理解為底層就是一個epoll(java nio實現),前後端的連接都是註冊在這個epoll上,我們只需要根據事件是讀事件或寫事件調用相應的回調函數即可。這種模型的特點是系統幾乎沒有太多的上下文切換,而且性能很高。

分佈式MySQL集群方案的探索與思考


基於事件驅動的網絡模型的好處是性能很高,但問題也很明顯,編寫時複雜度非常高,一條SQL發送過來到收到結果的上下文被切成很多片段,同一時刻有來自很多不同上下文的不同的片段要處理,全程只有一個進(線)程來處理這些片段(暫且假設NIOReactor只配置成一個),所以在實現的過程中要求把所有的細節都考慮非常周全,一旦某個片段的處理有阻塞或者耗時,整個程序都將阻塞,個人覺得這種編程方式有點反人類思維。

關於分佈式事務的思考

另外關於分佈式事務的支持也是一個大家可能比較感興趣的點,基於MySQL的方式來做分佈式數據庫的時候分佈式事務是不可能滿足嚴格的分佈式事務語義的。

數據庫事務有ACID四個屬性,分別是原子性、一致性、隔離性、持久性。

原子性(Atomicity)的意思是整個事務最終只能是要麼成功要麼失敗,不能存在中間狀態,如果發生錯誤了就需要回滾回去,就像這個事務從來沒有執行過一樣。

一致性(Consistency)是指系統要處於一個一致的狀態,不能因為併發事務的多少影響到系統的一致性,舉個典型的例子就是轉帳的情況,假設有ABC三個帳號各有100元,那麼不管這三個帳號之間怎麼轉賬,整個系統總的額度是300元這一點是應該是不變的。其實ACID裡的一致性更多的是應用程序需要考慮的問題,和分佈式系統裡的CAP裡的一致性完全不是一個概念。

隔離性(Isolation),本質上是解決併發執行的事務如何保證數據庫狀態是正確的,抽象描述叫可串行化,就是併發的事務在執行的時候效果要求達到看起來像是一個個事務串行執行的效果。有衝突的事務之間的隔離性如果保證不了會引起前面的一致性(consistency)也無法滿足。

每個事務包含多個動作,這些動作如果按照事務本身的順序依次執行就是所謂的串行執行,這些動作也可以重新排列,排列完以後的動作如果效果可以等價於事務串行執行的效果我們就叫做可串行化調度。

實際實現的時候往往採用的是衝突可串行化,這個條件比可串行化要求會更高一點,規定了一些讀寫順序規定了一些訪問衝突的情況規定了哪些情況兩個事物的動作可以調換哪些是不可以的,可以理解為衝突可串行化是可串行化的充分條件。

持久性(Durability),在事務完成以後所有的修改可以持久的保存在數據庫中,一般會採用WAL的方式,會把操作提前記錄到日誌中來保證即使操作還沒有刷到磁盤就宕機的情況下有日誌可以恢復。

介紹完事務的ACID屬性以後,我們再來分析為什麼基於MySQL無法提供嚴格的分佈式事務語義的支持。

如果客戶端發送的SQL只涉及到一個節點,那自然是可以保證事務的,但是如果客戶端發送的SQL涉及到兩個及以上節點的SQL,那就無法保證事務語義了。

原因主要是兩個,一是原子性無法保證,另一個是隔離性無法保證。在一個節點commit成功以後,在另外的節點commit失敗了,這個事務就處在一箇中間狀態,此時原子性被打破。

引起的另一個問題就是隔離性,這個事務的一部分提交了,另一部分未提交,此時該事務正常是不該被讀取到的,但是提交成功的部分會被其他事務讀到,此時就無法保證隔離性了。

另外就算是涉及多個節點的操作都是成功的,理論上來說也是無法保證隔離性的。因為假設A事務的一個節點先commit成功,其他的節點後commit成功,而此時B事務在讀取的時候可能會讀取到了A事務最早commit成功的那部分內容,卻沒有讀到後來commit成功的內容,此時依然無法保證隔離性。

更本質一點的原因是MySQL的事務都是每個實例維護自身的事務ID,而基於MySQL集群的分佈式方案沒有一個全局的事務ID來標識每個MySQL實例上的事務以及全局事務的元信息的管理,所以無法做到嚴格的分佈式事務語義。

但實際上絕大多數業務對這個需求未必那麼強烈,因為絕大多數的業務邏輯都是可以拆分的,拆成一個個只落在一個分庫裡的操作在絕大多數場景下是完全可行的,而且拆分完以後也會更可控,所以這個問題在我們支撐業務的過程中也不是一個特別大的問題。

生產環境監控很關鍵

在實際生產環境中有很多方面都非常重要,高可用高可靠可擴展等,但是除了這些之外還有一個非常關鍵的是監控。

一個再健壯再牛x的系統都需要配備完善的監控系統,監控系統是生產環境中非常重要的一道防線,沒有監控的系統就像是在裸奔,線上突發狀況很多完善的監控系統可以做到第一時間發現問題及時定位以及解決問題。

物理機監控。

我們在生產環境中會對系統所在物理機進行監控,京東有一個專門的物理機監控系統,可以監控包括CPU、內存、網卡、TCP連接數、磁盤使用情況、機器load等很多基礎指標,針對這些指標可以設置相應的報警閾值,當超過一定閾值時會以郵件及短信的方式報警。

存活監控

但物理機的監控對於具體的系統的來說是遠遠不夠的,我們還需要關注很多系統本身的信息,首先要有存活監控,這是最基本的。一個系統在線上運行的時候服務本身宕掉一定要求是可以第一時間監控到的。

但除了物理機監控以外,還有一個非常關鍵的是存活監控。系統的一切前提是可以活著,我們在每個模塊都會提供相應的http接口,接入公司的統一監控平臺,一旦有異常統一監控平臺會及時通知相應的負責人。

系統內部狀態可視化監控。

除了活下來以後,如何活得更好也是很關鍵的,所以我們還有專門針對分佈式MySQL集群的JMonitor系統,該系統會整合各個模塊的內部詳細狀態信息,包括慢查詢、用戶訪問情況以及數據分佈情況等。

一句話一個穩定健壯的系統一定要配備相應的完善的監控系統。今天我的分享就是這些,主要就是介紹一些分佈式MySQL的相關方案以及京東是怎麼做的,討論了一下分佈式事務的問題,最後是一小部分生產實踐經驗,謝謝大家。

Q&A

問題1:請介紹下分佈式事務保證數據最終一致性的具體方案例子。

首先分佈式事務涉及到的一致性和CAP中一致性是兩個概念,事務ACID屬性中的一致性不涉及最終一致性,對於關係型數據庫中事務的概念,我的理解都是強一致的(通過原子性和隔離型保證)。只有涉及到某一個節點(內容是相同的情況)多副本之間的複製問題才會涉及到弱一致性或者最終一致性(CAP中C)的問題。而分佈式事務本身如果保證了原子性和隔離性,數據庫層面就提供了一致性保證,其餘的是應用邏輯層面保證。如果問的是數據庫主從複製之間的一致性問題,這個事情本質上和事務(ACID的C)的一致性就沒有關係了,所以這個問題本身可能有待商榷。

問題2:分佈式事務如何支持,現在可以支持多大規模的集群。

基於Mysql的分佈式集群方案無法保證嚴格的分佈式事務語義,但是在實際使用的時候看業務情況,如果事務之間不怎麼衝突的情況下也是ok的,如果可以改成只涉及一個分庫的情況下那就繞開分佈式事務的問題了。另外支持的集群,我們其實是根據業務來劃分資源的,目前整體資源不能說特別大,千臺規模。

問題3:JProxy是否可以支持所有複雜sql查詢,主要是誇庫的關聯查詢,具體內部邏輯可否介紹下?

我們目前不支持誇庫關聯查詢,從業務層面來解決。因為大表之間分庫以後如果要支持跨庫關聯查詢的話,作為一個OLTP系統在實際生產環境估計就沒法用了。

問題4:請介紹下MySQL實際應用中主從複製的方案,以及主從的數據差異會在什麼程度,謝謝!

這個其實更多的是主從之間關注的問題,一般會採用基於mix的模式。另外主從差異這個不同業務不一樣,加上嚴格的監控,正常訪問的情況下一般不會出現延遲,但是如果涉及到業務倒數據或者突增的訪問量可能會引起延遲,所以這個不太好參考,如果有異常我們都會第一時間及時介入處理。

問題5:長時間SQL不會造成堵塞嗎?

主要看這條SQL具體是做什麼的,如果是抽數據,就正常抽就可以了。如果有阻塞基本都是因為在MySQL端因為鎖衝突等原因造成阻塞,最終可能是這個事務被abort掉或者最終搶到鎖成功做完了這個事務。

問題6:請介紹一下JTransfers的工作機制,以及實現過程中最難的部分。

遷移確實是比較刺手的一件事情,要考慮的細節很多。大體的步驟是:我們提交遷移計劃,指定什麼時間開始遷移,到時間點以後JTransfer就會自動遷移。JTransfer一開始是dump源分庫的數據,然後將這些數據恢復到目標實例上,但是在這個期間業務是正常訪問的,需要將增量數據遷移完,所以會有追增量過程。當增量追到一定程度,我們會阻塞這個庫的訪問,最後將剩餘的少量數據遷移完。因為最後剩餘數據量不多的時候,阻塞過程其實很短暫,所以對業務影響非常小。

最難的部分是:整個遷移過程中的路由變更,要保證路由變更的過程中數據不能寫花,且變更以後的路由要準確的推送到JProxy中,由JManager和多個JProxy之間在變更路由的時候採用類似兩階段提交的協議,從而保證路由的變更是正確的。

問題7:可以分享一下JProxy的併發性能優化,以及JProxy中間狀態的異常與恢復機制嗎?謝謝!

併發性能優化我們主要是通過採用基於事件驅動的網絡模型,這種方式的特點是避免上下文切換避免鎖的開銷,但是代價的話剛才也說了需要考慮得非常周全,把一個上下文切成很多片段,不太符合人類思維。

JProxy中間件狀態的異常與恢復機制這個我不是太理解什麼含義哈,我的理解是如果jproxy運行過程中訪問出異常了怎麼處理,如果是某個連接過來的sql出了問題我們的做法是將整個連接涉及到的資源都關閉,把該次查詢涉及到的前後端資源清理乾淨,這樣就不會影響到其他客戶端的訪問。正常來說不應該出現這種情況,所以也需要完善的日誌信息以及監控信息。


分享到:


相關文章: