1. 背景知識、需求描述與公共依賴
1.1. 背景知識 & 需求描述
Spring Cloud 官方文檔說了,它是一個完整的微服務體系,用戶可以通過使用 Spring Cloud 快速搭建一個自己的微服務系統。那麼 Spring Cloud 究竟是如何使用的呢?他到底有哪些組件?
spring-cloud-commons
組件裏面,就有 Spring Cloud 默認提供的所有組件功能的抽象接口,有的還有默認實現。目前的 2020.0.x (按照之前的命名規則應該是 iiford),也就是spring-cloud-commons-3.0.x
包括:
- 服務發現:
DiscoveryClient
,從註冊中心發現微服務。 - 服務註冊:
ServiceRegistry
,註冊微服務到註冊中心。 - 負載均衡:
LoadBalancerClient
,客戶端調用負載均衡。其中,重試策略從spring-cloud-commons-2.2.6
加入了負載均衡的抽象中。 - 斷路器:
CircuitBreaker
,負責什麼情況下將服務斷路並降級 - 調用 http 客戶端:內部 RPC 調用都是 http 調用
然後,一般一個完整的微服務系統還包括:
- 統一網關
- 配置中心
- 全鏈路監控與監控中心
在之前的系列中,我們將 Spring cloud 升級到了 Hoxton 版本,組件體系是:
- 註冊中心:Eureka
- 客戶端封裝:OpenFeign
- 客戶端負載均衡:Spring Cloud LoadBalancer
- 斷路器與隔離: Resilience4J
並且實現瞭如下的功能:
註冊中心相關:
- 所有集羣公用同一個公共 Eureka 集羣。
- 實現實例的快速上下線。
微服務實例相關:
- 不同集羣之間不互相調用,通過實例的
metamap
中的zone
配置,來區分不同集羣的實例。只有實例的metamap
中的zone
配置一樣的實例才能互相調用。 - 微服務之間調用依然基於利用 open-feign 的方式,有重試,僅對GET請求並且狀態碼爲4xx和5xx進行重試(對4xx重試是因爲滾動升級的時候,老的實例沒有新的 api,重試可以將請求發到新的實例上)
- 某個微服務調用其他的微服務 A 和微服務 B, 調用 A 和調用 B 的線程池不一樣。並且調用不同實例的線程池也不一樣。也就是實例級別的線程隔離
- 實現實例 + 方法級別的熔斷,默認的實例級別的熔斷太過於粗暴。實例上某些接口有問題,但不代表所有接口都有問題。
- 負載均衡的輪詢算法,需要請求與請求之間隔離,不能共用同一個 position 導致某個請求失敗之後的重試還是原來失敗的實例。
- 對於 WebFlux 這種非 Servlet 的異步調用也實現相同的功能。
網關相關:
- 通過
metamap
中的zone
配置鑑別所處集羣,僅把請求轉發到相同集羣的微服務實例 - 轉發請求,有重試,僅對GET請求並且狀態碼爲4xx和5xx進行重試
- 不同微服務的不同實例線程隔離
- 實現實例級別的熔斷。
- 負載均衡的輪詢算法,需要請求與請求之間隔離,不能共用同一個 position 導致某個請求失敗之後的重試還是原來失敗的實例
- 實現請求 body 修改(可能請求需要加解密,請求 body 需要打印日誌,所以會涉及請求 body 的修改)
在後續的使用,開發,線上運行過程中,我們還遇到了一些問題:
- 業務在某些時刻,例如 6.30 購物狂歡,雙 11 大促,雙 12 剁手節,以及在法定假日的時候的快速增長,是很難預期的。雖然有根據實例 CPU 負載的擴容策略,但是這樣也還是會有滯後性,還是會有流量猛增的時候導致核心業務(例如下單)有一段時間的不可用(可能5~30分鐘)。主要原因是系統壓力大之後導致很多請求排隊,排隊時間過長後等到處理這些請求時已經過了響應超時,導致本來可以正常處理的請求也沒能處理。而且用戶的行爲就是,越是下不成單,越要刷新重試,這樣進一步增加了系統壓力,也就是雪崩。通過實例級別的線程隔離,我們限制了每個實例調用其他微服務的最大併發度,但是因爲等待隊列的存在還是具有排隊。同時,在 API 網關由於沒有做限流,由於 API 網關 Spring Cloud gateway 是異步響應式的,導致很多請求積壓,進一步加劇了雪崩。所以這裏,我們要考慮這些情況,重新設計線程隔離以及增加 API 網關限流。
- 微服務發現,未來爲了兼容雲原生應用,例如 K8s 的一些特性,最好服務發現是多個源
- 鏈路監控與指標監控是兩套系統,使用麻煩,並且成本也偏高,是否可以優化成爲一套。
接下來,我們要對現有依賴進行升級,並且對現有的功能進行一些拓展和延伸,形成一套完整的 Spring Cloud 微服務體系與監控體系。
1.2. 編寫公共依賴
本次項目代碼,請參考:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford
這次我們抽象出更加具體的各種場景的依賴。一般的,我們的整個項目一般會包括:
- 公共工具包依賴:一般所有項目都會依賴一些第三方的工具庫,例如 lombok, guava 這樣的。對於這些依賴放入公共工具包依賴。
- 傳統 servlet 同步微服務依賴:對於沒有應用響應式編程而是用的傳統 web servlet 模式的微服務的依賴管理。
- 響應式微服務依賴:對於基於 Project Reactor 響應式編程實現的微服務的依賴管理。響應式編程是一種大趨勢,Spring 社區也在極力推廣。可以從 Spring 的各個組件,尤其是 Spring Cloud 組件上可以看出來。spring-cloud-commons 更是對於微服務的每個組件抽象都提供了同步接口還有異步接口。我們的項目中也有一部分使用了響應式編程。
爲何微服務要抽象分離出響應式的和傳統 servlet 的呢?
- 首先,Spring 官方其實還是很推崇響應式編程的,尤其是在 Hoxton 版本發佈後, spring-cloud-commons 將所有公共接口都抽象了傳統的同步版還有基於 Project Reactor 的異步版本。並且在實現上,默認的實現同步版的底層也是通過 Project Reactor 轉化爲同步實現的。可以看出,異步化已經是一種趨勢。
- 但是, 異步化學習需要一定門檻,並且傳統項目大多還是同步的,一些新組件或者微服務可以使用響應式實現。
- 響應式和同步式的依賴並不完全兼容,雖然同一個項目內同步異步共存,但是這種並不是官方推薦的做法(這種做法其實啓動的 WebServer 還是 Servlet WebServer),並且 Spring Cloud gateway 這種實現的項目就完全不兼容,所以最好還是分離開來。
- 爲什麼響應式編程不普及?主要因爲數據庫 IO,不是 NIO。不論是Java自帶的Future框架,還是 Spring WebFlux,還是 Vert.x,他們都是一種非阻塞的基於Ractor模型的框架(後兩個框架都是利用netty實現)。在阻塞編程模式裏,任何一個請求,都需要一個線程去處理,如果io阻塞了,那麼這個線程也會阻塞在那。但是在非阻塞編程裏面,基於響應式的編程,線程不會被阻塞,還可以處理其他請求。舉一個簡單例子:假設只有一個線程池,請求來的時候,線程池處理,需要讀取數據庫 IO,這個 IO 是 NIO 非阻塞 IO,那麼就將請求數據寫入數據庫連接,直接返回。之後數據庫返回數據,這個鏈接的 Selector 會有 Read 事件準備就緒,這時候,再通過這個線程池去讀取數據處理(相當於回調),這時候用的線程和之前不一定是同一個線程。這樣的話,線程就不用等待數據庫返回,而是直接處理其他請求。這樣情況下,即使某個業務 SQL 的執行時間長,也不會影響其他業務的執行。但是,這一切的基礎,是 IO 必須是非阻塞 IO,也就是 NIO(或者 AIO)。官方JDBC沒有 NIO,只有 BIO 實現(因爲官方是 Oracle 提供維護,但是 Oracle 認爲下面會提到的 Project Loom 是可以解決同步風格代碼硬件效率低下的問題的,所以一直不出)。這樣無法讓線程將請求寫入鏈接之後直接返回,必須等待響應。但是也就解決方案,就是通過其他線程池,專門處理數據庫請求並等待返回進行回調,也就是業務線程池 A 將數據庫 BIO 請求交給線程池B處理,讀取完數據之後,再交給 A 執行剩下的業務邏輯。這樣A也不用阻塞,可以處理其他請求。但是,這樣還是有因爲某個業務 SQL 的執行時間長,導致B所有線程被阻塞住隊列也滿了從而A的請求也被阻塞的情況,這是不完美的實現。真正完美的,需要 JDBC 實現 NIO。
- Java 響應式編程的未來會怎樣?是否會有另一種解決辦法?我個人覺得,如果有興趣可以研究下響應式編程 WebFlux,但是不必強求一定要使用響應式編程。雖然異步化編程是大趨勢,響應式編程越來越被推崇,但是 Java 也有另外的辦法解決同步式編碼帶來的性能瓶頸,也就是 Project Loom。Project Loom 可以讓你繼續使用同步風格寫代碼,在底層用的其實是非阻塞輕量級虛擬線程,網絡 IO 是不會造成系統線程阻塞的,但是目前 sychronized 以及本地文件 IO 還是會造成阻塞。不過,主要問題是解決了的。所以,本系列還是會以同步風格代碼和 API 爲主。
1.2.1. 公共 parent
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.hashjang</groupId>
<artifactId>spring-cloud-iiford</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<project.version>1.0-SNAPSHOT</project.version>
</properties>
<dependencies>
<!--junit單元測試-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--spring-boot單元測試-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--mockito擴展,主要是需要mock final類-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.6.28</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<!--最好用JDK 12版本及以上編譯,11.0.7對於spring-cloud-gateway有時候編譯會有bug-->
<!--雖然官網說已解決,但是11.0.7還是偶爾會出現-->
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2.2. 公共基礎依賴包
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-iiford</artifactId>
<groupId>com.github.hashjang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-iiford-common</artifactId>
<properties>
<guava.version>30.1.1-jre</guava.version>
<fastjson.version>1.2.75</fastjson.version>
<disruptor.version>3.4.2</disruptor.version>
<jaxb.version>2.3.1</jaxb.version>
<activation.version>1.1.1</activation.version>
</properties>
<dependencies>
<!--內部緩存框架統一採用caffeine-->
<!--這樣Spring cloud loadbalancer用的本地實例緩存也是基於Caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- guava 工具包 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!--內部序列化統一採用fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--日誌需要用log4j2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--lombok簡化代碼-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--log4j2異步日誌需要的依賴,所有項目都必須用log4j2和異步日誌配置-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${disruptor.version}</version>
</dependency>
<!--JDK 9之後的模塊化特性導致javax.xml不自動加載,所以需要如下模塊-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-xjc</artifactId>
<version>${jaxb.version}</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>${activation.version}</version>
</dependency>
</dependencies>
</project>
1. 緩存框架 caffeine 很高效的本地緩存框架,接口設計與 Guava-Cache 完全一致,可以很容易地升級。性能上,caffeine 源碼裏面就有和 Guava-Cache, ConcurrentHashMap,ElasticSearchMap,Collision 和 Ehcache 等等實現的對比測試,並且測試給予了 yahoo 測試庫,模擬了近似於真實用戶場景,並且,caffeine 參考了很多論文實現不同場景適用的緩存,例如:
- Adaptive Replacement Cache:http://www.cs.cmu.edu/~15-440/READINGS/megiddo-computer2004.pdf 2.Quadruply-segmented LRU:http://www.cs.cornell.edu/~qhuang/papers/sosp_fbanalysis.pdf
- 2 Queue:http://www.tedunangst.com/flak/post/2Q-buffer-cache-algorithm
- Segmented LRU:http://www.is.kyusan-u.ac.jp/~chengk/pub/papers/compsac00_A07-07.pdf
- Filtering-based Buffer Cache:http://storageconference.us/2017/Papers/FilteringBasedBufferCacheAlgorithm.pdf
所以,我們選擇 caffeine 作爲我們的本地緩存框架
參考:https://github.com/ben-manes/caffeine
2. guava
guava 是 google 的 Java 庫,雖然本地緩存我們不使用 guava,但是 guava 還有很多其他的元素我們經常用到。
參考:https://guava.dev/releases/snapshot-jre/api/docs/
3. 內部序列化從 fastjson 改爲 jackson
json 庫一般都需要預熱一下,後面會提到怎麼做。 我們項目中有一些內部序列化是 fastjson 序列化,但是看 fastjson 已經很久沒有更新,有很多 issue 了,爲了避免以後出現問題(或者漏洞,或者性能問題)增加線上可能的問題點,我們這一版本做了兼容。在下一版本會把 fastjson 去掉。後面會詳細說明如何去做。
4. 日誌採用 log4j2
主要是看中其異步日誌的特性,讓打印大量業務日誌不成爲性能瓶頸。但是,還是不建議在線上環境輸出代碼行等位置信息,具體原因以及解決辦法後面會提到。由於 log4j2 異步日誌特性依賴 disruptor,還需要加入 disruptor 的依賴。
參考:
5. 兼容 JDK 9+ 需要添加的一些依賴
JDK 9之後的模塊化特性導致 javax.xml 不自動加載,而項目中的很多依賴都需要這個模塊,所以手動添加了這些依賴。
1.2.3. Servlet 微服務公共依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-iiford</artifactId>
<groupId>com.github.hashjang</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-iiford-service-common</artifactId>
<dependencies>
<dependency>
<groupId>com.github.hashjang</groupId>
<artifactId>spring-cloud-iiford-common</artifactId>
<version>${project.version}</version>
</dependency>
<!--註冊到eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--不用Ribbon,用Spring Cloud LoadBalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!--微服務間調用主要靠 openfeign 封裝 API-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--resilience4j 作爲重試,斷路,限併發,限流的組件基礎-->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-cloud2</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.github.resilience4j/resilience4j-feign -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-feign</artifactId>
</dependency>
<!--actuator接口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--調用路徑記錄-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!--暴露actuator相關端口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--暴露http接口, servlet框架採用nio的undertow,注意直接內存使用,減少GC-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>
</project>
這裏面相關的依賴,我們後面會用到。
1.2.4. Webflux 微服務相關依賴
對於 Webflux 響應式風格的微服務,其實就是將 spring-boot-starter-web
替換成 spring-boot-starter-webflux
即可
參考:pom.xml