低成本和高性能的MySQL雲數據庫的實現

轉載自:https://blog.csdn.net/ywh147/article/details/8954625

感謝原博主


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

      在“低成本和高性能的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中刪除消息之前崩潰了,那麼重啓後仍然會繼續收到並處理這個消息。

      因此消息的接受方在處理消息時需要保證冪等性(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進行進程間的限制。

 

數據安全

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

 

  • 支持SSL連接,proxy服務器實現了完整的MySQL客戶端/服務器協議,可以與客戶端之間建立加密連接。
  • 通過白名單來設置允許訪問數據庫的IP地址列表,用戶可以把白名單配置成應用服務器的地址,增加賬號的安全性。
  • Proxy服務器會把用戶所有的數據庫操作記錄到日誌分析服務器,安全部門可以定期導出日誌文本,掃描檢查安全漏洞。
  • Proxy服務器可以根據安全部門的要求攔截各種類型的SQL語句,例如全表select *的語句、結果條數超出限額的語句等。

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

 

結束語

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章