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


image

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

image

image

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

  • 微服務會向注冊中心進行注冊。
  • 微服務從注冊中心讀取服務實例列表。
  • 基於讀取到的服務實例列表,微服務之間互相調用。
  • 外部訪問通過統一的 API 網關。
  • API 網關從注冊中心讀取服務實例列表,根據訪問路徑調用相應的微服務進行訪問。

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

image

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

image

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

  • 輸出日志,並且在日志中輸出鏈路追蹤信息。並且,隨着業務壓力越來越大,每個進程輸出的日志可能越來越多,輸出日志可能會成為性能瓶頸,我們這里使用了 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-loadbalancer 作為我們的負載均衡器。
  • 優雅關閉:我們希望微服務進程在收到關閉信號后,在注冊中心標記自己為下線;同時收到的請求全部不處理,返回類似於 503 的狀態碼;並且在所有線程處理完手頭的活之后,再退出,這就是優雅關閉。在 Spring Boot 2.3.x 之后,引入了這個功能,在我們這個系列中也會用到。

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

image

來看幾個場景:

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

image

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

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

image

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

image

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

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

image

再看下面一個場景:

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

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

image

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

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

image

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

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

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM