資料聚集(低成本和高性能的MySQL雲數據庫的實現 2)

做者: 鳴嵩/曹偉(集團技術專家) 

 本文刊登於《程序員》雜誌2012年12期上,轉載請註明 程序員

 

UMP(Unified MySQL Platform)系統是淘寶核心系統數據庫團隊開發的低成本和高性能的MySQL雲數據方案,關鍵模塊採用Erlang語言實現。系統中包含了controller服務器、proxy服務器、agent服務器、API/Web服務器、日誌分析服務器、信息統計服務器等組件,而且依賴於Mnesia、LVS、RabbitMQ、ZooKeeper等開源組件。 web

在「低成本和高性能的MySQL雲數據庫的架構探索」一文中,咱們介紹了UMP的系統結構和各個組件的功能,本文裏,咱們會進一步來探索RabbitMQ和ZooKeeper在系統中的應用以及proxy服務器的實現,整個系統如何實現容災、讀寫分離、分庫分表等功能,介紹資源管理、隔離和調度等技術,以及在保障用戶數據安全上的作法。 算法

 

RabbitMQ 數據庫

RabbitMQ是一個用Erlang開發的工業級的消息隊列產品。集羣中各節點間的通訊(不包括SQL查詢、日誌等大數據流的傳輸,這些仍是直接走TCP的)都經過RabbitMQ,做爲消息通信的中間件來使用,來保證消息發送的可靠性。 編程

集羣初始化時會在RabbitMQ中爲集羣裏的每一個節點建立一個隊列,做爲節點的「信箱」。節點間發送消息時無論對方在不在線,只要寫消息到對方的「信箱」裏便可,接下來由對方節點上運行的RabbitMQ客戶端接收消息,調用相應的處理例程。消息處理完後,客戶端會回覆一個ACK包到RabbitMQ,從「信箱」中刪除這條消息。基於RabbitMQ能夠實現RPC,客戶端除了回覆ACK包給RabbitMQ刪除Request消息外,還向發送者的「信箱」寫入一條Reply消息。RabbitMQ是支持事務的,能夠保證刪除Request消息和寫Reply消息在一個原子操做中完成。 安全

alt

圖1節點之間經過RabbitMQ實現RPC 服務器

若是接收者在處理消息的過程當中崩潰了,那麼消息還會存儲在RabbitMQ中,重啓後,消息會再次推送過來,由接收者繼續處理。 網絡

RabbitMQ能夠保證消息被髮送出去,被接收者處理,但不幸的是,沒法保證消息只被發送/處理一次,主要緣由在於RabbitMQ不支持XA。首先,發送者將消息寫到MQ和在本地寫一條日誌不能在同一個事務中完成,若是發送者將消息寫到MQ以後,在本地寫日誌以前崩潰了,重啓後沒法肯定消息是否被髮送,只能嘗試重發;一樣,消息的接收方沒法將處理消息和從MQ中刪除消息放在同一個事務中完成,若是消息的接收方在處理完消息以後,從MQ中刪除消息以前崩潰了,那麼重啓後仍然會繼續收到並處理這個消息。 session

所以消息的接受方在處理消息時須要保證冪等性(idempotent),即同一條消息被處理多遍不會有反作用,好比controller向agent發送備份命令時能夠捎帶上一次備份的時間點,agent檢查這個時間點一致後再執行備份操做,這樣能夠保證同一條備份命令被髮送屢次時不會建立多個備份。 多線程

利用RabbitMQ的路由功能(Exchange)還能夠實現消息廣播,例如系統中會建立一個叫proxy的Exchange,類型配置爲'fanout',當有新的proxy服務器註冊時,節點的「信箱」就會綁定到該Exchange上。這樣當controller服務器須要向全部的proxy服務器發送通知時,好比執行主備切換操做,發送到Exchange上的消息會寫入全部proxy服務器的「信箱」中。

RabbitMQ還實現了一種鏡像隊列(mirrored queue)的算法提供HA。建立隊列時能夠經過傳入「x-ha-policy」參數設置隊列爲鏡像隊列,鏡像隊列會存儲在多個Rabbit MQ節點上,並配置成一主多從的結構,能夠經過「x-ha-policy-params」參數來具體指定master節點和slave節點的列表。全部發送到鏡像隊列上的操做,好比消息的發送和刪除,都會先在master節點上執行,再經過一種叫GM(Guaranteed Multicast)的原子廣播(atomic broadcast)算法同步到各slave節點。GM算法經過兩階段的提交,能夠保證master節點發送到全部slave節點上的消息要麼所有執行成功,要麼所有失敗;經過環形的消息發送順序,即master節點發送消息給一個slave節點,這個slave節點依次發送給下一個slave節點,最終消息回到master節點,保證了主從節點上的負載差異不大。

 

ZooKeeper

ZooKeeper在分佈式集羣中提供分佈式鎖、名字服務等,它把分佈式集羣比作動物園,而本身則扮演動物園管理員的角色。ZooKeeper最先是由Yahoo!開發,應用在Hadoop軟件棧中發揮Google Chubby的做用,咱們在項目中單獨使用ZooKeeper,實現三個功能:

1.做爲全局的配置服務器。配置文件原先是放在本地的,變動配置須要到全部的節點上去修改,這不只是重複性的工做並且容易出錯。放在ZooKeeper上後,全部節點都監視配置文件的變化,文件一旦被修改,全部節點都會從新加載並觸發相應動做。

2.提供分佈式鎖。集羣中部署了多個controller服務器經過熱備實現HA,但這些controller服務器不能同時執行同一個操做。例如,一個MySQL實例掛掉後,若是全部的controller服務器都去跟蹤處理而且發起主備切換流程,proxy服務器和agent服務器就會收到多條切換的命令,集羣就亂套了。所以簡單起見,咱們規定同一時間,整個集羣中多個controller服務器只能選舉出一個leader,由這個leader負責發起各類系統任務。Leader的選舉功能就是經過ZooKeeper的分佈式鎖功能實現的。

3.監控全部MySQL實例。咱們爲MySQL服務器開發了一個ZooKeeper客戶端插件,啓動後會鏈接到ZooKeeper服務器上並建立一個臨時節點,若是MySQL進程死掉,通過5秒的超時時間,這個臨時節點就會被刪除,從而被後臺運行的監控daemon檢查到,若是死掉的MySQL進程是主庫的話則觸發主從切換流程,是從庫的話則從庫的讀權重被設置爲0。

 

容災

當MySQL服務器出現故障時,系統會執行對用戶透明的故障恢復過程,用戶感知不到主庫宕機和上線事件,proxy服務器向用戶隱藏了這些事件,提供給用戶的是一直可用的數據庫鏈接。

對每一個用戶,系統中都會維護主庫和從庫兩個MySQL實例,而把主從庫的複製(Replication)關係配置成Dual Master結構,即兩個MySQL實例都把對方設置爲本身的Master,從對方讀取數據更新,複製到本地,這樣向其中任意一個MySQL實例寫入數據,都會更新到另外一個實例上。Dual Master結構存在的問題是,若是兩個MySQL實例同時修改同一行數據,就會有發生衝突的可能性,最終寫到兩個實例中的數據版本不相同,所以爲了保證數據的一致性還須要保持"single write",即只向主庫中寫入數據,這點由proxy服務器來保證。

當主庫宕機後,MySQL插件在ZooKeeper上保持的臨時節點會由於會話超時而被刪除掉,controller服務器檢測到這一事件後,會發起主從切換操做,在路由表中把主庫標記爲不可用狀態,並經過RabbitMQ通知全部的proxy服務器執行切換。

當宕機的主庫再次上線時,策略會稍微複雜一點。這時候從庫中的數據比主庫要新一些,主庫須要一段時間執行更新,當主庫的版本接近從庫時,controller服務器會發送停寫命令到從庫,等待主庫和從庫狀態徹底一致後,發起主從切換操做,在路由表中恢復主庫爲活動狀態並通知proxy服務器把寫操做切回主庫上,所有完成後再把從庫修改成可寫狀態。從上述過程能夠看出,把主從庫的複製關係配置爲Dual Master結構,簡化了執行主從切換的步驟。

上述過程當中,宕機的主庫再次上線會使用戶感覺到短期的不可寫,進一步的,proxy服務器端能夠經過捕捉錯誤,延遲重試的方法屏蔽掉這個問題。

 

讀寫分離

咱們還實現了對用戶透明的讀寫分離。當功能的開關打開時,proxy服務器會解析用戶傳入的SQL語句,將寫操做發送到主庫,讀操做負載均衡的分發到主庫與從庫上執行。爲了不用戶剛寫入數據到主庫,在同步到從庫以前就去讀從庫,從而讀不到或者讀到舊版本的狀況出現,咱們在每次寫操做發生後都會添加一個計時器,用戶每次寫操做後300毫秒內讀任何數據都會強行分發到主庫。經過主從多線程複製技術,300毫秒基本能夠保證數據從主庫同步到從庫,而這個值也能夠在配置中調節。

proxy服務器還須要解析MySQL鏈接相關的屬性,例如用戶經過鏈接參數或者「use database」語句設置的默認庫,以及經過set語句設置的會話變量(session variables)等,將這些參數設置到主庫和從庫的鏈接上,並記錄到一張內存表中,當與後臺數據庫新建鏈接或與斷開重連時,會從新設置這些環境參數,避免讓用戶感知到差別。

 

分庫分表

咱們還實現了對用戶透明的分庫分表(shard / horizontal partition)。在建立用戶帳號的時候就須要指定類型爲多實例,並設置實例的個數,會建立多組MySQL實例。用戶建表時須要指定分庫分表的規則,規則中須要指定分庫分表的字段(partition key),partition key怎麼映射到分表上去,分表怎麼映射到多個實例上去。這些規則能夠經過在建表語句前添加SQL註釋的方式的傳入。

首先,proxy服務器會對用戶傳入的SQL語句進行語法分析,抽取出重寫和分發SQL語句所須要的信息,例如SQL語句操做的表名,插入語句中每條記錄裏partition key所對應的值(必須包含該值),查詢語句中的where子句中的條件,order by、group by語句中的字段,以及limit語句中對結果條數的限制等。目前,支持的SQL限於insert,select,update,delete這四種DML語句的基本形式,錶鏈接和嵌套select查詢目前還不支持,order by和group by也限於單個字段,這些地方還要繼續投入人力去實現與完善。

下一步,是將SQL語句重寫爲到各個分表上去執行的子語句的形式,主要是表名替換和where條件改寫,接着將子語句併發的發送到對應的分表上去執行。

最後,是接收與合併各個子表上返回的執行結果。爲了不查詢語句的結果集過大撐爆proxy服務器的內存,或者是在用戶只須要一部分結果的狀況下減少通迅開銷,咱們對查詢結果得接收與合併過程作了一些優化。經過設置緩衝區大小,能夠限制MySQL實例每次返回的結果行數,當全部分表上都返回部分結果後,就開始執行歸併排序,並將排好序的結果返回給用戶,當來自某個分表的結果都用完後,再去讀socket填充緩衝區,獲取下一批結果。整個過程比較相似於搜索引擎中將查詢分發到檢索服務器再進行結果合併的過程。

alt

圖2 Proxy服務器的實現層次

爲了提高性能,SQL的解析、重寫以及合併多個MySQL服務器返回的結果集均是用C++實現,經過NIF接口方式被Erlang語言編寫的狀態機調用。

 

資源管理

咱們參考了VMware DRS等雲計算系統中資源管理的方法,實現了一套資源池機制來管理數據庫服務器上的CPU、內存、磁盤等計算資源。管理員先按照整個集羣全部服務器的機型、所在機房等因素劃分多個資源池,服務器上的agent進程啓動後會註冊到controller節點上,管理員再經過web管理界面將每臺服務器加入到合適的資源池中。

分配實例的單位是資源池,管理員能夠根據應用部署在哪些機房、須要的計算資源等因素分別指定主庫、從庫所在的資源池,實例管理服務再從資源池中選擇負載較輕的服務器來建立實例。後期咱們還將開發資源池內的調度管理,若是資源池中一臺服務器的負載長期明顯高於其餘服務器,調度進程會將其中的MySQL實例遷出到低負載的機器上。

除了將服務器劃分爲資源池,在每臺服務器內部,咱們也結合Cgroup將它的資源進一步的細化以方便管理和隔離。例如,一臺16核,48G的服務器,咱們會將它的資源劃分到16個進程組中,至關於每一個進程組分配到一個CPU核和2G的內存,這樣一個進程組中能夠放入8個內存規格爲256M的MySQL進程,而一個須要4G內存的MySQL進程能夠經過合併兩個進程組來實現。Cgroup能夠限制每一個進程組使用資源的上限,也能夠保證進程組之間相互隔離。還有一點是,這種資源管理方式是可能形成碎片的,例如向16個進程組每一個組裏都分配一個內存256M的MySQL進程,這時總共才佔用4G內存,服務器上還有44G空閒內存,但此時已經沒法分配出一個內存4G的MySQL進程了,這個問題能夠經過Buddy System來解決。

 

資源調度

目前系統中支持三種規格的用戶:

第一種是數據量和流量比較小的用戶,例如博客站點、小應用以及開發中的應用。多個小用戶能夠共享同一個MySQL實例,每一個用戶一個庫,單機能夠支持幾百到上千個小用戶,但文件數量過多會對系統性能有不利的影響。

第二種是中等規模的用戶,每一個用戶獨佔一個MySQL實例,每一個實例佔用的內存從256M到32G不等。用戶的內存空間和磁盤空間也是能夠調節的,當前機器知足不了用戶對資源的要求時,能夠遷移到資源有空閒或者更高配的服務器上。

alt

圖3經過實例遷移實現資源調度

第三種是須要分庫分表的用戶,用戶能夠佔有多個獨立的MySQL實例。這些實例能夠同其餘實例共存在同一臺物理機上,也能夠由於業務數據量規模的增加每一個實例獨佔一臺物理機。

用戶的規格能夠在建立的時候指定,也能夠經過遷移工具升級或降級。咱們使用了集團中間件團隊開發的愚公系統,這是一個全量複製結合bin log分析進行增量複製的工具,能夠實如今不停機的狀況下動態擴容、縮容和遷移。目前,用戶規格的升級和降級須要在控制檯上觸發,未來,咱們但願能夠基於用戶過去一段時間數據庫使用狀況的統計信息進行自動化的調度。

 

資源隔離

當多個用戶共享同一個MySQL實例,或者是多個MySQL實例共享同一臺物理機時,資源隔離顯得尤其重要。例如某用戶執行了一條IO操做很是多的SQL語句,例如沒有爲字段設置索引形成在一張大表上進行全表掃描,會嚴重影響其餘用戶的體驗。目前咱們採用在數據庫服務器上用Cgroup限制MySQL進程資源,以及在proxy服務器端限制QPS相結合的方法進行資源隔離。

第一種方法是,是經過創建進程組,利用Cgroup的cpuset、memcg以及blkio子模塊分別限制用戶的MySQL進程最大可使用的CPU使用率、內存和IOPS。這種方法適用於多個MySQL實例共享同一臺物理機的狀況。

第二種方法,是經過在數據庫端部署的agent服務器分析MySQL進程的slow query log,採集和彙總用戶最近執行的SQL語句的開銷,並按期將信息反饋到controller服務器,controller服務器將數據同用戶的配額進行比較,若是明顯超出,會通知proxy端經過增長延遲的方法去限制用戶的QPS,達到了減少該用戶消耗的系統資源的目的。這種方法比較適用於多個用戶共享同一個MySQL實例的狀況,由於沒法使用Cgroup進行進程間的限制。

 

數據安全

用戶和企業的安所有門都會比較關心數據的安全問題,咱們實現了多種方法保證用戶數據的安全性:

l 支持SSL鏈接,proxy服務器實現了完整的MySQL客戶端/服務器協議,能夠與客戶端之間創建加密鏈接。

l 經過白名單來設置容許訪問數據庫的IP地址列表,用戶能夠把白名單配置成應用服務器的地址,增長帳號的安全性。

l Proxy服務器會把用戶全部的數據庫操做記錄到日誌分析服務器,安所有門能夠按期導出日誌文本,掃描檢查安全漏洞。

l Proxy服務器能夠根據安所有門的要求攔截各類類型的SQL語句,例如全表select *的語句、結果條數超出限額的語句等。

後期,咱們還會保留MySQL實例的bin log和slow query log,這樣用戶在誤操做刪除數據又沒有備份的狀況能夠經過bin log工具恢復數據,後臺會按期運行slow query log分析工具,對用戶SQL執行過程當中索引使用狀況、IO操做數量等進行分析,指導用戶改進SQL語句。

 

結束語

 

在工程實踐中,咱們堅持着不去重複發明輪子的原則,充分利用開源的、成熟的技術和工具。例如咱們在Erlang的網絡編程框架上實現高性能的proxy服務器,基於RabbitMQ實現消息中間件,使用ZooKeeper管理服務器心跳,也充分利用了集團內部成熟的數據備份、遷移、擴容/縮容方案及其餘bin log工具。這一原則使得咱們能夠將有限的資源關注在下降成本和改善用戶體驗上。

相關文章
相關標籤/搜索