SpringCloud升級之路2020.0.x版-2.微服務框架需要考慮的問題

本系列爲之前系列的整理重啓版,隨着項目的發展以及項目中的使用,之前系列裏面很多東西發生了變化,並且還有一些東西之前系列並沒有提到,所以重啓這個系列重新整理下,歡迎各位留言交流,謝謝!~

上圖中演示了一個非常簡單的微服務架構:

  • 微服務會向註冊中心進行註冊。

  • 微服務從註冊中心讀取服務實例列表。

  • 基於讀取到的服務實例列表,微服務之間互相調用。

  • 外部訪問通過統一的 API 網關。

  • API 網關從註冊中心讀取服務實例列表,根據訪問路徑調用相應的微服務進行訪問。

在這個微服務架構中的每個進程需要實現的功能都在下圖中:

接下來我們逐個分析這個架構中的每個角色涉及的功能、要考慮的問題以及我們這個系列使用的庫。

每個微服務的基礎功能包括:

  • 輸出日誌,並且在日誌中輸出鏈路追蹤信息。並且,隨着業務壓力越來越大,每個進程輸出的日誌可能越來越多,輸出日誌可能會成爲性能瓶頸,我們這裏使用了 log4j2 異步日誌,並且使用了 spring-cloud-sleuth 作爲鏈路追蹤的核心依賴。

  • Http 容器:提供 Http 接口的容器,分爲針對同步的 spring-mvc 以及針對異步的 spring-webflux 的:

    • 對於 spring-mvc,默認的 Http 容器爲 Tomcat。在高併發環境下,請求會有很多。我們考慮通過使用直接內存處理請求來減少應用 GC 來優化性能,所以沒有使用默認的 Tomcat,而是使用 Undertow

    • 對於 spring-webflux,我們直接使用 webflux 本身作爲 Http 容器,其實底層就是 reactor-http,再底層其實就是基於 Http 協議的 netty 服務器。本身就是異步響應式的,並且請求內存基本使用了直接內存。

  • 微服務發現與註冊:我們使用了 Eureka 作爲註冊中心。我們的集羣平常有很多發佈,需要快速感知實例的上下線。同時我們有很多套集羣,每個集羣服務實例節點數量是 100 個左右,如果每個集羣使用一個 Eureka 集羣感覺有些浪費,並且我們希望能有一個直接管理所有集羣節點的管理平臺。所以我們所有集羣使用同一套 Eureka,但是通過框架配置保證只有同一集羣內的實例互相發現並調用

  • 健康檢查:由於 K8s 需要進程提供健康檢查接口,我們使用 Spring Boot 的 actuator 功能,來作爲健康檢查接口。同時,我們也通過 Http 暴露了其他 actuator 相關接口,例如動態修改日誌級別,熱重啓等等。

  • 指標採集:我們通過 prometheus 實現進程內部指標採集,並且暴露了 actuator 接口供 grafana 以及 K8s 調用採集。

  • Http 客戶端:內部微服務調用都是 Http 調用。每個微服務都需要 Http 客戶端。在我們這裏 Http 客戶端有:

    • 對於同步的 spring-mvc,我們一般使用 Open-feign,並且每個微服務自己維護自己微服務提供的 Open-feign 客戶端。我們一般不使用 @LoadBalanced 註解的 RestTemplate

    • 對於同步的 spring-flux,一般使用 WebClient 進行調用

  • 負載均衡:很明顯,Spring Cloud 中的負載均衡大多是客戶端負載均衡,我們使用 spring-cloud-gateway 作爲我們的負載均衡器。

  • 優雅關閉:我們希望微服務進程在收到關閉信號後,在註冊中心標記自己爲下線;同時收到的請求全部不處理,返回類似於 503 的狀態碼;並且在所有線程處理完手頭的活之後,再退出,這就是優雅關閉。在 Spring Boot 2.3.x 之後,引入了這個功能,在我們這個系列中也會用到。

另外還會有重試機制,限流機制以及斷路機制,這裏我們先來關心最核心的針對調用其他微服務的 Http 客戶端中的這些機制以及需要考慮的問題。

來看幾個場景:

1.在線發佈服務的時候,或者某個服務出現問題下線的時候,舊服務實例已經在註冊中心下線並且實例已經關閉,但是其他微服務本地有服務實例緩存或者正在使用這個服務實例進行調用,這時候一般會因爲無法建立 TCP 連接而拋出一個 java.io.IOException,不同框架使用的是這個異常的不同子異常,但是提示信息一般有 connect time out 或者 no route to host。這時候如果重試,並且重試的實例不是這個實例而是正常的實例,就能調用成功。如下圖所示:

2.當調用一個微服務返回了非 2XX 的響應碼

a) 4XX:在發佈接口更新的時候,可能調用方和被調用方都需要發佈。假設新的接口參數發生變化,沒有兼容老的調用的時候,就會有異常,一般是參數錯誤,即返回 4XX 的響應碼。例如新的調用方調用老的被調用方。針對這種情況,重試可以解決。但是爲了保險,我們對於這種請求已經發出的,只重試 GET 方法(即查詢方法,或者明確標註可以重試的非 GET 方法),對於非 GET 請求我們不重試。如下圖所示:

b) 5XX:當某個實例發生異常的時候,例如連不上數據庫,JVM Stop-the-world 等等,就會有 5XX 的異常。針對這種情況,重試也可以解決。同樣爲了保險,我們對於這種請求已經發出的,只重試 GET 方法(即查詢方法,或者明確標註可以重試的非 GET 方法),對於非 GET 請求我們不重試。如下圖所示:

3.斷路器打開的異常:後面我們會知道,我們的斷路器是針對微服務某個實例某個方法級別的,如果拋出了斷路器打開的異常,請求其實並沒有發出去,我們可以直接重試。

這些場景在線上在線發佈更新的時候,以及流量突然到來導致某些實例出現問題的時候,還是很常見的。如果沒有重試,用戶會經常看到異常頁面,影響用戶體驗。所以這些場景下的重試還是很必要的。對於重試,我們使用 resilience4j 作爲我們整個框架實現重試機制的核心

再看下面一個場景:

微服務 A 通過同一個線程池調用微服務 B 的所有實例。如果有一個實例有問題,阻塞了請求,或者是響應非常慢。那麼久而久之,這個線程池會被髮送到這個異常實例的請求而佔滿,但是實際上微服務 B 是有正常工作的實例的。

爲了防止這種情況,也爲了限制調用每個微服務實例的併發(也就是限流),我們使用不同線程池調用不同的微服務的不同實例。這個也是通過 resilience4j 實現的。

如果一個實例在一段時間內壓力過大導致請求慢,或者實例正在關閉,以及實例有問題導致請求響應大多是 500,那麼即使我們有重試機制,如果很多請求都是按照請求到有問題的實例 -> 失敗 -> 重試其他實例,這樣效率也是很低的。這就需要使用斷路器

在實際應用中我們發現,大部分異常情況下,是某個微服務的某些實例的某些接口有異常,而這些問題實例上的其他接口往往是可用的。所以我們的斷路器不能直接將這個實例整個斷路,更不能將整個微服務斷路。所以,我們使用 resilience4j 實現的是微服務實例方法級別的斷路器(即不同微服務,不同實例的不同方法是不同的斷路器)。

本小節我們提出了一個簡單的微服務架構,並仔細分析了其微服務實例的涉及的公共組件使用的庫以及需要考慮的問題,並且針對微服務調用的核心 Http 客戶端的重試機制,線程隔離機制和斷路器機制需要考慮的問題以及如何設計做了較爲詳細的說明。接下來我們繼續分析關於 Eureka 註冊中心以及 API 網關設計需要考慮的機制。

微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer


本文分享自微信公衆號 - 我的編程喵(MyProCat)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章