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

相关文章
相关标签/搜索