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