微服務網關——設計篇
微服務網關——設計篇
在《微服務網關——需求篇》中,我們討論了微服務網關的需求,本文將對微服務網關進行設計。考慮到實際情況的差異,這裏實際給出的是設計選項,最終設計基於實際場景來確定。
網關功能性設計
路由
一般情況下,服務對外提供的是 RESTful 接口,所以一般路由模塊根據請求的 host, url 等規則轉發到指定的服務。
考慮到路由規則需要頻繁的修改發佈,爲了發佈的便利性,考慮針對規則實現熱發佈。有幾種實現方式:
基於數據庫
即將路由規則配置到數據庫中,當網關收到請求後,從數據庫中查詢規則進行規則匹配。根據匹配到的規則進行路由。
考慮到性能,可以緩存規則,例如緩存到 redis 中。當修改配置後,需要將修改的數據刷到緩存中。
此方式需要實現數據庫與緩存的同步邏輯,提供操作界面,需要一定的開發量。
基於配置文件
即將路由規則配置到配置文件中,網關啓動時直接加載即可。普通的配置文件方式無法動態處理配置,每次修改後都需要啓動網關,比較麻煩。對於微服務架構來說,一般會有配置服務器,可以基於配置服務器來實現配置的實時生效。
相對於前一種方法,可以基於微服務基礎設施來實現,降低了一定的開發量。
負載均衡
一般負載均衡算法有:
- 隨機算法:從多個服務中,隨機選擇一個服務來處理請求。此算法的問題是,實際無法做到負載均衡,極端情況下可能會導致所有請求都由同一個服務進行處理。且對於有狀態的服務,對狀態的管理會比較麻煩。
- 加權隨機:同隨機算法,不同之處是每個服務的權重不同。比如有的服務器性能較好,則可以提高權重,能夠處理較多的請求;有的服務器性能較差,則可以降低權重,處理較少的請求。
- 輪詢算法:對服務進行排序,將請求按順序發送給對應的服務來處理。假設有兩個服務 A,B,第一個請求由 A 處理,第二個請求則由 B 處理,第三個請求還由 A 處理,以次類推。對於有狀態的服務,輪詢算法對狀態處理也比較麻煩。
- 加權輪詢:同加權輪詢,不同之處是每個服務的權重不同。比如還以上面的例子,A,B 權重 2:1,則第一個請求 A 處理,第二個請求還是 A 處理,第三個請求 B 處理,第四個請求 A 處理,以次類推。
- 最小連接算法:根據服務的連接數來判斷請求由哪個服務來處理,選擇當前連接數最少的服務來處理請求。此算法需要維護每個服務的連接數,比較複雜,不推薦使用。
- 源地址 hash:根據請求地址取 hash,然後對服務數量取模,由對應的服務來處理對應的請求。此算法可以保證相同用戶的請求由同一個服務來處理,可以保障服務端狀態。
對於微服務場景來說,優先選擇源地址 hash:
- 首先,不需要處理隨機、輪詢這種算法需要處理的服務端 Session 共享的問題
- 其次,實現簡單
- 最後,考慮服務的變動不會太頻繁,前期用戶量也不會很大,使用源地址 hash 的性價比最高
聚合服務
聚合服務有兩種方案:
-
GraphQL:一種用於 API 的查詢語言。使用 GraphQL 有三種可選方案
-
在網關前增加一個聚合服務 Server,基於 GraphQL 來實現服務聚合(也可以使用編碼的形式來處理,此服務主要是 IO 密集型操作,故可以使用擅長 IO 密集型操作的技術,比如 nodeJs,golang)
-
直接在網關中使用 GraphQL 來進行服務聚合,此方式需要重啓網關
-
網關後增加聚合服務層,用於組裝聚合請求
-
編碼:在網關層進行服務請求的處理,針對需要聚合的服務構建微服務請求,將獲得的結果構建爲最終結果返回。此方案需要編碼,發佈。對於需要頻繁發佈的聚合服務,也可以考慮獨立「聚合服務」,避免頻繁的發佈網關,影響系統穩定性。
考慮到 GraphQL 的學習成本,以及聚合服務的量不是很多,優先考慮在網關中直接進行編碼的方式。
認證授權
目前大部分系統採用的都是基於 RBAC 的認證授權。RBAC 模型是目前主流權限控制的理論基礎。
RBAC(Role-Based Access Control)即:基於角色的權限控制。通過角色關聯用戶,用戶關聯權限的方式間接賦予用戶權限。如下圖:
RBAC 模型可分爲:RBAC0、RBAC1、RBAC2、RBAC3 四種。其中 RBAC0 是基礎,也是最簡單的,相當於底層邏輯,RBAC1、RBAC2、RBAC3 都是以 RBAC0 爲基礎的升級。具體內容請自行 Google。
考慮互聯網項目對用戶角色的區分沒有特別的嚴格(相對後臺管理系統),RBAC0 模型就可以滿足常規的權限管理系統的需求,所以選擇基於 RBAC0 來實現認證與鑑權。
對於 Java 來說,主流的認證與鑑權框架是 SpringSecurity 和 Shiro,考慮集成的便利性,選擇 SpringSecurity 作爲認證鑑權框架。
過載保護
流量控制
一般的流量控制模式有:
- 控制併發,即限制併發的總數量(比如數據庫連接池、線程池)
- 控制速率,即限制併發訪問的速率(如 nginx 的 limitconn 模塊,用來限制瞬時併發連接數)
- 控制單位時間窗口內的請求數量(如 Guava 的 RateLimiter、nginx 的 limitreq 模塊,限制每秒的平均速率)
- 控制遠程接口調用速率
- 控制 MQ 的消費速率
- 根據網絡連接數、網絡流量、CPU 或內存負載等來限流。
對於微服務場景來說,控制速率是比較合適的流量控制方案。通常情況下,使用令牌桶算法來實現訪問速率的控制,常用的令牌桶算法有兩種:
-
漏桶算法:水(請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大會直接溢出。可以看出漏桶算法能強行限制數據的傳輸速率,但是某些情況下,系統可能需要允許某種程度的突發訪問量,此時可以使用令牌桶算法。
-
令牌桶算法:系統會以一個恆定的速度向桶裏放入令牌。如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。令牌桶算法通過發放令牌,根據令牌的 rate 頻率做請求頻率限制,容量限制等。
流量控制算法在確定後也是基本不需要變化的,所以對於熱部署的需求不是必要的。
另外流量控制可以前置,放到接入層來處理,一般的網絡接入服務,如 nginx 是支持流量控制的。如果前期對流量控制沒有太多的定製化需求,可以考慮基於 nginx 來進行處理。
熔斷
服務熔斷的實現思路:
- 調用失敗次數累積達到了閾值(或一定比例)則啓動熔斷機制
- 此時對調用直接返回錯誤。待達到設置的時間後(這個時間一般設置成平均故障處理時間,也就是 MTTR),進入半熔斷狀態
- 此時允許定量的服務請求,如果調用都成功(或一定比例)則認爲恢復了,關閉熔斷;否則認爲還沒好,繼續熔斷
考慮到有較成熟的開源項目,推薦直接使用開源項目來處理。
服務升降級
一種服務升降級的方案可以基於阻塞隊列來實現:
- 網關接收到請求後,進入定長的阻塞隊列
- 消費線程從消費隊列中獲取請求來進行處理
- 當生產速率大於消費速率,會導致隊列中請求不斷增加,當請求數量超過設定的閾值時,根據配置的服務升降級規則判定當前請求是否屬於可降級的服務(或者基於隊列來判定),如果屬於可降級的服務,則根據配置的降級邏輯對該請求進行處理(比如直接拒絕);如果請求屬於不可降級的服務,則依然添加到請求隊列中
考慮到有較成熟的開源項目,推薦直接使用開源項目來處理。
緩存
考慮到網關是集羣化部署,所以優先使用集中式緩存方式,即網關中所有需要緩存的數據都集中進行緩存。使用常用的分佈式緩存中間件即可,例如 redis。
基於緩存的網關工作步驟:
- 網關通過加載緩存模塊,根據請求 URL 和參數解析,從緩存中查詢數據
- 如果緩存命中(緩存有效期內),那麼直接返回結果
- 如果緩存未命中(緩存失效或者未緩存),那麼請求目標服務
- 請求結果返回網關
- 網關緩存請求結果
此處需要注意緩存常見問題:緩存雪崩、緩存擊穿、緩存穿透,需要針對性的做好處理。
服務重試
對於服務重試至少需要提供兩個功能:
- 配置:即需要配置哪些接口需要進行重試,重試幾次
- 執行:針對配置進行重試
對於配置來說,需要配置請求的超時時間、單次請求的超時時間、重試次數,注意單次請求的超時時間 * 重試次數要小於請求的超時時間,否則會影響服務重試邏輯。同時,也需要考慮配置的動態生效,以保障網關的穩定性。
對於執行來說,根據配置的次數來進行處理即可。
邏輯實現並不複雜,不過考慮到有較成熟的開源項目,推薦直接使用開源項目來處理。
日誌
應用日誌記錄遵循項目日誌規範。對於訪問日誌來說,前期可以考慮在接入層實現,例如通過 nginx 的訪問日誌來實現對訪問請求的記錄。待後期有特定需求後再進行定製化。
管理
對於管理功能,由於是非核心需求,前期可以暫不考慮。
網關非功能性設計
高性能
傳統的基於線程的併發模型(Thread-based concurrency),爲每一個請求分配一個線程或進程。這種模型編程簡單,可以將處理一個完整請求的代碼編寫在一個代碼路徑中。這種模型的弊端是,隨着線程 (進程) 數的上升,操作系統在這些線程 (進程) 之間的頻繁切換,將急劇降低系統的性能。
網關作爲整個系統的入口,需要處理大量的請求,故基於線程的併發模型並不適用。需要使用 Reactor 模型來進行處理。
關於 Reactor 模型請參考《EDA 風格與 Reactor 模式 》
目前常用的 IO 框架 Netty 可通過配置實現上述 Reactor 模型,如自行開發網關,可基於 Netty 進行開發。
高可用設計
高可用包含了前面所說的流量控制、熔斷和服務升降級。除了這些功能外,還需要提供服務的優雅上下線功能以及自身的優雅下線功能。
對於使用 Java 開發的項目來說,由於 JVM 的特性,一般需要一個預熱的過程,即服務啓動後,需要訪問一段時間後,服務纔會達到最佳狀態。如果服務剛啓動就接收高強度的請求,可能會導致響應時間過長、服務負載過高的問題,嚴重時可能導致服務被瞬間壓垮。爲了避免這種情況,網關可以考慮支持 Slow Start 特性。即經過一段時間,逐漸把請求壓力增加到預設的值。
另外,當一個服務下線時,不能直接關閉服務,需要先關閉該服務的對外接口,當該服務處理完所有正在處理的請求並返回後,方可關閉服務。
對於網關自身也類似,當網關需要關閉時,不是直接結束網關進程,而是先關閉監聽套接字,但是繼續爲當前連接的客戶提供服務,當所有客戶端的服務都完成後,再把進程關閉。
擴展性
網關對請求的處理,可以分爲:
-
接受請求
-
路由並轉發請求
-
如果是直接路由轉發,則將請求直接轉發給目標服務
-
如果是聚合服務,則可能分發多個請求到各個目標服務
-
接受服務的返回數據並返回給請求者
-
如果是直接路由轉發,則直接將結果進行返回
-
如果是聚合服務,則等待所有服務返回結果後,組裝結果數據後再返回
-
錯誤處理
-
統一的錯誤處理,例如服務請求錯誤返回統一的錯誤
-
對於聚合服務,如果部分請求錯誤,根據業務需求決定是返回統一請求錯誤還是組合部分結果返回
對於此類請求的擴展,主要是基於過濾器 / 攔截器來實現。
一般攔截器可以分爲兩大類:
- 全局攔截器,即對所有請求都進行攔截處理,例如安全校驗、日誌記錄等
- 業務攔截器,即爲了某些業務邏輯,針對符合特定規則的請求進行攔截處理。
一般來說,先執行全局攔截器,再執行爲了業務邏輯編寫的攔截器。不過,爲了靈活性,網關最好能提供一種機制,可以較容易地調整攔截器的執行順序。最簡單的一種方法,就是給每個攔截器定義一個優先級,網關按優先級順序依次調用各攔截器。
同時,網關也需要能方便的動態配置攔截器,即動態配置攔截器的開啓與關閉、以及配置哪些攔截器針對哪些請求生效。可以通過兩種方式來處理:
- 通過接口調用的方式來處理
- 基於配置服務器的方式來處理
伸縮性
網關層爲保證高可用,易於伸縮,快速啓動,需要設計成無狀態的(微服務裏的絕大部分服務都需要設計爲無狀態的)。但是,由於網關需要處理用戶的認證與鑑權,勢必與用戶狀態有關係,此處需要解耦用戶狀態關係。目前一般做法是基於 token 來進行處理:
- 用戶在登錄頁完成登錄操作後,服務端會生成一個登錄用戶信息,緩存起來,同時設置失效時間。返回給前端對應的 key 作爲登錄 token 憑證。
- 用戶後續的每次請求裏會帶着這個 token 信息,服務端根據 token 從緩存中獲取登錄用戶信息,進行校驗,校驗通過就認爲是合法用戶,執行請求操作。否則就拒絕操作。
- 對於訪問鑑權流程類似,服務端根據 token 從緩存中獲取登錄用戶信息,根據用戶角色、當前訪問的接口,判定當前用戶是否有權限訪問該接口,如果有權限則執行請求操作,否則就拒絕操作。
通過此方式,保證了網關的無狀態,繼而保證網關的快速擴容。
服務監控
對於微服務監控目前市面上有較完善的項目,例如 SkyWalking,Pinpoint。可以基於這些項目快速搭建一個服務監控系統。對於定製化需求,可以進行二次開發。
同時可以基於 ELK 對日誌進行收集分析,方便快速的定位問題。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://www.toutiao.com/i6901247551744557576/