本系列為之前系列的整理重啟版,隨著專案的發展以及專案中的使用,之前系列裡面很多東西發生了變化,並且還有一些東西之前系列並沒有提到,所以重啟這個系列重新整理下,歡迎各位留言交流,謝謝!~
上圖中演示了一個非常簡單的微服務架構:
- 微服務會向註冊中心進行註冊。
- 微服務從註冊中心讀取服務例項列表。
- 基於讀取到的服務例項列表,微服務之間互相呼叫。
- 外部訪問通過統一的 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-mvc,我們一般使用 Open-feign,並且每個微服務自己維護自己微服務提供的 Open-feign 客戶端。我們一般不使用
- 負載均衡:很明顯,Spring Cloud 中的負載均衡大多是客戶端負載均衡,我們使用 spring-cloud-loadbalancer 作為我們的負載均衡器。
- 優雅關閉:我們希望微服務程式在收到關閉訊號後,在註冊中心標記自己為下線;同時收到的請求全部不處理,返回類似於 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: