背景
在業務發展到一定階段之后,任何因故障而導致的服務中斷都會帶來巨大的損失。為了提高系統的伸縮能力與高可用能力,我們都不斷的在努力消除系統單點瓶頸。如使用應用集群是為了解決服務層的單點問題,使用主從數據庫是為了解決數據庫層面的單點問題。
盡管我們使用微服務架構,很好的解決了服務治理與監控問題,使得少數服務器出現故障仍不影響整體服務質量。但是由於所有的設備都存放在同一個機房內部,對於機房級的故障是無法承受的,如機房斷電、火災、地震等,造成的后果是災難性的。雖然機房內部很好的解決了單點故障,但是機房本身卻是單點的。
為了提升機房級的容災能力,業界多采用 “兩地三中心” 方案。
兩地三中心
顧名思義,兩地指的是兩個城市:同城,異地。三中心指的是三個數據中心:生產中心、同城容災中心、異地容災中心。
在同一個城市或者臨近的城市建設兩個相同的系統,雙中心具備相當的業務處理能力,機房之間通過高速網絡實時同步數據。
在異地建設災備中心,通過異步傳輸的方式,將雙機房的數據備份至異地災備中心,以應對城市級別的災難。
備份模式
由於金融行業對系統建設要求高,因此在早期絕大部分銀行都采用“兩地三中心”建設方案。在這種模式下,多個中心是主備關系,即只有生產中心對外提供服務,同城容災中心是生產中心的備份,當生產中心無法提供服務時,將流量切換至同城容災中心。當同城雙機房都發生故障時,啟用異地災備中心。
這種模式建設方案簡單,實際上是通過資源的堆砌與冗余來應對不確定事件的發生。但由於對災難的響應和機房的切換周期非常長,無法實現業務的零中斷,對設備資源的利用率低下,因此,近年來各個企業都開始尋求轉變,將系統建設為雙活,使同城雙中心同時對外提供服務,節約成本,同時繼續保留異地容災中心。
雙活模式
雙活不僅僅是將流量切分至兩個機房這么簡單,更多的是要考慮如何能讓用戶的請求在一個機房中就能完成,避免跨機房調用帶來的延時增加,從而影響客戶體驗。
因此,對於雙活架構,要考慮一下幾個方面的因素:
-
業務能否在一個機房內完成整個交易鏈路上所有處理;
-
應用程序如何進行雙活;
-
中間件如何進行雙活;
-
數據庫能否雙活,如何同步。
1.業務能否在一個機房內完成整個交易鏈路上所有處理
業務拆分微服務后,通常由多個服務協作共同完成一筆業務請求。以購買理財為例,請求鏈條為:互聯網網關->理財渠道服務->理財系統->賬務系統。如果這些服務部署在不同的機房,則每次請求都要進行跨機房的訪問,必然會增加性能損耗,造成資源浪費。
2.應用程序如何進行雙活
應用程序雙活,主要需考慮的是業務請求是否是有狀態的。如果是有狀態的請求,則必須指定固定的機房來處理同一筆請求,如用戶的理財數據在A機房,則應該由A機房來處理用戶的購買請求。
為了達到上述要求,通常會在互聯網網關(或者nginx)上,進行流量的分發,根據相應的規則,將請求分發到指定的機房處理。如下圖:
3.中間件如何進行雙活
對於常用的中間件,如redis、kafka、ZooKeeper等,需要考慮雙機房如何進行數據同步。
以redis為例,官方並沒有提供跨機房的主主同步機制。如果僅利用redis的主從數據同步機制的話,需要將主節點與從節點部署在不同的機房。當主節點所在機房出現故障時,從節點可以升級為主節點,應用可以持續對外提供服務。但這種模式下,若要寫數據,則只能通過主節點寫,寫請求有一半還是會跨機房訪問。
若要實現redis的主主同步,需自己研發相應的插件,例如可以通過訂閱mysql的binlog日志來做緩存數據的同步。通過實現同步組件,監聽mysql的binlog並解析,將數據同步到兩個機房的redis集群中。如下圖:
該方案看起來還不錯,但是它具有以下弊端:
- 由於跨機房,數據同步會有幾十到上百毫秒的延時。
- 同步組件將數據寫入到本地redis和遠程redis,由於沒有事務的約束,不能保證兩邊都寫成功,因此有可能會出現不一致。
- redis的數據可以同步,但數據的過期時間無法同步。
- redis具備5種數據結構,需要根據業務提前約定好使用哪種數據結構,業務侵入到了數據同步組件。
3.數據庫能否雙活,如何同步
應用的雙活和中間件的雙活,最終都依賴於數據如何存放。如果兩個機房中各部署一個數據庫,那么機房間的數據如何同步呢。
以mysql為例,業界最常用的做法就是利用binlog做數據同步,最具代表性的就是阿里開源的Canal+Otter數據同步方案。
Canal可以偽裝成一個Mysql Slave,接收binlog文件,獲取到Mysql Master的數據變更,如圖:
Otter可以將Canal獲取的數據,同步到目標數據庫,如圖:
Canal+Otter不僅可以實現同構數據的同步,還能實現異構數據的同步,同時會簡化壓縮要傳輸的binlog,減少網絡壓力,傳輸速度更快。
小結
上面介紹了兩地三中心的備份模式與雙活模式,可以看到,這兩種模式下,每個機房的數據量都是全量的,在某個機房故障時,另外一個機房會接管全部的流量。
然而,對於大的互聯網公司來說,單個機房甚至兩個機房都不能承載業務容量,需要更多的機房來共同對外提供服務,在這樣的場景下,上述所說的雙活方案就不太適用了。因此,支付寶等公司就提出了新的解決方案:單元化。
單元化
所謂單元化,就是將業務划分為一個個小的業務單元,每一個單元的功能完全相同,但只能處理一部分數據,所有單元的數據合並起來,才是完整的數據;麻雀雖小,五臟俱全,每個單元內部都能處理完整的業務流程。如下圖:
單元化要求應用層也可以按照數據層的維度去分片,將整個請求收斂到一個單元內部完成,盡量不與別的單元交互。這樣一個單元就是一個最小的邏輯單位,可以根據需要“搬遷”到不同的機房。在單元化架構下,機房可以橫向擴展(增加或減少),而應用系統無需改造。
而要做到單元化,必須要滿足以下要求:
-
業務必須是可分片的,如購買理財可根據客戶號進行分片
-
單元內的業務是自包含的,調用盡量封閉
-
系統是面向邏輯分區的,而不是物理部署
為了實現單元化,需要由以下關鍵技術組件做支撐。
全局路由網關
由於實施單元化后,整個交易鏈路從前到后的分片規則都是一致的,因此需要在入口處識別用戶請求的所屬單元,直接將請求路由至目標單元處理。這就使得必須有一套機制或系統來專門完成在這項工作,而又因為是在網絡入口處處理,因此需要一個全局路由網關。
此時,需要所有交易盡可能的帶上分片鍵,以便全局路由網關判斷當前交易屬於哪個單元。然而實際應用過程中,並不是所有交易都能帶上分片鍵,這種情況就需要應用跨單元交易轉發組件來處理了。
應用跨單元交易轉發
如上所述,當網關層無法識別交易所屬單元時,就需要在業務層識別處理了。例如單元划分按用戶uid分片,但在登錄場景下,用戶可能使用手機號登錄,也可能使用身份證號登錄,還有可能使用微信登錄(此時使用的是unionid和openid),此時需要先按照請求信息查出uid,然后將交易轉發至該uid所在單元處理。
此時肯定就有小伙伴們想,為什么應用不能直接跨單元訪問數據庫呢,還省去了應用轉發處理的過程。主要原因如下圖:
當應用層直接跨單元訪問數據庫時,每個數據庫都對應多個應用,然而數據庫的連接數是非常寶貴的系統資源,不可能無限增長,這就導致當應用數量達到一定規模時,數據庫連接數會被占滿,此時應用將無法再進行橫向擴容,業務將無法繼續發展。因此不建議應用直接跨單元訪問數據庫,而是通過應用層直接的轉發來處理,每個數據庫只被本單元內的應用訪問。當然,應用層的轉發規則需要與全局路由網關的轉發規則保持一致。
異構索引與分布式事務
上面所描述的登錄過程中,在應用不能跨單元訪問數據庫時,是如何做到根據手機號、身份證號等信息查出用戶的uid呢。這就需要異構索引來支持了。
異構索引即“按照不一樣的結構再建一份索引”。如我們以uid存儲用戶信息,在分片時由於不知道手機號所屬分片,無法直接使用手機號查詢到用戶信息,因此會再存儲一份手機號到uid的映射關系,這個映射關系就是異構索引。通常為了提高性能,會使用redis或者es等中間件來存儲異構索引。
當然,涉及到數據的多處存放,就會涉及到數據的一致性問題,就免不了要實現分布式事務。不僅多個單元之間要實現分布式事務,在數據庫與異構索引之間也要使用分布式事務使其達到數據一致。關於分布式事務的詳細概念及其實現方案,可參考文章《分布式事務基礎概念及其模式比較》。
小結
通過單元化架構,每個單元內部都可以完整的完成業務流程,以盡可能避免跨單元的訪問。通過全局路由網關、應用跨單元交易轉發,可使用一致的單元划分規則,將交易轉發至相應的單元處理。而在不帶分片鍵的交易過程中,要找到目標單元,可通過異構索引實現。
關於單元化的具體技術實現,后面會拆解為多篇文章來描述,敬請期待。
多地多活
在實現單元化架構之后,此時系統是面向邏輯分區的,因此可將某個單元部署至任意數據中心,而應用無須改造。此時系統便實現了多地多活。
在實現多地多活后,需要注意的是,雖然系統是面向邏輯分區的,但是在容災策略上還是要考慮部署位置,做好單元的數據備份工作。通常會將每個單元部署為2-3個備份,不同的備份部署在不同的機房,有一個主節點對外提供服務,在主節點故障時,可快速切換至備份節點,實現業務的零中斷服務。
本文着重介紹了應用在大規模服務下如何做多機房多活,其中單元化是目前業界較好的實踐,涉及到較多技術細節,歡迎大家一起討論。
【參考資料】
《高可用可伸縮微服務架構:基於Dubbo、Spring Cloud和Service Mesh》
《螞蟻金服異地多活單元化架構下的微服務體系》
歡迎關注公眾號:程序員順仔和他的朋友們,回復【資料】,即可獲得多本架構進階電子書籍。