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

1. 背景知識、需求描述與公共依賴

1.1. 背景知識 & 需求描述

Spring Cloud 官方文檔說了,它是一個完整的微服務體系,用戶能夠經過使用 Spring Cloud 快速搭建一個本身的微服務系統。那麼 Spring Cloud 到底是如何使用的呢?他到底有哪些組件?java

spring-cloud-commons組件裏面,就有 Spring Cloud 默認提供的全部組件功能的抽象接口,有的還有默認實現。目前的 2020.0.x (按照以前的命名規則應該是 iiford),也就是spring-cloud-commons-3.0.x包括:git

  • 服務發現DiscoveryClient,從註冊中心發現微服務。
  • 服務註冊ServiceRegistry,註冊微服務到註冊中心。
  • 負載均衡LoadBalancerClient,客戶端調用負載均衡。其中,重試策略spring-cloud-commons-2.2.6加入了負載均衡的抽象中。
  • 斷路器CircuitBreaker,負責什麼狀況下將服務斷路並降級
  • 調用 http 客戶端:內部 RPC 調用都是 http 調用

而後,通常一個完整的微服務系統還包括:github

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

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

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

而且實現了以下的功能:算法

註冊中心相關spring

  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 的異步調用也實現相同的功能。

網關相關apache

  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 微服務體系與監控體系。json

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

相關文章
相關標籤/搜索