李 丹,葉廷東
(廣東輕工職業(yè)技術(shù)學(xué)院 信息技術(shù)學(xué)院, 廣州 510300)
隨著人類進入移動互聯(lián)網(wǎng)(互聯(lián)網(wǎng)+)時代,新的業(yè)務(wù)形態(tài)層出不窮,應(yīng)用程序?qū)笈_性能的要求也越來越高。傳統(tǒng)的單機、單點、關(guān)系型數(shù)據(jù)庫早已不能滿足業(yè)界需要,因此很多公司把數(shù)據(jù)存儲在分布式NoSql數(shù)據(jù)庫中[1]。 最簡單的分布式存儲,就是在單一機房,通過多服務(wù)器組成一個集群等模式來實現(xiàn)。
然而,為了更進一步縮短用戶訪問時間,一般讓用戶就近接入,所以分布式后臺服務(wù)都跨機房部署,通過接入層的某種路由算法,可以把用戶請求分配到就近的機房。這樣可以大大縮短數(shù)據(jù)的傳輸距離,從而提升了用戶體驗。
而且,異地部署可以有容災(zāi)的好處,如果整個大片區(qū)的網(wǎng)絡(luò)不通或者機房宕機,另外一個片區(qū)的機房可以無縫的接管所有的服務(wù)請求,從而提高整個后臺服務(wù)的可靠性[2]。
伴隨著后臺業(yè)務(wù)服務(wù)的跨機房部署,存儲也需要跨機房部署,怎么能使部署在不同機房存儲中的數(shù)據(jù)保持同步,且不影響分布式服務(wù)的可用性,是一項巨大的挑戰(zhàn)。本文從分布式系統(tǒng)的基礎(chǔ)理論入手,分析了分布式存儲的特性、應(yīng)用場景和技術(shù)挑戰(zhàn),利用開源的Redis存儲, RabbitMQ消息隊列等技術(shù),搭建了一個滿足最終一致性,可用性和分區(qū)容忍性的“異地多活”分布式存儲系統(tǒng)。
異地多活數(shù)據(jù)中心是現(xiàn)在傳統(tǒng)大型數(shù)據(jù)中心的發(fā)展趨勢?!爱惖亍毕鄬τ谕嵌?,一般不在同一城域;“多活”是區(qū)別于一個數(shù)據(jù)中心、多個災(zāi)備中心的模式,前者是多個數(shù)據(jù)中心都在運行中,所以稱為“多活”, 且互為備份。后者是一個數(shù)據(jù)中心投入運行,另外一個多個數(shù)據(jù)中心備份全量數(shù)據(jù),平時處于不工作狀態(tài),只有在主機房出現(xiàn)故障的時候才會切換到備用機房。冷備份的主要問題是成本高,不跑業(yè)務(wù),當(dāng)主機房出問題的時候,也不一定能成功把業(yè)務(wù)接管過來[3]。
異地多活主要有如下好處[4]:
1)服務(wù)端離用戶更近,接入層可以把用戶請求路由到離用戶最近的機房,減少了網(wǎng)絡(luò)傳輸距離,大大提升了用戶訪問性能和體驗。
2)異地快速容災(zāi):如果整個機房(比如整個大區(qū))掛掉或者網(wǎng)絡(luò)癱瘓,另外一個異地機房能無縫單獨提供服務(wù),用戶完全無感知,大大提高了服務(wù)的可靠性。
在分布式的環(huán)境下,設(shè)計和部署系統(tǒng)時主要考慮下述3個重要的核心系統(tǒng)需求。
1)一致性(Consistency):所有節(jié)點在同一時間具有相同的數(shù)據(jù)。
2)可用性(Availability):保證對每個請求的成功或者失敗都有響應(yīng)。
3)分區(qū)容錯性(Partition Tolerance):分布式系統(tǒng)在遇到某節(jié)點或網(wǎng)絡(luò)分區(qū)故障的時候,仍然能夠?qū)ν馓峁M足一致性或可用性的服務(wù)。
上述3 個重要的核心系統(tǒng)需求又簡稱為 CAP需求[5],在理論計算機科學(xué)中,CAP定理(CAP theorem),又被稱作布魯爾定理(Brewer’s theorem),它指出對于一個分布式計算系統(tǒng)來說,不可能同時滿足以上三點, 如圖1所示,三個核心系統(tǒng)特性沒有交疊的區(qū)域。即在構(gòu)建分布式系統(tǒng)的時候,一致性,可用性,分區(qū)容忍性,這三者只可以同時選擇兩樣[6]。
圖1 CAP理論分布式系統(tǒng)的三特性
不幸的是,分區(qū)容錯性是實際運營的分布式系統(tǒng)所必需的。設(shè)想下,誰能保證系統(tǒng)的各節(jié)點永遠保持網(wǎng)絡(luò)聯(lián)通?一旦網(wǎng)絡(luò)出現(xiàn)丟包,系統(tǒng)就不可用。而保證分區(qū)容錯性最基本的要求是數(shù)據(jù)要跨數(shù)據(jù)中心存儲。所以需要在一致性和可用性中二選其一。
為什么強一致性和高可用性不能同時滿足? 假如需要滿足強一致性,就需要寫入一條數(shù)據(jù)的時候,擴散到分布式系統(tǒng)里面的每一臺機器,每一臺機器都回復(fù)ACK確認后再給客戶端確認,這就是強一致性。如果集群任何一臺機器故障了,都回滾數(shù)據(jù),對客戶端返回失敗,因此影響了可用性。如果只滿足高可用性,任何一臺機器寫入成功都返回成功,那么有可能中途因為網(wǎng)絡(luò)抖動或者其他原因造成了數(shù)據(jù)不同步,部分客戶端獨到的仍然是舊數(shù)據(jù),因此,無法滿足強一致性。
選擇一致性,構(gòu)建的就是強一致性系統(tǒng),比如符合ACID特性的數(shù)據(jù)庫系統(tǒng)。選擇可用性,構(gòu)建的就是最終一致性系統(tǒng)。前者的特點是數(shù)據(jù)落地即是一致的,但是可用性不能時時保證,這意思就是,有時系統(tǒng)在忙著保證一致性,無法對外界服務(wù)。后者的特點是時時刻刻都保證可用性,用戶隨時都可以訪問,但是各個節(jié)點之間會存在不一致的時刻。
需要注意的是最終一致性的系統(tǒng)不是不保證一致性,而是不在保證可用性和分區(qū)容錯性的同時保證一致性。最終我們還是要在最終一致性的各節(jié)點之間處理數(shù)據(jù),使他們達到一致[7]。
眾所周知,如果數(shù)據(jù)只是保存在單一節(jié)點,就沒有一致性的問題;但是單機房存儲連最基本的“分區(qū)容忍性”就保證不了。而對于“異地多活”系統(tǒng)而言,數(shù)據(jù)必然是跨數(shù)據(jù)中心存儲的。保存在異地機房NoSql數(shù)據(jù)庫中的數(shù)據(jù)要做到可用、并且盡可能一致,因為理論上,任何一個機房在任何時刻要給每一個用戶提供服務(wù),這就給分布式存儲系統(tǒng)帶來如下挑戰(zhàn)[8]:
1)網(wǎng)絡(luò)延遲&丟包:異地多活系統(tǒng)由于要跨數(shù)據(jù)中心存儲, 而跨數(shù)據(jù)中心的路途遙遠導(dǎo)致的弱網(wǎng)絡(luò)質(zhì)量,數(shù)據(jù)同步是非常大的挑戰(zhàn);
2)一致性:用戶可能幾乎同時在兩個機房寫入數(shù)據(jù),怎么保寫入的數(shù)據(jù)不沖突并一致。
鑒于異地多活系統(tǒng)的上述挑戰(zhàn),及分布式系統(tǒng)的CAP定理,本文設(shè)計的分布式存儲系統(tǒng)滿足了分區(qū)容錯性和可用性,實現(xiàn)了最終一致性。
滿足分區(qū)容錯性,分布式數(shù)據(jù)在異地存儲,任何一個機房掛掉或者跨區(qū)域網(wǎng)絡(luò)不通,單機房可以立即提供服務(wù)。 對分區(qū)錯誤的容忍性可以達到100%。
滿足可用性,分布式存儲本地機房寫成功后就返回給用戶,不等待遠端機房是否寫成功。
為了滿足最終一致性,引入消息中間件進行多地域數(shù)據(jù)分發(fā),消息中間件可以確保消息不丟失[9]。并對寫入的數(shù)據(jù)附上時間戳,通過時間戳的記錄和比較機制,確保兩邊同時寫入的數(shù)據(jù)不沖突。同時,引入一致性校驗和補償機制,數(shù)據(jù)最終一致性得到進一步的保證。
如圖2 所示,在性能方面,由于redis的卓越表現(xiàn),選擇redis作為數(shù)據(jù)的承載[10],在一個機房中部署多個redis,組成一個集群,滿足一個機房對數(shù)據(jù)的讀寫需求。為了解耦業(yè)務(wù)層和存儲redis,在redis之上引入一個proxy層,業(yè)務(wù)通過proxy,按一定的hash策略訪問redis[11]。對于多機房分布高可用方面的需求,在proxy層實現(xiàn)數(shù)據(jù)在多機房間的互相同步機制,提供最終一致性。在多機房網(wǎng)絡(luò)通信方面,數(shù)據(jù)同步以消息的形式發(fā)送。為了保證不丟消息,本文選擇用RabbitMQ作為中間件發(fā)送。
圖2 系統(tǒng)架構(gòu)圖
4.2.1 引入中間代理層redisProxy節(jié)點
業(yè)務(wù)進程通過redisProxy讀寫本地redis。redisProxy對key進行hash,訪問其對應(yīng)的redis。同時,redisProxy節(jié)點做到無狀態(tài),按組管理,一個組內(nèi)部署多個redisProxy,組成集群。集群內(nèi)的redisProxy可以方便地水平擴展,業(yè)務(wù)系統(tǒng)無感知。
業(yè)務(wù)進程通過配置文件指定需要訪問哪個組的redisProxy。另外,redisProxy節(jié)點也對redis的讀寫情況、訪問質(zhì)量等做統(tǒng)計和監(jiān)控。
4.2.2 寫沖突問題的解決機制
本設(shè)計支持對數(shù)據(jù)的put(寫覆蓋)操作,而支持對數(shù)據(jù)的寫覆蓋,必然帶來寫沖突的問題,即兩個機房同時對同一個Key寫入數(shù)據(jù),因為時序問題,兩個機房最終呈現(xiàn)的結(jié)果可能不一致。為了解決此問題,我們對存儲在系統(tǒng)中的key,維護一份元數(shù)據(jù),目前維護的有key的版本,即key寫操作的時間戳[13]。對key的寫操作(插入、更新、刪除),需要將操作發(fā)生的時間和本地記錄的key對應(yīng)的時間戳版本做比較,比版本更新(更晚)的操作將被執(zhí)行,更舊(更早)的操作將被丟棄。
Redis支持的數(shù)據(jù)類型比較豐富。除了最基本的string類型,通過上述方法,能夠直接支持外,對set、sorted set、hash結(jié)構(gòu),通過做一些轉(zhuǎn)換,也能夠支持。轉(zhuǎn)換方式如下:為set、sorted set、hash結(jié)構(gòu)中存儲的每個成員維護一份時間戳版本,對key做寫操作時,需要對操作涉及的每個成員做時間戳比較,以決定是執(zhí)行還是放棄。基于時間戳版本的寫操作流程如圖3所示。
圖3 寫操作流程圖
時間戳機制能夠工作的一個前提是服務(wù)器之間同步系統(tǒng)時間。一般線上服務(wù)器都有同步系統(tǒng)時間,機器之間系統(tǒng)時間誤差一般不超過1 s,為毫秒(ms)級別。這個能滿足互聯(lián)網(wǎng)生產(chǎn)環(huán)境對存儲系統(tǒng)最終一致性的要求。同時,為了減少時間沖突,對一個key的讀寫,我們hash到一臺機器上執(zhí)行。
4.2.3 引入第三方消息隊列,增強同步消息傳遞的可靠性
redisProxy需要保證帶有時間戳的寫操作能夠同步到其他組。為了增加同步消息的可靠性,本設(shè)計通過引入一個第三方隊列來滿足對同步的可靠性要求。RabbitMQ是我們本文選定的方案, RabbitMQ實現(xiàn)了高級消息隊列協(xié)議(AMQP),RabbitMQ消息中間件有著完善的可靠性機制并且使用方便[14]。通過RabbitMQ對同步消息的持久化、集群部署及mirrored queue等機制,實現(xiàn)寫操作的可靠同步。
4.2.4 平滑的升級擴容機制
為實現(xiàn)升級擴容時部署一個新的組,我們利用redis的主從同步獲取‘舊’的最近未更新的數(shù)據(jù),利用RabbitMQ的同步獲取‘新’的最近變化的操作。通過兩者的結(jié)合,使新組的數(shù)據(jù)與已有組一致。
4.3.1 業(yè)務(wù)節(jié)點(redis client)
App業(yè)務(wù)進程,由配置文件指定通過哪個組的redisProxy訪問存儲系統(tǒng)。對redisProxy的訪問,通過輪詢的方式來均衡負載。為了接口使用方便友好,redis client提供類似于redis的接口。
4.3.2 redisProxy
訪問代理層,負責(zé)對redis讀寫訪問。對外提供網(wǎng)絡(luò)協(xié)議接口訪問存儲系統(tǒng)。底層存儲用redis,將數(shù)據(jù)存在內(nèi)存中。內(nèi)部對數(shù)據(jù)按key進行hash分片,每片存儲在一個redis中。寫操作帶上時間戳,通過RabbitMQ同步到其他組。寫操作成功發(fā)送到RabbitMQ即認為同步成功,返回。因此,狀態(tài)系統(tǒng)各集群間實現(xiàn)的是最終一致性。
4.3.3 RabbitMQ
第三方消息隊列,負責(zé)將redisProxy的寫操作可靠地同步到其他組。通過配置,將同步隊列持久化,防止RabbitMQ重啟后消息丟失[15]。一套狀態(tài)系統(tǒng)內(nèi),部署多個RabbitMQ,組成集群,防止RabbitMQ單點失敗。配置mirrored queue,使同步隊列在集群中有多個鏡像,進一步提高可靠性。
4.3.4 redis
4.4.1 讀數(shù)據(jù)流程
讀數(shù)據(jù)流程如圖4所示。
圖4 讀數(shù)據(jù)流程圖
1)業(yè)務(wù)節(jié)點發(fā)消息給RedisProxy節(jié)點,此消息是基于TCP的網(wǎng)絡(luò)消息,由業(yè)務(wù)自定義。
2)RedisProxy節(jié)點收到業(yè)務(wù)讀請求后,按照Key Hash到某個Redis本地分片。
3)執(zhí)行標準的Redis命令,從本地redis讀取數(shù)據(jù)。
4)標準Redis命令的回包。
5)業(yè)務(wù)收到Redis命令的回復(fù)后,返回給業(yè)務(wù)節(jié)點一個回包(此回包也是由業(yè)務(wù)定義的基于TCP的網(wǎng)絡(luò)消息)
4.4.2 寫數(shù)據(jù)流程
寫數(shù)據(jù)流程如圖5所示。
圖5 寫數(shù)據(jù)時序圖
1)業(yè)務(wù)節(jié)點發(fā)消息給RedisProxy節(jié)點,此消息是基于TCP的網(wǎng)絡(luò)消息,由業(yè)務(wù)自定義。
2)RedisProxy把寫操作請求同步給RabbitMQ;
3)RedisProxy節(jié)點收到業(yè)務(wù)讀請求后,按照Key Hash到某個Redis本地分片。
4)步驟4.1、4.2、4.3、4.4為一個事務(wù)操作,通過比較操作的時間戳和本地保存的時間戳來決定是否執(zhí)行本次操作,以避免寫沖突,確保兩邊數(shù)據(jù)一致。
4.4.3 同步遠端數(shù)據(jù)流
同步遠端數(shù)據(jù)流如圖6所示。
1)本地RabbitMQ從遠端RabbitMQ收到寫同步消息;
2)推送同步消息到本機房的RedisProxy節(jié)點;
3)RedisProxy節(jié)點處理推送過來的寫請求,按key Hash到某個本地Redis分片(步驟3.1); 步驟3.2、3.3、3.4、3.5是一個事務(wù)操作,通過比較操作的時間戳和本地保存的時間戳大小來決定是否執(zhí)行本次操作,以避免寫沖突,確保兩邊數(shù)據(jù)一致。
圖6 同步遠端數(shù)據(jù)
4.5.1 redisProxy服務(wù)器宕機
redisProxy多臺機集群化部署提高可用性。如果某臺服務(wù)器宕機,其redisProxy服務(wù)器可以繼續(xù)提供服務(wù)。
4.5.2 RabbitMQ服務(wù)器宕機
同步消息隊列持久化,同時RabbitMQ在多臺機集群化部署,同步消息在集群中有多個鏡像。如果某臺服務(wù)器宕機,集群中的其他RabbitMQ可以繼續(xù)提供服務(wù)。宕機恢復(fù)后的RabbitMQ,追趕上其他RabbitMQ后可以繼續(xù)提供服務(wù)。
4.5.3 Redis服務(wù)器宕機
當(dāng)做整個集群不可用,切換到另一個狀態(tài)系統(tǒng)集群。當(dāng)機器恢復(fù)后,按升級擴容的策略對待,重新部署該組狀態(tài)系統(tǒng)。等追趕上其他集群后,可開始對外提供服務(wù),將業(yè)務(wù)流量切換到本集群。
4.5.4 集群機房網(wǎng)絡(luò)不可用
切換業(yè)務(wù)流量到其他集群,繼續(xù)提供服務(wù)。等機房恢復(fù)后,通過RabbitMQ獲取其他集群中存儲著的同步消息,本地回放,追趕數(shù)據(jù)。同步隊列處理完,即已追趕上其他集群,此時可將業(yè)務(wù)流量切換回,對外提供服務(wù)。
如圖7所示,升級擴容的步驟如下:
1)在新機房部署redis從庫,同步現(xiàn)有機房的數(shù)據(jù)。
2)在新機房部署RabbitMQ,同步并存儲更新操作。
3) 在redis主庫中寫入一測試數(shù)據(jù),測試從庫是否同步上。
4) 第三步中的測試數(shù)據(jù),如果已同步到從庫,將從庫提升為主庫,同時啟動redisProxy,開始回放RabbitMQ中的同步消息。
圖7 回放同步消息
通過腳本,定期隨機抽取一批key,比較在各集群之間的數(shù)據(jù)是否一致。如發(fā)現(xiàn)不一致,人工介入校正,根據(jù)業(yè)務(wù)特性做一致性補償?shù)却胧?/p>
用C++編寫模擬測試代碼訪問redisProxy對系統(tǒng)進行性能測試,跨機房部署,分別從兩個機房對多個key進行讀寫。用Redis-set、Redis-get 分別表示Redis寫、Redis讀命令。 通過代碼分別統(tǒng)計出redisProxy的平均處理時延, 吞吐量,和數(shù)據(jù)一致性時延等指標。
環(huán)境搭建:異地兩個機房 RedisProxy部署在單臺服務(wù)器,CPU: E5-2620/2.10 GHz,內(nèi)存: 32G, 操作系統(tǒng) ubuntu 12.04。異地兩個機房,分別部署4個Redis 實例,Key-Value 通過鍵名hash到不同的redis實例。
實驗1:測試系統(tǒng)的吞吐量和系統(tǒng)處理時延。
分別以每秒1個、 100個、 1 000個, 5 000個、10 000個讀寫不同的key請求, 逐步增大系統(tǒng)的請求數(shù), 觀察系統(tǒng)的CPU指標和系統(tǒng)延時。
結(jié)果分析: 如圖8和圖9所示,系統(tǒng)時延和CPU隨著請求數(shù)增加而緩慢增加。 當(dāng)請求數(shù)接近達到1萬/秒時,系統(tǒng)延時和CPU利用率明顯增大,所以單redisProxy進程部署的QPS接近1萬。
圖8 系統(tǒng)處理時延與請求數(shù)的關(guān)系
圖9 CPU利用率與請求數(shù)的關(guān)系
實驗2:在保持系統(tǒng)80% QPS情況下,對某一個key反復(fù)進行寫操作,寫完之后分別在兩個機房實時讀,記錄兩個機房不同數(shù)據(jù)結(jié)構(gòu)數(shù)據(jù)一致的平均時間差。
結(jié)果分析:如圖10所示,實驗結(jié)果可以看出,不同數(shù)據(jù)結(jié)構(gòu)的數(shù)據(jù)同步時間有略微差別,但是都在1s以內(nèi),可以滿足工業(yè)級數(shù)據(jù)同步要求。
圖10 系統(tǒng)一致性時間
跨機房的存儲設(shè)計是業(yè)界的難點,根據(jù)CAP理論,根本做不到同時滿足一致性要求和可用性的系統(tǒng)。本文根據(jù)分布式存儲應(yīng)用的特點,選擇了滿足可用性和最終一致性。 通過多機集群,異地部署等保證可用性;通過開源的可靠消息隊列技術(shù)和定時一致性校驗&補償技術(shù)來確保最終一致性。經(jīng)過壓測,系統(tǒng)基本能滿足工業(yè)級互聯(lián)網(wǎng)應(yīng)用吞吐量和可用性的要求。 NoSql技術(shù)、Web應(yīng)用的規(guī)模和使用模式的發(fā)展,為分布式存儲和相關(guān)領(lǐng)域的發(fā)展帶來了新的契機。近年來,業(yè)界開始研究CRDT(Conflict-Free Replicated Data Type)數(shù)據(jù)結(jié)構(gòu),CRDT是各種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)最終一致算法的理論總結(jié),能根據(jù)一定的規(guī)則自動合并,解決沖突,達到強最終一致的效果[16]。這可以作為本課題的以后研究方向。