Spring Cloud 升級之路 - 2020.0.x - 1. 背景知識、需求描述與公共依賴

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 調用

然後,一般一個完整的微服務系統還包括:

  1. 統一網關
  2. 配置中心
  3. 全鏈路監控與監控中心

在之前的系列中,我們將 Spring cloud 升級到了 Hoxton 版本,組件體系是:

  1. 註冊中心:Eureka
  2. 客戶端封裝:OpenFeign
  3. 客戶端負載均衡:Spring Cloud LoadBalancer
  4. 斷路器與隔離: Resilience4J

並且實現瞭如下的功能:

註冊中心相關

  1. 所有集羣公用同一個公共 Eureka 集羣
  2. 實現實例的快速上下線。

微服務實例相關

  1. 不同集羣之間不互相調用,通過實例的metamap中的zone配置,來區分不同集羣的實例。只有實例的metamap中的zone配置一樣的實例才能互相調用。
  2. 微服務之間調用依然基於利用 open-feign 的方式,有重試,僅對GET請求並且狀態碼爲4xx和5xx進行重試(對4xx重試是因爲滾動升級的時候,老的實例沒有新的 api,重試可以將請求發到新的實例上)
  3. 某個微服務調用其他的微服務 A 和微服務 B, 調用 A 和調用 B 的線程池不一樣。並且調用不同實例的線程池也不一樣。也就是實例級別的線程隔離
  4. 實現實例 + 方法級別的熔斷,默認的實例級別的熔斷太過於粗暴。實例上某些接口有問題,但不代表所有接口都有問題。
  5. 負載均衡的輪詢算法,需要請求與請求之間隔離,不能共用同一個 position 導致某個請求失敗之後的重試還是原來失敗的實例。
  6. 對於 WebFlux 這種非 Servlet 的異步調用也實現相同的功能。

網關相關

  1. 通過metamap中的zone配置鑑別所處集羣,僅把請求轉發到相同集羣的微服務實例
  2. 轉發請求,有重試,僅對GET請求並且狀態碼爲4xx和5xx進行重試
  3. 不同微服務的不同實例線程隔離
  4. 實現實例級別的熔斷。
  5. 負載均衡的輪詢算法,需要請求與請求之間隔離,不能共用同一個 position 導致某個請求失敗之後的重試還是原來失敗的實例
  6. 實現請求 body 修改(可能請求需要加解密,請求 body 需要打印日誌,所以會涉及請求 body 的修改)

在後續的使用,開發,線上運行過程中,我們還遇到了一些問題:

  1. 業務在某些時刻,例如 6.30 購物狂歡,雙 11 大促,雙 12 剁手節,以及在法定假日的時候的快速增長,是很難預期的。雖然有根據實例 CPU 負載的擴容策略,但是這樣也還是會有滯後性,還是會有流量猛增的時候導致核心業務(例如下單)有一段時間的不可用(可能5~30分鐘)。主要原因是系統壓力大之後導致很多請求排隊,排隊時間過長後等到處理這些請求時已經過了響應超時,導致本來可以正常處理的請求也沒能處理。而且用戶的行爲就是,越是下不成單,越要刷新重試,這樣進一步增加了系統壓力,也就是雪崩。通過實例級別的線程隔離,我們限制了每個實例調用其他微服務的最大併發度,但是因爲等待隊列的存在還是具有排隊。同時,在 API 網關由於沒有做限流,由於 API 網關 Spring Cloud gateway 是異步響應式的,導致很多請求積壓,進一步加劇了雪崩。所以這裏,我們要考慮這些情況,重新設計線程隔離以及增加 API 網關限流。
  2. 微服務發現,未來爲了兼容雲原生應用,例如 K8s 的一些特性,最好服務發現是多個源
  3. 鏈路監控與指標監控是兩套系統,使用麻煩,並且成本也偏高,是否可以優化成爲一套。

接下來,我們要對現有依賴進行升級,並且對現有的功能進行一些拓展和延伸,形成一套完整的 Spring Cloud 微服務體系與監控體系。

1.2. 編寫公共依賴

本次項目代碼,請參考:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford

這次我們抽象出更加具體的各種場景的依賴。一般的,我們的整個項目一般會包括:

  1. 公共工具包依賴:一般所有項目都會依賴一些第三方的工具庫,例如 lombok, guava 這樣的。對於這些依賴放入公共工具包依賴。
  2. 傳統 servlet 同步微服務依賴:對於沒有應用響應式編程而是用的傳統 web servlet 模式的微服務的依賴管理。
  3. 響應式微服務依賴:對於基於 Project Reactor 響應式編程實現的微服務的依賴管理。響應式編程是一種大趨勢,Spring 社區也在極力推廣。可以從 Spring 的各個組件,尤其是 Spring Cloud 組件上可以看出來。spring-cloud-commons 更是對於微服務的每個組件抽象都提供了同步接口還有異步接口。我們的項目中也有一部分使用了響應式編程。

爲何微服務要抽象分離出響應式的和傳統 servlet 的呢

  1. 首先,Spring 官方其實還是很推崇響應式編程的,尤其是在 Hoxton 版本發佈後, spring-cloud-commons 將所有公共接口都抽象了傳統的同步版還有基於 Project Reactor 的異步版本。並且在實現上,默認的實現同步版的底層也是通過 Project Reactor 轉化爲同步實現的。可以看出,異步化已經是一種趨勢。
  2. 但是, 異步化學習需要一定門檻,並且傳統項目大多還是同步的,一些新組件或者微服務可以使用響應式實現。
  3. 響應式和同步式的依賴並不完全兼容,雖然同一個項目內同步異步共存,但是這種並不是官方推薦的做法(這種做法其實啓動的 WebServer 還是 Servlet WebServer),並且 Spring Cloud gateway 這種實現的項目就完全不兼容,所以最好還是分離開來。
  4. 爲什麼響應式編程不普及主要因爲數據庫 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。
  5. Java 響應式編程的未來會怎樣是否會有另一種解決辦法?我個人覺得,如果有興趣可以研究下響應式編程 WebFlux,但是不必強求一定要使用響應式編程。雖然異步化編程是大趨勢,響應式編程越來越被推崇,但是 Java 也有另外的辦法解決同步式編碼帶來的性能瓶頸,也就是 Project LoomProject Loom 可以讓你繼續使用同步風格寫代碼,在底層用的其實是非阻塞輕量級虛擬線程,網絡 IO 是不會造成系統線程阻塞的,但是目前 sychronized 以及本地文件 IO 還是會造成阻塞。不過,主要問題是解決了的。所以,本系列還是會以同步風格代碼和 API 爲主。

1.2.1. 公共 parent

pom.xml

<?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. 公共基礎依賴包

pom.xml

<?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 參考了很多論文實現不同場景適用的緩存,例如:

  1. 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. 2 Queue:http://www.tedunangst.com/flak/post/2Q-buffer-cache-algorithm
  3. Segmented LRU:http://www.is.kyusan-u.ac.jp/~chengk/pub/papers/compsac00_A07-07.pdf
  4. 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 微服務公共依賴

pom.xml

<?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

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