大規模微服務單元化與高可用設計
- 2019 年 10 月 3 日
- 筆記
說到大規模微服務系統,往往是一些7*24時不間斷運行的在線系統,這樣的系統往往有以下的要求:
第一,高可用。這類的系統往往需要保持一定的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,進行分庫分表,到一定的階段需要對分散式資料庫進行擴容。
第五,故障演練和性能壓測
再好的規劃也比不上演練,再好的性能評估也比不上在線的性能壓測。
性能問題往往通過線上性能壓測發現的。線上壓力測試需要有一個性能測試的平台,做多種形式的壓力測試。例如容量測試,通過梯度的加壓,看到什麼時候實在不行。摸高測試,測試在最大的限度之上還能承受多大的量,有一定的餘量會保險一些,心裡相對比較有底。再就是穩定性測試,測試峰值的穩定性,看這個峰值能夠撐一分鐘,兩分鐘還是30分鐘。還有秒殺場景測試,限流降級演練測試等。
只有經過性能壓測,才能發現線上系統的瓶頸點,通過不斷的修復和擴容瓶頸點,最終才能知道服務之間應該以各種副本數的比例部署,才能承載期望的QPS。
對於可能遇到的故障,可以進行故障演練,故意模擬一些故障,來看系統如何反應,是否會因為自修復,多副本,容錯等機制,使得這些故障對於客戶端來講沒有影響。
戰術設計
下面,我們就從架構的每個層次,進行戰術設計。我們先來看一下高可用部署架構選型以及他們的優劣。
架構類型 |
可用性 |
優勢 |
問題 |
單體應用 |
– |
網路開銷小 |
擴展性差,維護困難 |
單機房服務化 |
應用級高可用 |
網路開銷小,解耦可擴展 |
容量受限,機房級單點 |
同城多活階段一 |
機房級高可用 |
突破單機房容量瓶頸 |
非必要的跨機房開銷大 |
同城多活階段二 |
機房級高可用 |
非必要的跨機房網路開銷小,提供機房級容災 |
城市級單點,仍存在非必要的跨機房開銷 |
異地多活單元化 |
城市級高可用 |
異地容災,可用性高 |
成本較高,要求各應用組件實現單元化 |
高可用性要求和系統的負載度和成本是強相關的。越簡單的架構,部署成本越低的架構,高可用性越小,例如上面的單體應用。而微服務化,單元化,異地多活,必然導致架構複雜難以維護,機房成本比較高,所以要使用多少成本實現什麼程度的高可用,是一個權衡。
高可用的實現需要多個層次一起考慮:
首先是應用層,可以通過異地多活單元保證城市級高可用,這樣使得一個城市因為災難宕機的時候,另外一個城市可以提供服務。另外每個多活單元採用雙機房保證機房級高可用,也即同城雙機房,使得一個城市中一個機房宕機,另一個機房可以提供服務。再者每個機房中採用多副本保證實例級高可用,使得一個副本宕機的時候,其他的副本可以提供服務。
其次是資料庫層,在數據中心之間,通過主從複製或MGR實現數據非同步複製,在每個集群單元中採用DDB分庫分表,分庫分表中的每個實例都是有資料庫同步複製。
其三是快取層,在數據中心之間,快取採用多集群單元化複製,在每個集群單元中採用多副本主從複製。
其四微服務治理平台層,平台組件異地多活單元保證了城市級高可用,平台組件每個多活單元採用雙機房保證機房級高可用,平台組件每個機房中採用多副本保證實例級高可用。
當有了以上高可用方案之後,則以下的故障等級以及影響時間如下表格。
故障層級 |
預計影響範圍 |
預計SLA影響 |
恢復手段 |
應用單實例故障 (進程異常退出,FGC等) |
單個或部分用戶請求失敗。應用有多副本自動重試即可正常 |
無影響 |
K8S通過健康檢測到異常,自動重啟容器 |
應用單節點故障 (主機硬體異常,掉電等) |
部分用戶請求失敗。應用有多副本自動重試即可正常 |
<1分鐘 |
K8S檢測到節點異常會自動遷移該節點上的容器 |
中間件單節點故障 (Zookeeper、Eureka) |
ZK故障為從節點無影響; ZK故障為主節點,在重選舉時無法註冊發現; Eureka單節點故障無影響; |
無影響 |
自動剔除故障節點,故障節點需手工恢復 |
快取單節點故障 (Redis) |
Redis集群模式單節點故障無影響; |
無影響 |
自動剔除故障節點,故障節點需手工恢復 |
資料庫單節點故障 (DDB、RDS) |
DDN從節點故障無影響; DDN主節點故障會影響部分用戶請求超時; |
<1分鐘 |
DDB自動主備切換,故障節點需手工恢復 |
機房故障 (機房斷電,網路分區等) |
部分用戶請求失敗 |
<15分鐘 |
通過監控檢查,切換流量入口至同城機房; |
接下來,我們每個層次詳細論述。
第一,應用層
下圖以最複雜的場景,假設有三個城市,每個城市都有兩個完全對等的數據中心。三個城市的數據中心也是完全對等的。
我們將整個業務數據按照某個維度分成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服務治理與管理平台在服務之間相互調用的時候,同樣會插入此單元的路由資訊,當一個單元某實例全掛的時候,可以穿梭到另一個單元進行調用,並在下一跳調用回本單元,這種方式稱為流量染色。
歡迎關注個人公眾號
Related Posts
- 2021 年 11 月 27 日
Kubernetes Deployment 最佳實踐
零、示例 首先給出一個 Deployment+HPA+ PodDisruptionBudget 的完整 demo,後面再 ..