轉載請註明,該文已在《程序員》2012年10期上發表 css
MySQL做爲一個低成本、高性能、可靠性好並且開源的數據庫產品,在互聯網企業應用很是普遍,例如淘寶網有數千臺MySQL服務器的規模。雖然近兩年來NoSQL的發展很快,新產品層出不窮,但在業務中應用NoSQL對開發者來講要求比較高,而MySQL擁有成熟的中間件、運維工具,已經造成一個良性的生態圈等,所以從現階段來看,MySQL佔主導性,NoSQL爲輔。 mysql
在過去一年時間裏,咱們(淘寶核心系統數據庫團隊)在MySQL託管平臺方向作了大量工做,設計和實現了一套UMP(Unified MySQL Platform)系統,提供低成本和高性能的MySQL雲數據服務。開發者從平臺上申請MySQL實例資源,經過平臺提供的單一入口來訪問數據,UMP系統內部維護和管理資源池,以對用戶透明的形式提供主從熱備、數據備份、遷移、容災、讀寫分離、分庫分表等一系列服務。平臺經過在一臺物理機上運行多個MySQL實例的方式來下降成本,而且實現了資源隔離,按需分配和限制CPU、內存和IO資源,同時支持不影響提供數據服務的前提下根據用戶業務的發展動態的擴容和縮容。 程序員
架構的演變 web
UMP系統初版基於mysql-proxy 0.8版修復若干bug,並對proxy插件中管理用戶鏈接和數據庫鏈接的狀態機流程進行一些修改,同時編寫Lua腳本實現去中心數據庫獲取用戶認證信息和後臺數據庫地址,對用戶進行驗證,創建到後臺數據庫的鏈接和轉發數據包等邏輯。 算法
圖1 UMP系統的初版採用MySQL Proxy sql
在開發和部署初版的過程當中,咱們逐漸認識到幾個問題: 數據庫
首先,mysql-proxy 0.8版對多線程的支持比較簡單粗暴,多個工做線程共享同一個消息隊列,同時監聽着同一個socketpair通道,當有新事件進入消息隊列後,socketpair會被寫入一個字節,全部休眠中的線程都會被喚醒,去競爭一個互斥鎖從消息隊列中取任務。這種實現一是形成「驚羣」現象,多個線程被喚醒但只有一個線程須要去任務,二是任務的CPU親緣性比較差,在同一個狀態機上觸發的事件會在多個處理器上來回切換執行。此外,mysql-proxy中還使用了全局Lua鎖,同時僅容許一個工做線程執行Lua腳本(計劃在0.9版本中改進),所以mysql-proxy多線程模式下的性能遠不能同CPU核數保持線性增加,甚至在16核上的性能還不如4核。而使用單進程模式時,一臺物理機上須要部署多個進程纔能有效利用機器的處理能力,但給部署、監控和服務的升級帶來麻煩。 編程
其次,限於mysql-proxy的框架,功能上不容易擴展,實現用戶的鏈接數限制、QPS限制、以及主從切換、讀寫分離、分庫分表等一系列功能比較困難。 後端
最後,mysql-proxy的社區近些年來並不活躍,並且C語言對開發者功底的要求比較高,很難要求團隊全部成員協同開發出兼顧優雅和正確性的代碼。 安全
所以咱們決定用Erlang語言從新編寫proxy服務器,替換了原有的mysql-proxy模塊。目前整個項目擁有5萬行Erlang源碼,3萬行C/C++源碼,2萬行其餘語言源碼。
爲何選擇Erlang語言
Erlang是一個結構化的、動態的、函數式的編程語言。常見的一個說法是Erlang是面向併發的(concurrent-oriented),這主要指Erlang在語言中定義了Erlang進程的概念和行爲(下文中凡是「Erlang進程」都是指Erlang語言中定義的進程,以區分於你們熟悉的操做系統進程),和操做系統的進程/線程相比,Erlang進程一樣是併發執行的單位,但特別的輕量級,它是在Erlang虛擬機內管理和調度的「綠進程」,即用戶態進程。舉個例子,在關閉了HiPE和SMP支持的Erlang虛擬機中,一個新建立的進程佔用的內存僅爲309個字(word,64位服務器上爲8個字節),其中233個字爲堆空間(包含棧),建立和結束一個進程的代價約耗時1~3微秒,而一個Erlang虛擬機中能夠同時支持幾十萬甚至更多個進程。
圖2 Erlang的輕量級進程
說到Erlang語言,就必須說起OTP(Open Telecom Platform,開放電信平臺),OTP是用於開發分佈式的、高容錯性的Erlang應用程序的框架與平臺。例如,一個Erlang節點鏈接並註冊到Erlang集羣上,發現集羣中的其餘節點,與它們進行RPC通訊,都在OTP裏的kernel服務中實現的。OTP和Erlang語言關係如此緊密以致於二者一般合稱爲Erlang/OTP,所以從嚴格的意義上來說,應該說咱們選擇了Erlang/OTP爲主來構造UMP系統。Erlang/OTP很好的抽象了開發一個分佈式的、高容錯性的應用程序所需的要素,包括:網絡編程框架、序列化和反序列化、容錯、熱部署。
爲了支持併發,服務器端多采用多進程/多線程模型,即每一個進程/線程處理一個客戶端鏈接,受限於操做系統資源,每臺服務器能夠處理的併發鏈接數並不高,同時因爲進程/線程上下文切換開銷,系統的性能會受到影響。而開發高併發、高性能的服務器通常採用事件驅動的狀態機模型,底層採用非阻塞I/O(Linux中的epoll,BSD系統中的kqueue,Java中的nio)或者異步I/O,或者採用異步的事件通知的I/O框架,例如C/C++下的ACE、boost::asio、libevent,Java下的MINA等,在業務層則使用狀態機來表示每一個客戶端鏈接,經過I/O事件、超時事件驅動着狀態機進行跳轉,每一個進程/線程能夠處理成千上萬個客戶端鏈接。事件驅動的狀態機模型和多進程/多線程模型相比雖然併發量更大、性能更好,可是把業務邏輯表達成狀態機是一件困難的事情,相比而下,多進程/多線程模型中業務邏輯能夠實現爲順序執行的代碼,開發起來要簡單的多。
Erlang/OTP中的網絡編程模型則結合了二者的優勢,每一個Erlang進程處理一個客戶端鏈接,業務邏輯是順序執行的。而Erlang進程是極輕量級的,能夠認爲每一個Erlang進程是一個狀態機,堆和棧上的數據是這個狀態機的狀態,而Erlang進程收到數據包或是其餘進程發來消息後執行處理例程至關於狀態機的跳轉,所以也具備高併發和高性能的優點。
Erlang/OTP定義了「external term format」協議將Erlang數據結構與二進制串相互轉化,用C實如今Erlang虛擬機中,跨節點通信時聽從這個協議。所以,開發者無需額外考慮序列化和反序列化的問題。
至於容錯,Erlang進程的數據空間是相互隔離的,沒有共享內存,所以一個Erlang進程崩潰不會影響其餘Erlang進程的運行,更不會形成Erlang虛擬機崩潰。OTP還提供了監督樹機制和heart模塊,前者在監控到Erlang進程崩潰時進行故障恢復,後者發現Erlang虛擬機失去響應時重啓程序。
Erlang/OTP提供熱部署方式,能夠避免服務升級時形成不可用時間。此外,OTP還提供了一些在系統運行時觀察系統狀態的工具,例如lcnt工具,能夠統計虛擬機內部的鎖使用次數和衝突次數,指導系統的優化。
當前系統架構
在設計UMP系統時,咱們遵循瞭如下幾條原則:
l 系統對外保持單一入口,對內維護單一的資源池。
l 保證服務的高可用性,消除單點故障。
l 保證系統是彈性可伸縮的,能夠動態的增長、刪減計算與存儲節點。
l 保證分配給用戶的資源也是彈性可伸縮的,資源之間相互隔離。
UMP系統中的角色包括:controller服務器、proxy服務器、agent服務器、API/Web服務器、日誌分析服務器、信息統計服務器。
依賴的開源組件有:Mnesia、LVS、RabbitMQ、ZooKeeper
圖3當前UMP系統架構圖
Mnesia是OTP提供的分佈式數據庫,與MySQL NDB出自同門,都是上世紀90年代中期Ericsson爲電信業務研發的數據產品。Mnesia支持事務,支持透明的數據分片,利用兩階段鎖實現分佈式事務,能夠線性擴展到至少50個節點。從CAP理論的角度上來講,Mnesia更傾向於犧牲可用性來換取強一致性,屬於CP陣營,但它也提供了髒讀、髒寫操做,能夠繞過事務管理去操做數據,這時不保證一致性,這又相似於AP的系統。在工程實踐中,咱們用事務去修改關鍵數據例如路由表,而用髒寫接口去寫非關鍵數據例如用戶的狀態信息,讀取數據用髒讀接口。
Controller服務器向UMP集羣提供各類管理服務,實現元數據存儲、集羣成員管理、MySQL實例管理、故障恢復、備份、遷移、擴容等功能。Controller服務器上運行了一組mnesia分佈式數據庫服務,系統的元數據好比集羣成員,用戶的配置和狀態信息,以及用戶名到後端MySQL實例地址的映射關係(路由表)等都存儲在mnesia裏,其它服務器組件經過發送請求到controller服務器獲取用戶數據。爲了達到高可用性,系統中會部署多臺controller服務器,它們會經過ZooKeeper提供的分佈式鎖算法選舉出一個leader,這個leader負責調度和監控各類系統任務,例如建立和刪除數據庫實例、備份、遷移等。這些系統任務能夠分紅多個步驟,並且會涉及到系統中的多個組件,例如主庫、從庫、proxy服務器等,還須要提供失敗時回滾的方法,所以咱們採用相似工做流的方式來實現。每一個系統任務都是分紅多個階段的Erlang進程,每執行完一個步驟跳進入下個步驟以前會把中間狀態持久化到mnesia中,若是任務由於節點故障的緣由中止,leader會檢測到並從新發起該任務,任務重啓後會從上一次失敗的「斷點」繼續向下執行。
API/Web服務器向用戶提供了系統管理界面。它們是基於開源項目Mochiweb與Chicago Boss開發的,Mochiweb提供http/https服務,而Chicago Boss是由Nginx的做者之一Evan Miller開發,提供相似Rails的MVC框架。和Rails比,Erlang開發的框架天生就對併發有很好的支持,每一個請求佔用一個輕量級的Erlang進程,而Rails雖然在最近引入了多線程安全,但處理每條請求的時候仍然是獨佔整個進程的,所以須要使用多進程模型處理併發請求,經過Phusion Passenger等應用服務器進行派發。
Proxy服務器向用戶提供訪問MySQL數據庫的服務,它徹底實現了MySQL協議,用戶可使用已有的MySQL客戶端鏈接到proxy服務器,proxy服務器經過用戶名獲取到用戶的認證信息、資源配額的限制(例如最大鏈接數、QPS、IOPS等),以及後臺MySQL實例的地址(列表),再將用戶的SQL查詢請求轉發到正確的MySQL實例上。除了數據路由的基本功能外,Proxy服務器中還實現了資源限制、屏蔽MySQL實例故障、讀寫分離、分庫分表、記錄用戶訪問日誌的功能。Proxy服務器是無狀態的,服務器宕機不會對系統中其餘服務器形成影響,只會形成鏈接到該proxy的用戶鏈接斷開。多臺Proxy服務器採用LVS HA方案實現負載均衡,用戶應用重連後會被LVS定向到其餘的proxy上。
Agent服務器部署在運行MySQL進程的機器上,用來管理每臺物理機上MySQL實例,執行建立、刪除、備份、遷移、主從切換等操做,收集和分析MySQL進程的統計信息、bin log、slow query log。
日誌分析服務器會存儲和分析Proxy服務器傳入的用戶訪問日誌,並實現了實時索引供用戶查詢一段時間內的慢日誌和統計報表。信息統計服務器按期將採集到的用戶的鏈接數、QPS數值,以及MySQL實例的進程狀態用RRDtool進行統計,能夠畫圖展現到Web界面上,也能夠爲從此實現彈性的資源分配和自動化的MySQL實例遷移提供依據。
UMP系統中各節點間的通訊(不包括SQL查詢、日誌等大數據流的傳輸,這些仍是直接走TCP的)都經過RabbitMQ,做爲消息通信的中間件來使用,來保證消息發送的可靠性。ZooKeeper則主要發揮配置服務器、分佈式鎖,以及監控全部MySQL實例的做用。在多個組件的協同做業下,整個系統實現了對用戶透明的容災、讀寫分離、分庫分表功能。系統內部還經過多個小規模用戶共享同一個MySQL實例,中等規模用戶獨佔一個MySQL實例,多個MySQL實例共享同一個物理機的方式實現資源的虛擬化,下降總體成本。在資源隔離方面,經過Cgroup限制MySQL進程資源,以及在proxy服務器端限制QPS相結合的方法,UMP系統實現了資源虛擬化的同時保障用戶的服務質量。此外,UMP系統綜合運用SSL數據庫鏈接、數據訪問IP白名單、記錄用戶操做日誌、SQL攔截等.