說到大規(guī)模微服務系統(tǒng),往往是一些 7*24 時不間斷運行的在線系統(tǒng)。那么如何設計一個大規(guī)模的微服務系統(tǒng)呢?
圖片來自 Pexels
這樣的系統(tǒng)往往有以下的要求:
例如我們常說的可用性需達到 4 個 9(99.99%),全年停機總計不能超過 1 小時,約為 53 分鐘,也即服務停用時間小于 53 分鐘,就說明高可用設計合格。
另外一旦有了性能瓶頸或者故障點,應該有自動發(fā)現(xiàn)定位的機制,迅速找到瓶頸點和故障點,及時修復,才能保障 SLA。
戰(zhàn)略設計
為了滿足以上的要求,這個系統(tǒng)絕不是運維組努力一把,或者開發(fā)組努力一把,就能解決的,是一個端到端的,各個部門共同完成的一個目標,所以我們常稱為戰(zhàn)略設計。
研發(fā)
一個能支撐高并發(fā),高可用的系統(tǒng),一定是需要從研發(fā)環(huán)節(jié)就開始下功夫的。
首先,每一個微服務都有實現(xiàn)良好的無狀態(tài)化處理,冪等服務接口設計
狀態(tài)分為分發(fā),處理,存儲幾個過程,如果對于一個用戶的所有的信息都保存在一個進程中,則從分發(fā)階段,就必須將這個用戶分發(fā)到這個進程,否則無法對這個用戶進行處理。
然而當一個進程壓力很大的時候,根本無法擴容,新啟動的進程根本無法處理那些保存在原來進程的用戶的數(shù)據(jù),不能分擔壓力。
所以要將整個架構分成兩個部分,無狀態(tài)部分和有狀態(tài)部分,而業(yè)務邏輯的部分往往作為無狀態(tài)的部分,而將狀態(tài)保存在有狀態(tài)的中間件中,如緩存,數(shù)據(jù)庫,對象存儲,大數(shù)據(jù)平臺,消息隊列等。
這樣無狀態(tài)的部分可以很容易的橫向擴展,在用戶分發(fā)的時候,可以很容易分發(fā)到新的進程進行處理,而狀態(tài)保存到后端。
而后端的中間件是有狀態(tài)的,這些中間件設計之初,就考慮了擴容的時候,狀態(tài)的遷移,復制,同步等機制,不用業(yè)務層關心。
對于數(shù)據(jù)的存儲,主要包含幾類數(shù)據(jù):
但是還有一個遺留的問題,就是已經(jīng)分發(fā),正在處理,但是尚未存儲的數(shù)據(jù),肯定會在內(nèi)存中有一些,在進程重啟的時候,數(shù)據(jù)還是會丟一些的,那這部分數(shù)據(jù)怎么辦呢?
這部分就需要通過重試進行解決,當本次調(diào)用過程中失敗之后,前序的進程會進行重試,例如 Dubbo 就有重試機制。
既然重試,就需要接口是冪等的,也即同一次交易,調(diào)用兩次轉賬 1 元,不能最終轉走 2 元。
接口分為查詢,插入,更新,刪除等操作:
為了保持冪等性,往往要有一個冪等表,通過傳入冪等參數(shù)匹配冪等表中 ID 的方式,保證每個操作只被執(zhí)行一次,而且在實行最終一致性的時候,可以通過不斷重試,保證最終接口調(diào)用的成功。
對于并發(fā)條件下,誰先調(diào)用,誰后調(diào)用,需要通過分布式鎖如 Redis,ZooKeeper 等來實現(xiàn)同一個時刻只有一個請求被執(zhí)行,如何保證多次執(zhí)行結果仍然一致呢?則往往需要通過狀態(tài)機,每個狀態(tài)只流轉一次。
還有就是樂觀鎖,也即分布式的 CAS 操作,將狀態(tài)的判斷、更新整合在一條語句中,可以保證狀態(tài)流轉的原子性。樂觀鎖并不保證更新一定成功,需要有對應的機制來應對更新失敗。
其次,根據(jù)服務重要度實現(xiàn)熔斷降級、限流保護策略
服務拆分多了,在應用層面就會遇到以下問題:
服務雪崩:即一個服務掛了,整個調(diào)用鏈路上的所有的服務都會受到影響。
大量請求堆積、故障恢復慢:即一個服務慢,卡住了,整個調(diào)用鏈路出現(xiàn)大量超時,要長時間等待慢的服務恢復到正常狀態(tài)。
為了解決這些問題,我們在應用層面實施了以下方案:
通過熔斷機制,當一個服務掛了,被影響的服務能夠及時熔斷,使用 Fallback 數(shù)據(jù)保證流程在非關鍵服務不可用的情況下,仍然可以進行。
通過線程池和消息隊列機制實現(xiàn)異步化,允許服務快速失敗,當一個服務因為過慢而阻塞,被影響服務可以在超時后快速失敗,不會影響整個調(diào)用鏈路。
當發(fā)現(xiàn)整個系統(tǒng)的確負載過高的時候,可以選擇降級某些功能或某些調(diào)用,保證最重要的交易流程的通過,以及最重要的資源全部用于保證最核心的流程。
還有一種手段就是限流,當既設置了熔斷策略,又設置了降級策略,通過全鏈路的壓力測試,應該能夠知道整個系統(tǒng)的支撐能力。
因而就需要制定限流策略,保證系統(tǒng)在測試過的支撐能力范圍內(nèi)進行服務,超出支撐能力范圍的,可拒絕服務。
當你下單的時候,系統(tǒng)彈出對話框說 “系統(tǒng)忙,請重試”,并不代表系統(tǒng)掛了,而是說明系統(tǒng)是正常工作的,只不過限流策略起到了作用。
其三,每個服務都要設計有效探活接口,以便健康檢查感知到服務狀態(tài)
當我們部署一個服務的時候,對于運維部門來講,可以監(jiān)控機器的狀態(tài)或者容器的狀態(tài)是否處于啟動狀態(tài),也可以監(jiān)控到進程是否啟動,端口是否監(jiān)聽等。
但是對于已經(jīng)啟動的進程,是否能夠正常服務,運維部門無法感知,需要開發(fā)每個服務的時候,設計一個有效探活接口,讓運維的監(jiān)控系統(tǒng)可以通過調(diào)用這個接口,來判斷進程能夠正常提供服務。
這個接口不要直接返回,而是應該在進程內(nèi)部探查提供服務的線程是否出去正常狀態(tài),再返回相應的狀態(tài)編碼。
只有這樣,開發(fā)出來的服務和運維才能合作起來,保持服務處于某個副本數(shù),否則如果一部分服務雖然啟動,但是處于假死狀態(tài),會使得其他正常服務,無法承受壓力。
其四,通過制定良好的代碼檢查規(guī)范和靜態(tài)掃描工具,最大化限制因為代碼問題造成的系統(tǒng)不可用
要保持線上代碼的高可用性,代碼質量是關鍵,大部分線上問題,無論是性能問題,還是穩(wěn)定性問題,都是代碼造成的,而非基礎設施造成的。
而且基礎設施的可用率為 99.95%,但是服務層要求的可用率高于這個值,所以必須從業(yè)務層高可用來彌補。
除了下面的高可用架構部分,對于每一個服務來講,制定良好的代碼檢查規(guī)范和靜態(tài)掃描工具,通過大量的測試用例,最大化限制因為代碼問題造成的系統(tǒng)不可用,是必須的,是高可用的基礎。
高可用架構設計
在系統(tǒng)的每一個部分,都要避免單點。系統(tǒng)冗余往往分管控面和數(shù)據(jù)面,而且分多個層次,往往每一個層次都需要進行高可用的設計。
在機房層面,為了高可用應該部署在多個區(qū)域,或者多個云,每個區(qū)域分多個可用區(qū)進行部署。
對于云來講,云的管控要多機房高可用部署,使得任何一個機房故障,都會使得管控依然可以使用。
這就需要管控的組件分布于至少兩個機房,管控的數(shù)據(jù)庫和消息隊列跨機房進行數(shù)據(jù)同步。
對于云的數(shù)據(jù)面來講,入口的網(wǎng)關要和機房網(wǎng)絡配合做跨機房的高可用,使得入口公網(wǎng) IP 和負載均衡器,在一個機房故障的情況下,可以切換至另一個機房。
在云之上要部署 Kubernetes 平臺,管控層面 Kubernetes 要實現(xiàn)高可用部署,etcd 要跨機房高可用部署,Kubernetes 的管控組件也要跨機房部署。
當然還有一種情況是機房之間距離比較遠,需要在每一個機房各部署一套 Kubernetes。
這種情況下,Kubernetes 的管控依然要實現(xiàn)高可用,只不過跨機房的高可用就需要應用層來實現(xiàn)了。
在應用層,微服務的治理平臺,例如注冊發(fā)現(xiàn),ZooKeeper 或者 Euraka,APM,配置中心等都需要實現(xiàn)跨機房的高可用。另外就是服務要跨機房部署,實現(xiàn)城市級機房故障遷移能力。
運維
運維一個大規(guī)模微服務系統(tǒng)也有不一樣的挑戰(zhàn)。
首先,建議使用的是 Kubernetes 編排的聲明式的運維方式,而非 Ansible 之類命令式的運維方式。
另外,對于系統(tǒng)的發(fā)布,要進行灰度、藍綠發(fā)布,降低系統(tǒng)上線發(fā)布風險。要有這樣的理念,任何一個新上線的系統(tǒng),都是不可靠的。
所以可以通過流量分發(fā)的模式,逐漸切換到新的服務,從而保障系統(tǒng)的穩(wěn)定。
其三,完善監(jiān)控及應對機制,對系統(tǒng)各節(jié)點、應用、組件全面地監(jiān)控,能夠第一時間快速發(fā)現(xiàn)并解決問題。
監(jiān)控絕非只有基礎設施的 CPU,網(wǎng)絡,磁盤的監(jiān)控,應用的,業(yè)務的,調(diào)用鏈的監(jiān)控都應該有。
而且對于緊急事件,應該有應急預案,應急預案是在高可用已經(jīng)考慮過之后,仍然出現(xiàn)異常情況下,應該采取的預案,例如三個 etcd 全掛了的情況。
其四,持續(xù)關注線上系統(tǒng)網(wǎng)絡使用、服務器性能、硬件存儲、中間件、數(shù)據(jù)庫燈指標,重點關注臨界狀態(tài),也即當前還健康,但是馬上可能出問題的狀態(tài)。
例如網(wǎng)關 PPS 達到臨界值,下一步就要開始丟包了,數(shù)據(jù)庫快滿了,消息出現(xiàn)大量堆積等等。
DBA
對于一個在線業(yè)務系統(tǒng)來講,數(shù)據(jù)庫是重中之重,很多的性能瓶頸定位到最后,都可能是數(shù)據(jù)庫的問題。所以 DBA 團隊要對數(shù)據(jù)庫的使用,進行把關。
造成數(shù)據(jù)庫性能問題,一方面是 SQL 語句的問題,一方面是容量的問題。
例如查詢沒有被索引覆蓋,或者在區(qū)分度不大的字段上建立的索引,是否持鎖時間過長,是否存在鎖沖突等等,都會導致數(shù)據(jù)庫慢的問題。
因而所有上線的 SQL 語句,都需要 DBA 提前審核,并且要對于數(shù)據(jù)庫的性能做持續(xù)的監(jiān)控,例如慢 SQL 語句等。
另外對于數(shù)據(jù)庫中的數(shù)據(jù)量也要持續(xù)的監(jiān)控,到一定的量就需要改分布式數(shù)據(jù)庫 DDB,進行分庫分表,到一定的階段需要對分布式數(shù)據(jù)庫進行擴容。
故障演練和性能壓測
再好的規(guī)劃也比不上演練,再好的性能評估也比不上在線的性能壓測。
性能問題往往是通過線上性能壓測發(fā)現(xiàn)的。線上壓力測試需要有一個性能測試的平臺,做多種形式的壓力測試。
例如容量測試,通過梯度的加壓,看到什么時候實在不行。摸高測試,測試在最大的限度之上還能承受多大的量,有一定的余量會保險一些,心里相對比較有底。
再就是穩(wěn)定性測試,測試峰值的穩(wěn)定性,看這個峰值能夠撐一分鐘,兩分鐘還是三十分鐘。還有秒殺場景測試,限流降級演練測試等。
只有經(jīng)過性能壓測,才能發(fā)現(xiàn)線上系統(tǒng)的瓶頸點,通過不斷的修復和擴容瓶頸點,最終才能知道服務之間應該以各種副本數(shù)的比例部署,才能承載期望的 QPS。
對于可能遇到的故障,可以進行故障演練,故意模擬一些故障,來看系統(tǒng)如何反應,是否會因為自修復,多副本,容錯等機制,使得這些故障對于客戶端來講沒有影響。
戰(zhàn)術設計
下面,我們就從架構的每個層次,進行戰(zhàn)術設計。我們先來看一下高可用部署架構選型以及他們的優(yōu)劣:
高可用性要求和系統(tǒng)的負載度和成本是強相關的。越簡單的架構,部署成本越低的架構,高可用性越小,例如上面的單體應用。
而微服務化,單元化,異地多活,必然導致架構復雜難以維護,機房成本比較高,所以要使用多少成本實現(xiàn)什么程度的高可用,是一個權衡。
高可用的實現(xiàn)需要多個層次一起考慮:
首先是應用層,可以通過異地多活單元保證城市級高可用,這樣使得一個城市因為災難宕機的時候,另外一個城市可以提供服務。
另外每個多活單元采用雙機房保證機房級高可用,也即同城雙機房,使得一個城市中一個機房宕機,另一個機房可以提供服務。
再者每個機房中采用多副本保證實例級高可用,使得一個副本宕機的時候,其他的副本可以提供服務。
其次是數(shù)據(jù)庫層,在數(shù)據(jù)中心之間,通過主從復制或 MGR 實現(xiàn)數(shù)據(jù)異步復制,在每個集群單元中采用 DDB 分庫分表,分庫分表中的每個實例都是有數(shù)據(jù)庫同步復制。
其三是緩存層,在數(shù)據(jù)中心之間,緩存采用多集群單元化復制,在每個集群單元中采用多副本主從復制。
其四微服務治理平臺層,平臺組件異地多活單元保證了城市級高可用,平臺組件每個多活單元采用雙機房保證機房級高可用,平臺組件每個機房中采用多副本保證實例級高可用。
當有了以上高可用方案之后,則以下的故障等級以及影響時間如下表格:
接下來,我們每個層次詳細論述。
應用層
下圖以最復雜的場景,假設有三個城市,每個城市都有兩個完全對等的數(shù)據(jù)中心。三個城市的數(shù)據(jù)中心也是完全對等的。
我們將整個業(yè)務數(shù)據(jù)按照某個維度分成 A,B,C 三部分。這樣任何一部分全部宕機,其他部分照樣可以提供服務。
對于有的業(yè)務,如果省級別的服務中斷完全不能忍受,市級別的服務中斷要求恢復時間相當短,而區(qū)縣級別的服務中斷恢復時間可以相對延長。
在這種場景下,可以根據(jù)地區(qū)來區(qū)分維度,使得一個區(qū)縣和另外一個區(qū)縣的數(shù)據(jù)屬于不同的單元。
為了節(jié)約成本,模型可能會更加簡化。中心節(jié)點和單元化節(jié)點不是對稱的。中心節(jié)點可以實現(xiàn)同城雙活,而異地單元化的部分只部署一個機房即可。這樣是能滿足大部分高可用性需求的。
這種架構要求實現(xiàn)中間件層和數(shù)據(jù)庫層單元化,這個我們后面會仔細講。
接入層
單元化要求 App 層或者在機房入口區(qū)域的接入層,實現(xiàn)中心單元和其他單元節(jié)點的流量分發(fā)。
對于初始請求沒有任何路由標記的,可以隨機分發(fā)給任何一個單元,也可以根據(jù)地區(qū)或者運營商在 GSLB 中分發(fā)給某個就近的單元。
應用層接收到請求以后,根據(jù)自己所在的單元生成路由信息,將路由信息返回給接入層或者 App。
接下來 App 或者接入層的請求,都會帶著路由信息,選擇相應的單元進行發(fā)送,從而實現(xiàn)了請求的處理集中在本單元。
中間件層
在中間件層,我們以 ZooKeeper 為例,分為以下兩個場景:
場景一:ZooKeeper 單元化主從多活
在這種場景下,主機房和單元化機房距離相隔較近,時延很小,可以當做一個機房來對待??梢圆捎?ZooKeeper 高可用保障通過多 ZooKeeper 實例部署來達成。
如圖所示,主機房 ZooKeeper 有 Leader 和 Follower,單元化機房的 ZooKeeper 僅為 Observer。
場景二:ZooKeeper 單元化多集群復制
兩個機房相距較遠,每個機房部署一套 ZooKeeper 集群,集群之間進行數(shù)據(jù)同步。
各機房應用連接機房內(nèi)的 ZooKeeper 集群,注冊的信息通過數(shù)據(jù)同步,能夠被其他機房應用獲取到。
單一機房 ZooKeeper 集群不可用,其余機房不受影響。當前不考慮做不同機房之間的集群切換。
數(shù)據(jù)庫層
在數(shù)據(jù)庫層,首先要解決的問題是,分布式數(shù)據(jù)庫 DDB 集群多機房同步復制。
在單元內(nèi)采用同城主從復制模式,跨單元采用 DTS/NDC 實現(xiàn)應用層數(shù)據(jù)雙向同步能力。
對于數(shù)據(jù)的 ID 分配,應該采取全局唯一 ID 分配,有兩種實現(xiàn)方式,如果主機房和單元化機房距離較近,可采用 ID 分配依然采用中心式, 所有機房的單元全部向同一中心服務申請 ID 的方式。
如果主機房和單元化機房相隔較遠,可采用每個單元各自分配,通過特定規(guī)則保證每個機房得到的最終 ID 不沖突的方式。
緩存層
在緩存層,有兩種方式:
方式一是集群熱備,新增 Redis 集群作為熱備份集群。
主集群與備份集群之間在服務端進行數(shù)據(jù)同步,通過 Redis Replication 協(xié)議進行同步處理。
離線監(jiān)聽主集群狀態(tài),探測到故障則進行主備之間切換,信息通過配置中心下達客戶端,類哨兵方式進行監(jiān)聽探活。
在這種場景下,集群之間數(shù)據(jù)在服務端進行同步,正常情況下,集群之間數(shù)據(jù)會一致。但會存在一定的復制時延。
在故障切換時,可能存在極短時間內(nèi)的數(shù)據(jù)丟失。如果將緩存僅僅當緩存使用,不要做內(nèi)存數(shù)據(jù)庫使用,則沒有問題。
第二種方式,集群多活。新增集群作為多活集群,正常情況下客戶端根據(jù) Key 哈希策略選擇分發(fā)到不同集群。
客戶端通過 Proxy 連接集群中每一個節(jié)點,Proxy 的用處是區(qū)分客戶端寫入與集群復制寫入。
集群之間在服務端進行數(shù)據(jù)雙向復制,數(shù)據(jù)變更通過 Redis Replication 協(xié)議獲取。
離線監(jiān)聽主集群狀態(tài),探測到故障則進行切換,信息通過配置中心下達客戶端,類哨兵方式進行監(jiān)聽探活。
此方案應用于單純的集群間高可用時,同一個 Key 在同一段時間內(nèi)只會路由到同一個集群,數(shù)據(jù)一致性可以保證。
在故障切換情況下,可能存在極端時間內(nèi)的數(shù)據(jù)丟失。
微服務治理平臺
作為大規(guī)模微服務的微服務治理平臺,一方面自己要實現(xiàn)單元化,另外一方面要實現(xiàn)流量在不同單元之間的染色與穿梭。
從 API 網(wǎng)關,NSF 服務治理和管理中心,APM 性能管理,GXTS 分布式事務管理,容器平臺的管控都需要進行跨機房單元化部署。
當請求到達一個單元之后,API 網(wǎng)關上就帶有此單元的路由信息,NSF 服務治理與管理平臺在服務之間相互調(diào)用的時候,同樣會插入此單元的路由信息。
當一個單元某實例全掛的時候,可以穿梭到另一個單元進行調(diào)用,并在下一跳調(diào)用回本單元,這種方式稱為流量染色。