李 丹,葉廷東
(廣東輕工職業技術學院 信息技術學院, 廣州 510300)
隨著人類進入移動互聯網(互聯網+)時代,新的業務形態層出不窮,應用程序對后臺性能的要求也越來越高。傳統的單機、單點、關系型數據庫早已不能滿足業界需要,因此很多公司把數據存儲在分布式NoSql數據庫中[1]。 最簡單的分布式存儲,就是在單一機房,通過多服務器組成一個集群等模式來實現。
然而,為了更進一步縮短用戶訪問時間,一般讓用戶就近接入,所以分布式后臺服務都跨機房部署,通過接入層的某種路由算法,可以把用戶請求分配到就近的機房。這樣可以大大縮短數據的傳輸距離,從而提升了用戶體驗。
而且,異地部署可以有容災的好處,如果整個大片區的網絡不通或者機房宕機,另外一個片區的機房可以無縫的接管所有的服務請求,從而提高整個后臺服務的可靠性[2]。
伴隨著后臺業務服務的跨機房部署,存儲也需要跨機房部署,怎么能使部署在不同機房存儲中的數據保持同步,且不影響分布式服務的可用性,是一項巨大的挑戰。本文從分布式系統的基礎理論入手,分析了分布式存儲的特性、應用場景和技術挑戰,利用開源的Redis存儲, RabbitMQ消息隊列等技術,搭建了一個滿足最終一致性,可用性和分區容忍性的“異地多活”分布式存儲系統。
異地多活數據中心是現在傳統大型數據中心的發展趨勢。“異地”相對于同城而言,一般不在同一城域;“多活”是區別于一個數據中心、多個災備中心的模式,前者是多個數據中心都在運行中,所以稱為“多活”, 且互為備份。后者是一個數據中心投入運行,另外一個多個數據中心備份全量數據,平時處于不工作狀態,只有在主機房出現故障的時候才會切換到備用機房。冷備份的主要問題是成本高,不跑業務,當主機房出問題的時候,也不一定能成功把業務接管過來[3]。
異地多活主要有如下好處[4]:
1)服務端離用戶更近,接入層可以把用戶請求路由到離用戶最近的機房,減少了網絡傳輸距離,大大提升了用戶訪問性能和體驗。
2)異地快速容災:如果整個機房(比如整個大區)掛掉或者網絡癱瘓,另外一個異地機房能無縫單獨提供服務,用戶完全無感知,大大提高了服務的可靠性。
在分布式的環境下,設計和部署系統時主要考慮下述3個重要的核心系統需求。
1)一致性(Consistency):所有節點在同一時間具有相同的數據。
2)可用性(Availability):保證對每個請求的成功或者失敗都有響應。
3)分區容錯性(Partition Tolerance):分布式系統在遇到某節點或網絡分區故障的時候,仍然能夠對外提供滿足一致性或可用性的服務。
上述3 個重要的核心系統需求又簡稱為 CAP需求[5],在理論計算機科學中,CAP定理(CAP theorem),又被稱作布魯爾定理(Brewer’s theorem),它指出對于一個分布式計算系統來說,不可能同時滿足以上三點, 如圖1所示,三個核心系統特性沒有交疊的區域。即在構建分布式系統的時候,一致性,可用性,分區容忍性,這三者只可以同時選擇兩樣[6]。

圖1 CAP理論分布式系統的三特性
不幸的是,分區容錯性是實際運營的分布式系統所必需的。設想下,誰能保證系統的各節點永遠保持網絡聯通?一旦網絡出現丟包,系統就不可用。而保證分區容錯性最基本的要求是數據要跨數據中心存儲。所以需要在一致性和可用性中二選其一。
為什么強一致性和高可用性不能同時滿足? 假如需要滿足強一致性,就需要寫入一條數據的時候,擴散到分布式系統里面的每一臺機器,每一臺機器都回復ACK確認后再給客戶端確認,這就是強一致性。如果集群任何一臺機器故障了,都回滾數據,對客戶端返回失敗,因此影響了可用性。如果只滿足高可用性,任何一臺機器寫入成功都返回成功,那么有可能中途因為網絡抖動或者其他原因造成了數據不同步,部分客戶端獨到的仍然是舊數據,因此,無法滿足強一致性。
選擇一致性,構建的就是強一致性系統,比如符合ACID特性的數據庫系統。選擇可用性,構建的就是最終一致性系統。前者的特點是數據落地即是一致的,但是可用性不能時時保證,這意思就是,有時系統在忙著保證一致性,無法對外界服務。后者的特點是時時刻刻都保證可用性,用戶隨時都可以訪問,但是各個節點之間會存在不一致的時刻。
需要注意的是最終一致性的系統不是不保證一致性,而是不在保證可用性和分區容錯性的同時保證一致性。最終我們還是要在最終一致性的各節點之間處理數據,使他們達到一致[7]。
眾所周知,如果數據只是保存在單一節點,就沒有一致性的問題;但是單機房存儲連最基本的“分區容忍性”就保證不了。而對于“異地多活”系統而言,數據必然是跨數據中心存儲的。保存在異地機房NoSql數據庫中的數據要做到可用、并且盡可能一致,因為理論上,任何一個機房在任何時刻要給每一個用戶提供服務,這就給分布式存儲系統帶來如下挑戰[8]:
1)網絡延遲&丟包:異地多活系統由于要跨數據中心存儲, 而跨數據中心的路途遙遠導致的弱網絡質量,數據同步是非常大的挑戰;
2)一致性:用戶可能幾乎同時在兩個機房寫入數據,怎么保寫入的數據不沖突并一致。
鑒于異地多活系統的上述挑戰,及分布式系統的CAP定理,本文設計的分布式存儲系統滿足了分區容錯性和可用性,實現了最終一致性。
滿足分區容錯性,分布式數據在異地存儲,任何一個機房掛掉或者跨區域網絡不通,單機房可以立即提供服務。 對分區錯誤的容忍性可以達到100%。
滿足可用性,分布式存儲本地機房寫成功后就返回給用戶,不等待遠端機房是否寫成功。
為了滿足最終一致性,引入消息中間件進行多地域數據分發,消息中間件可以確保消息不丟失[9]。并對寫入的數據附上時間戳,通過時間戳的記錄和比較機制,確保兩邊同時寫入的數據不沖突。同時,引入一致性校驗和補償機制,數據最終一致性得到進一步的保證。
如圖2 所示,在性能方面,由于redis的卓越表現,選擇redis作為數據的承載[10],在一個機房中部署多個redis,組成一個集群,滿足一個機房對數據的讀寫需求。為了解耦業務層和存儲redis,在redis之上引入一個proxy層,業務通過proxy,按一定的hash策略訪問redis[11]。對于多機房分布高可用方面的需求,在proxy層實現數據在多機房間的互相同步機制,提供最終一致性。在多機房網絡通信方面,數據同步以消息的形式發送。為了保證不丟消息,本文選擇用RabbitMQ作為中間件發送。

圖2 系統架構圖
4.2.1 引入中間代理層redisProxy節點
業務進程通過redisProxy讀寫本地redis。redisProxy對key進行hash,訪問其對應的redis。同時,redisProxy節點做到無狀態,按組管理,一個組內部署多個redisProxy,組成集群。集群內的redisProxy可以方便地水平擴展,業務系統無感知。
業務進程通過配置文件指定需要訪問哪個組的redisProxy。另外,redisProxy節點也對redis的讀寫情況、訪問質量等做統計和監控。
4.2.2 寫沖突問題的解決機制
本設計支持對數據的put(寫覆蓋)操作,而支持對數據的寫覆蓋,必然帶來寫沖突的問題,即兩個機房同時對同一個Key寫入數據,因為時序問題,兩個機房最終呈現的結果可能不一致。為了解決此問題,我們對存儲在系統中的key,維護一份元數據,目前維護的有key的版本,即key寫操作的時間戳[13]。對key的寫操作(插入、更新、刪除),需要將操作發生的時間和本地記錄的key對應的時間戳版本做比較,比版本更新(更晚)的操作將被執行,更舊(更早)的操作將被丟棄。
Redis支持的數據類型比較豐富。除了最基本的string類型,通過上述方法,能夠直接支持外,對set、sorted set、hash結構,通過做一些轉換,也能夠支持。轉換方式如下:為set、sorted set、hash結構中存儲的每個成員維護一份時間戳版本,對key做寫操作時,需要對操作涉及的每個成員做時間戳比較,以決定是執行還是放棄。基于時間戳版本的寫操作流程如圖3所示。

圖3 寫操作流程圖
時間戳機制能夠工作的一個前提是服務器之間同步系統時間。一般線上服務器都有同步系統時間,機器之間系統時間誤差一般不超過1 s,為毫秒(ms)級別。這個能滿足互聯網生產環境對存儲系統最終一致性的要求。同時,為了減少時間沖突,對一個key的讀寫,我們hash到一臺機器上執行。
4.2.3 引入第三方消息隊列,增強同步消息傳遞的可靠性
redisProxy需要保證帶有時間戳的寫操作能夠同步到其他組。為了增加同步消息的可靠性,本設計通過引入一個第三方隊列來滿足對同步的可靠性要求。RabbitMQ是我們本文選定的方案, RabbitMQ實現了高級消息隊列協議(AMQP),RabbitMQ消息中間件有著完善的可靠性機制并且使用方便[14]。通過RabbitMQ對同步消息的持久化、集群部署及mirrored queue等機制,實現寫操作的可靠同步。
4.2.4 平滑的升級擴容機制
為實現升級擴容時部署一個新的組,我們利用redis的主從同步獲取‘舊’的最近未更新的數據,利用RabbitMQ的同步獲取‘新’的最近變化的操作。通過兩者的結合,使新組的數據與已有組一致。
4.3.1 業務節點(redis client)
App業務進程,由配置文件指定通過哪個組的redisProxy訪問存儲系統。對redisProxy的訪問,通過輪詢的方式來均衡負載。為了接口使用方便友好,redis client提供類似于redis的接口。
4.3.2 redisProxy
訪問代理層,負責對redis讀寫訪問。對外提供網絡協議接口訪問存儲系統。底層存儲用redis,將數據存在內存中。內部對數據按key進行hash分片,每片存儲在一個redis中。寫操作帶上時間戳,通過RabbitMQ同步到其他組。寫操作成功發送到RabbitMQ即認為同步成功,返回。因此,狀態系統各集群間實現的是最終一致性。
4.3.3 RabbitMQ
第三方消息隊列,負責將redisProxy的寫操作可靠地同步到其他組。通過配置,將同步隊列持久化,防止RabbitMQ重啟后消息丟失[15]。一套狀態系統內,部署多個RabbitMQ,組成集群,防止RabbitMQ單點失敗。配置mirrored queue,使同步隊列在集群中有多個鏡像,進一步提高可靠性。
4.3.4 redis
4.4.1 讀數據流程
讀數據流程如圖4所示。

圖4 讀數據流程圖
1)業務節點發消息給RedisProxy節點,此消息是基于TCP的網絡消息,由業務自定義。
2)RedisProxy節點收到業務讀請求后,按照Key Hash到某個Redis本地分片。
3)執行標準的Redis命令,從本地redis讀取數據。
4)標準Redis命令的回包。
5)業務收到Redis命令的回復后,返回給業務節點一個回包(此回包也是由業務定義的基于TCP的網絡消息)
4.4.2 寫數據流程
寫數據流程如圖5所示。

圖5 寫數據時序圖
1)業務節點發消息給RedisProxy節點,此消息是基于TCP的網絡消息,由業務自定義。
2)RedisProxy把寫操作請求同步給RabbitMQ;
3)RedisProxy節點收到業務讀請求后,按照Key Hash到某個Redis本地分片。
4)步驟4.1、4.2、4.3、4.4為一個事務操作,通過比較操作的時間戳和本地保存的時間戳來決定是否執行本次操作,以避免寫沖突,確保兩邊數據一致。
4.4.3 同步遠端數據流
同步遠端數據流如圖6所示。
1)本地RabbitMQ從遠端RabbitMQ收到寫同步消息;
2)推送同步消息到本機房的RedisProxy節點;
3)RedisProxy節點處理推送過來的寫請求,按key Hash到某個本地Redis分片(步驟3.1); 步驟3.2、3.3、3.4、3.5是一個事務操作,通過比較操作的時間戳和本地保存的時間戳大小來決定是否執行本次操作,以避免寫沖突,確保兩邊數據一致。

圖6 同步遠端數據
4.5.1 redisProxy服務器宕機
redisProxy多臺機集群化部署提高可用性。如果某臺服務器宕機,其redisProxy服務器可以繼續提供服務。
4.5.2 RabbitMQ服務器宕機
同步消息隊列持久化,同時RabbitMQ在多臺機集群化部署,同步消息在集群中有多個鏡像。如果某臺服務器宕機,集群中的其他RabbitMQ可以繼續提供服務。宕機恢復后的RabbitMQ,追趕上其他RabbitMQ后可以繼續提供服務。
4.5.3 Redis服務器宕機
當做整個集群不可用,切換到另一個狀態系統集群。當機器恢復后,按升級擴容的策略對待,重新部署該組狀態系統。等追趕上其他集群后,可開始對外提供服務,將業務流量切換到本集群。
4.5.4 集群機房網絡不可用
切換業務流量到其他集群,繼續提供服務。等機房恢復后,通過RabbitMQ獲取其他集群中存儲著的同步消息,本地回放,追趕數據。同步隊列處理完,即已追趕上其他集群,此時可將業務流量切換回,對外提供服務。
如圖7所示,升級擴容的步驟如下:
1)在新機房部署redis從庫,同步現有機房的數據。
2)在新機房部署RabbitMQ,同步并存儲更新操作。
3) 在redis主庫中寫入一測試數據,測試從庫是否同步上。
4) 第三步中的測試數據,如果已同步到從庫,將從庫提升為主庫,同時啟動redisProxy,開始回放RabbitMQ中的同步消息。

圖7 回放同步消息
通過腳本,定期隨機抽取一批key,比較在各集群之間的數據是否一致。如發現不一致,人工介入校正,根據業務特性做一致性補償等措施。
用C++編寫模擬測試代碼訪問redisProxy對系統進行性能測試,跨機房部署,分別從兩個機房對多個key進行讀寫。用Redis-set、Redis-get 分別表示Redis寫、Redis讀命令。 通過代碼分別統計出redisProxy的平均處理時延, 吞吐量,和數據一致性時延等指標。
環境搭建:異地兩個機房 RedisProxy部署在單臺服務器,CPU: E5-2620/2.10 GHz,內存: 32G, 操作系統 ubuntu 12.04。異地兩個機房,分別部署4個Redis 實例,Key-Value 通過鍵名hash到不同的redis實例。
實驗1:測試系統的吞吐量和系統處理時延。
分別以每秒1個、 100個、 1 000個, 5 000個、10 000個讀寫不同的key請求, 逐步增大系統的請求數, 觀察系統的CPU指標和系統延時。
結果分析: 如圖8和圖9所示,系統時延和CPU隨著請求數增加而緩慢增加。 當請求數接近達到1萬/秒時,系統延時和CPU利用率明顯增大,所以單redisProxy進程部署的QPS接近1萬。

圖8 系統處理時延與請求數的關系

圖9 CPU利用率與請求數的關系
實驗2:在保持系統80% QPS情況下,對某一個key反復進行寫操作,寫完之后分別在兩個機房實時讀,記錄兩個機房不同數據結構數據一致的平均時間差。
結果分析:如圖10所示,實驗結果可以看出,不同數據結構的數據同步時間有略微差別,但是都在1s以內,可以滿足工業級數據同步要求。

圖10 系統一致性時間
跨機房的存儲設計是業界的難點,根據CAP理論,根本做不到同時滿足一致性要求和可用性的系統。本文根據分布式存儲應用的特點,選擇了滿足可用性和最終一致性。 通過多機集群,異地部署等保證可用性;通過開源的可靠消息隊列技術和定時一致性校驗&補償技術來確保最終一致性。經過壓測,系統基本能滿足工業級互聯網應用吞吐量和可用性的要求。 NoSql技術、Web應用的規模和使用模式的發展,為分布式存儲和相關領域的發展帶來了新的契機。近年來,業界開始研究CRDT(Conflict-Free Replicated Data Type)數據結構,CRDT是各種基礎數據結構最終一致算法的理論總結,能根據一定的規則自動合并,解決沖突,達到強最終一致的效果[16]。這可以作為本課題的以后研究方向。