億級規模的高可用微服務系統,如何輕鬆設計?

說到大規模微服務系統,往往是一些 7*24 時不間斷運行的在線系統。那麼如何設計一個大規模的微服務系統呢?


億級規模的高可用微服務系統,如何輕鬆設計?


圖片來自 Pexels

這樣的系統往往有以下的要求:

  • 高可用。這類的系統往往需要保持一定的 SLA,7*24 時不間斷運行不代表完全不掛,而是有一定的百分比的。

例如我們常說的可用性需達到 4 個 9(99.99%),全年停機總計不能超過 1 小時,約為 53 分鐘,也即服務停用時間小於 53 分鐘,就說明高可用設計合格。

  • 用戶分佈在全國。大規模微服務系統所支撐的用戶一般在全國各地,因而每個地區的人,都希望能夠就近訪問,所以一般不會一套系統服務全國,而是每個地區都要有相應的業務單元,使得用戶可以就近訪問。
  • 併發量大,存在波峰波谷。微服務之所以規模比較大,其實是承載的壓力比較大,而且需要根據請求的波峰波谷進行彈性伸縮。
  • 有故障性能診斷和快速恢復的機制。大規模微服務場景下,運維人員很難進行命令式手動運維來控制應用的生命週期,應該採用聲明式的運維方法。

另外一旦有了性能瓶頸或者故障點,應該有自動發現定位的機制,迅速找到瓶頸點和故障點,及時修復,才能保障 SLA。

戰略設計

為了滿足以上的要求,這個系統絕不是運維組努力一把,或者開發組努力一把,就能解決的,是一個端到端的,各個部門共同完成的一個目標,所以我們常稱為戰略設計。

研發

一個能支撐高併發,高可用的系統,一定是需要從研發環節就開始下功夫的。

首先,每一個微服務都有實現良好的無狀態化處理,冪等服務接口設計

狀態分為分發,處理,存儲幾個過程,如果對於一個用戶的所有的信息都保存在一個進程中,則從分發階段,就必須將這個用戶分發到這個進程,否則無法對這個用戶進行處理。

然而當一個進程壓力很大的時候,根本無法擴容,新啟動的進程根本無法處理那些保存在原來進程的用戶的數據,不能分擔壓力。

所以要將整個架構分成兩個部分,無狀態部分和有狀態部分,而業務邏輯的部分往往作為無狀態的部分,而將狀態保存在有狀態的中間件中,如緩存,數據庫,對象存儲,大數據平臺,消息隊列等。

這樣無狀態的部分可以很容易的橫向擴展,在用戶分發的時候,可以很容易分發到新的進程進行處理,而狀態保存到後端。

而後端的中間件是有狀態的,這些中間件設計之初,就考慮了擴容的時候,狀態的遷移,複製,同步等機制,不用業務層關心。

對於數據的存儲,主要包含幾類數據:

  • 會話數據等,主要保存在內存中。對於保存在內存裡的數據,例如 Session,可以放在外部統一的緩存中。
  • 結構化數據,主要是業務邏輯相關。對於業務相關的數據,則應該保存在統一的數據庫中。
  • 文件圖片數據,比較大,往往通過 CDN 下發。對於文件,照片之類的數據,應該存放在統一的對象存儲裡面。
  • 非結構化數據,例如文本,評論等。對於非結構化數據,可以存在統一的搜索引擎裡面,例如 ElasticSearch。

但是還有一個遺留的問題,就是已經分發,正在處理,但是尚未存儲的數據,肯定會在內存中有一些,在進程重啟的時候,數據還是會丟一些的,那這部分數據怎麼辦呢?

這部分就需要通過重試進行解決,當本次調用過程中失敗之後,前序的進程會進行重試,例如 Dubbo 就有重試機制。

既然重試,就需要接口是冪等的,也即同一次交易,調用兩次轉賬 1 元,不能最終轉走 2 元。

接口分為查詢,插入,更新,刪除等操作:

  • 對於查詢接口來講,本身就是冪等的,不用做特殊的判斷。
  • 對於插入接口來講,如果每一個數據都有唯一的主鍵,也能保證插入的唯一性,一旦不唯一,則會報錯。
  • 對於更新操作來講,則比較複雜,分兩種情況。一種情況是同一個接口,前後調用多次的冪等性。另一種情況是同一個接口,併發環境下調用多次的正確性。

為了保持冪等性,往往要有一個冪等表,通過傳入冪等參數匹配冪等表中 ID 的方式,保證每個操作只被執行一次,而且在實行最終一致性的時候,可以通過不斷重試,保證最終接口調用的成功。

對於併發條件下,誰先調用,誰後調用,需要通過分佈式鎖如 Redis,ZooKeeper 等來實現同一個時刻只有一個請求被執行,如何保證多次執行結果仍然一致呢?則往往需要通過狀態機,每個狀態只流轉一次。

還有就是樂觀鎖,也即分佈式的 CAS 操作,將狀態的判斷、更新整合在一條語句中,可以保證狀態流轉的原子性。樂觀鎖並不保證更新一定成功,需要有對應的機制來應對更新失敗。

其次,根據服務重要度實現熔斷降級、限流保護策略

服務拆分多了,在應用層面就會遇到以下問題:

服務雪崩:即一個服務掛了,整個調用鏈路上的所有的服務都會受到影響。

大量請求堆積、故障恢復慢:即一個服務慢,卡住了,整個調用鏈路出現大量超時,要長時間等待慢的服務恢復到正常狀態。

為了解決這些問題,我們在應用層面實施了以下方案:

通過熔斷機制,當一個服務掛了,被影響的服務能夠及時熔斷,使用 Fallback 數據保證流程在非關鍵服務不可用的情況下,仍然可以進行。

通過線程池和消息隊列機制實現異步化,允許服務快速失敗,當一個服務因為過慢而阻塞,被影響服務可以在超時後快速失敗,不會影響整個調用鏈路。

當發現整個系統的確負載過高的時候,可以選擇降級某些功能或某些調用,保證最重要的交易流程的通過,以及最重要的資源全部用於保證最核心的流程。

還有一種手段就是限流,當既設置了熔斷策略,又設置了降級策略,通過全鏈路的壓力測試,應該能夠知道整個系統的支撐能力。

因而就需要制定限流策略,保證系統在測試過的支撐能力範圍內進行服務,超出支撐能力範圍的,可拒絕服務。

當你下單的時候,系統彈出對話框說 “系統忙,請重試”,並不代表系統掛了,而是說明系統是正常工作的,只不過限流策略起到了作用。

其三,每個服務都要設計有效探活接口,以便健康檢查感知到服務狀態

當我們部署一個服務的時候,對於運維部門來講,可以監控機器的狀態或者容器的狀態是否處於啟動狀態,也可以監控到進程是否啟動,端口是否監聽等。

但是對於已經啟動的進程,是否能夠正常服務,運維部門無法感知,需要開發每個服務的時候,設計一個有效探活接口,讓運維的監控系統可以通過調用這個接口,來判斷進程能夠正常提供服務。

這個接口不要直接返回,而是應該在進程內部探查提供服務的線程是否出去正常狀態,再返回相應的狀態編碼。

只有這樣,開發出來的服務和運維才能合作起來,保持服務處於某個副本數,否則如果一部分服務雖然啟動,但是處於假死狀態,會使得其他正常服務,無法承受壓力。

其四,通過制定良好的代碼檢查規範和靜態掃描工具,最大化限制因為代碼問題造成的系統不可用

要保持線上代碼的高可用性,代碼質量是關鍵,大部分線上問題,無論是性能問題,還是穩定性問題,都是代碼造成的,而非基礎設施造成的。

而且基礎設施的可用率為 99.95%,但是服務層要求的可用率高於這個值,所以必須從業務層高可用來彌補。

除了下面的高可用架構部分,對於每一個服務來講,制定良好的代碼檢查規範和靜態掃描工具,通過大量的測試用例,最大化限制因為代碼問題造成的系統不可用,是必須的,是高可用的基礎。

高可用架構設計

在系統的每一個部分,都要避免單點。系統冗餘往往分管控面和數據面,而且分多個層次,往往每一個層次都需要進行高可用的設計。


億級規模的高可用微服務系統,如何輕鬆設計?


在機房層面,為了高可用應該部署在多個區域,或者多個雲,每個區域分多個可用區進行部署。

對於雲來講,雲的管控要多機房高可用部署,使得任何一個機房故障,都會使得管控依然可以使用。

這就需要管控的組件分佈於至少兩個機房,管控的數據庫和消息隊列跨機房進行數據同步。

對於雲的數據面來講,入口的網關要和機房網絡配合做跨機房的高可用,使得入口公網 IP 和負載均衡器,在一個機房故障的情況下,可以切換至另一個機房。


億級規模的高可用微服務系統,如何輕鬆設計?


在雲之上要部署 Kubernetes 平臺,管控層面 Kubernetes 要實現高可用部署,etcd 要跨機房高可用部署,Kubernetes 的管控組件也要跨機房部署。

當然還有一種情況是機房之間距離比較遠,需要在每一個機房各部署一套 Kubernetes。

這種情況下,Kubernetes 的管控依然要實現高可用,只不過跨機房的高可用就需要應用層來實現了。

在應用層,微服務的治理平臺,例如註冊發現,ZooKeeper 或者 Euraka,APM,配置中心等都需要實現跨機房的高可用。另外就是服務要跨機房部署,實現城市級機房故障遷移能力。

運維

運維一個大規模微服務系統也有不一樣的挑戰。

首先,建議使用的是 Kubernetes 編排的聲明式的運維方式,而非 Ansible 之類命令式的運維方式。

另外,對於系統的發佈,要進行灰度、藍綠髮布,降低系統上線發佈風險。要有這樣的理念,任何一個新上線的系統,都是不可靠的。


億級規模的高可用微服務系統,如何輕鬆設計?


所以可以通過流量分發的模式,逐漸切換到新的服務,從而保障系統的穩定。

其三,完善監控及應對機制,對系統各節點、應用、組件全面地監控,能夠第一時間快速發現並解決問題。


億級規模的高可用微服務系統,如何輕鬆設計?


監控絕非只有基礎設施的 CPU,網絡,磁盤的監控,應用的,業務的,調用鏈的監控都應該有。

而且對於緊急事件,應該有應急預案,應急預案是在高可用已經考慮過之後,仍然出現異常情況下,應該採取的預案,例如三個 etcd 全掛了的情況。

其四,持續關注線上系統網絡使用、服務器性能、硬件存儲、中間件、數據庫燈指標,重點關注臨界狀態,也即當前還健康,但是馬上可能出問題的狀態。

例如網關 PPS 達到臨界值,下一步就要開始丟包了,數據庫快滿了,消息出現大量堆積等等。

DBA

對於一個在線業務系統來講,數據庫是重中之重,很多的性能瓶頸定位到最後,都可能是數據庫的問題。所以 DBA 團隊要對數據庫的使用,進行把關。

造成數據庫性能問題,一方面是 SQL 語句的問題,一方面是容量的問題。

例如查詢沒有被索引覆蓋,或者在區分度不大的字段上建立的索引,是否持鎖時間過長,是否存在鎖衝突等等,都會導致數據庫慢的問題。

因而所有上線的 SQL 語句,都需要 DBA 提前審核,並且要對於數據庫的性能做持續的監控,例如慢 SQL 語句等。

另外對於數據庫中的數據量也要持續的監控,到一定的量就需要改分佈式數據庫 DDB,進行分庫分表,到一定的階段需要對分佈式數據庫進行擴容。

故障演練和性能壓測

再好的規劃也比不上演練,再好的性能評估也比不上在線的性能壓測。

性能問題往往是通過線上性能壓測發現的。線上壓力測試需要有一個性能測試的平臺,做多種形式的壓力測試。

例如容量測試,通過梯度的加壓,看到什麼時候實在不行。摸高測試,測試在最大的限度之上還能承受多大的量,有一定的餘量會保險一些,心裡相對比較有底。

再就是穩定性測試,測試峰值的穩定性,看這個峰值能夠撐一分鐘,兩分鐘還是三十分鐘。還有秒殺場景測試,限流降級演練測試等。

只有經過性能壓測,才能發現線上系統的瓶頸點,通過不斷的修復和擴容瓶頸點,最終才能知道服務之間應該以各種副本數的比例部署,才能承載期望的 QPS。

對於可能遇到的故障,可以進行故障演練,故意模擬一些故障,來看系統如何反應,是否會因為自修復,多副本,容錯等機制,使得這些故障對於客戶端來講沒有影響。

戰術設計

下面,我們就從架構的每個層次,進行戰術設計。我們先來看一下高可用部署架構選型以及他們的優劣:


億級規模的高可用微服務系統,如何輕鬆設計?


高可用性要求和系統的負載度和成本是強相關的。越簡單的架構,部署成本越低的架構,高可用性越小,例如上面的單體應用。

而微服務化,單元化,異地多活,必然導致架構複雜難以維護,機房成本比較高,所以要使用多少成本實現什麼程度的高可用,是一個權衡。

高可用的實現需要多個層次一起考慮:


億級規模的高可用微服務系統,如何輕鬆設計?


首先是應用層,可以通過異地多活單元保證城市級高可用,這樣使得一個城市因為災難宕機的時候,另外一個城市可以提供服務。

另外每個多活單元採用雙機房保證機房級高可用,也即同城雙機房,使得一個城市中一個機房宕機,另一個機房可以提供服務。

再者每個機房中採用多副本保證實例級高可用,使得一個副本宕機的時候,其他的副本可以提供服務。

其次是數據庫層,在數據中心之間,通過主從複製或 MGR 實現數據異步複製,在每個集群單元中採用 DDB 分庫分表,分庫分表中的每個實例都是有數據庫同步複製。

其三是緩存層,在數據中心之間,緩存採用多集群單元化複製,在每個集群單元中採用多副本主從複製。

其四微服務治理平臺層,平臺組件異地多活單元保證了城市級高可用,平臺組件每個多活單元採用雙機房保證機房級高可用,平臺組件每個機房中採用多副本保證實例級高可用。

當有了以上高可用方案之後,則以下的故障等級以及影響時間如下表格:


億級規模的高可用微服務系統,如何輕鬆設計?


接下來,我們每個層次詳細論述。

應用層

下圖以最複雜的場景,假設有三個城市,每個城市都有兩個完全對等的數據中心。三個城市的數據中心也是完全對等的。

我們將整個業務數據按照某個維度分成 A,B,C 三部分。這樣任何一部分全部宕機,其他部分照樣可以提供服務。

對於有的業務,如果省級別的服務中斷完全不能忍受,市級別的服務中斷要求恢復時間相當短,而區縣級別的服務中斷恢復時間可以相對延長。

在這種場景下,可以根據地區來區分維度,使得一個區縣和另外一個區縣的數據屬於不同的單元。

為了節約成本,模型可能會更加簡化。中心節點和單元化節點不是對稱的。中心節點可以實現同城雙活,而異地單元化的部分只部署一個機房即可。這樣是能滿足大部分高可用性需求的。

這種架構要求實現中間件層和數據庫層單元化,這個我們後面會仔細講。


億級規模的高可用微服務系統,如何輕鬆設計?


接入層

單元化要求 App 層或者在機房入口區域的接入層,實現中心單元和其他單元節點的流量分發。

對於初始請求沒有任何路由標記的,可以隨機分發給任何一個單元,也可以根據地區或者運營商在 GSLB 中分發給某個就近的單元。

應用層接收到請求以後,根據自己所在的單元生成路由信息,將路由信息返回給接入層或者 App。

接下來 App 或者接入層的請求,都會帶著路由信息,選擇相應的單元進行發送,從而實現了請求的處理集中在本單元。


億級規模的高可用微服務系統,如何輕鬆設計?


中間件層

在中間件層,我們以 ZooKeeper 為例,分為以下兩個場景:

場景一:ZooKeeper 單元化主從多活

在這種場景下,主機房和單元化機房距離相隔較近,時延很小,可以當做一個機房來對待。可以採用 ZooKeeper 高可用保障通過多 ZooKeeper 實例部署來達成。

如圖所示,主機房 ZooKeeper 有 Leader 和 Follower,單元化機房的 ZooKeeper 僅為 Observer。


億級規模的高可用微服務系統,如何輕鬆設計?


場景二:ZooKeeper 單元化多集群複製

兩個機房相距較遠,每個機房部署一套 ZooKeeper 集群,集群之間進行數據同步。

各機房應用連接機房內的 ZooKeeper 集群,註冊的信息通過數據同步,能夠被其他機房應用獲取到。

單一機房 ZooKeeper 集群不可用,其餘機房不受影響。當前不考慮做不同機房之間的集群切換。


億級規模的高可用微服務系統,如何輕鬆設計?


數據庫層

在數據庫層,首先要解決的問題是,分佈式數據庫 DDB 集群多機房同步複製。

在單元內採用同城主從複製模式,跨單元採用 DTS/NDC 實現應用層數據雙向同步能力。


億級規模的高可用微服務系統,如何輕鬆設計?


對於數據的 ID 分配,應該採取全局唯一 ID 分配,有兩種實現方式,如果主機房和單元化機房距離較近,可採用 ID 分配依然採用中心式, 所有機房的單元全部向同一中心服務申請 ID 的方式。

如果主機房和單元化機房相隔較遠,可採用每個單元各自分配,通過特定規則保證每個機房得到的最終 ID 不衝突的方式。


億級規模的高可用微服務系統,如何輕鬆設計?


緩存層

在緩存層,有兩種方式:

方式一是集群熱備,新增 Redis 集群作為熱備份集群。

億級規模的高可用微服務系統,如何輕鬆設計?


主集群與備份集群之間在服務端進行數據同步,通過 Redis Replication 協議進行同步處理。

離線監聽主集群狀態,探測到故障則進行主備之間切換,信息通過配置中心下達客戶端,類哨兵方式進行監聽探活。

在這種場景下,集群之間數據在服務端進行同步,正常情況下,集群之間數據會一致。但會存在一定的複製時延。

在故障切換時,可能存在極短時間內的數據丟失。如果將緩存僅僅當緩存使用,不要做內存數據庫使用,則沒有問題。

第二種方式,集群多活。新增集群作為多活集群,正常情況下客戶端根據 Key 哈希策略選擇分發到不同集群。


億級規模的高可用微服務系統,如何輕鬆設計?


客戶端通過 Proxy 連接集群中每一個節點,Proxy 的用處是區分客戶端寫入與集群複製寫入。

集群之間在服務端進行數據雙向複製,數據變更通過 Redis Replication 協議獲取。

離線監聽主集群狀態,探測到故障則進行切換,信息通過配置中心下達客戶端,類哨兵方式進行監聽探活。

此方案應用於單純的集群間高可用時,同一個 Key 在同一段時間內只會路由到同一個集群,數據一致性可以保證。

在故障切換情況下,可能存在極端時間內的數據丟失。

微服務治理平臺

作為大規模微服務的微服務治理平臺,一方面自己要實現單元化,另外一方面要實現流量在不同單元之間的染色與穿梭。

從 API 網關,NSF 服務治理和管理中心,APM 性能管理,GXTS 分佈式事務管理,容器平臺的管控都需要進行跨機房單元化部署。


億級規模的高可用微服務系統,如何輕鬆設計?


當請求到達一個單元之後,API 網關上就帶有此單元的路由信息,NSF 服務治理與管理平臺在服務之間相互調用的時候,同樣會插入此單元的路由信息。

當一個單元某實例全掛的時候,可以穿梭到另一個單元進行調用,並在下一跳調用回本單元,這種方式稱為流量染色。


分享到:


相關文章: