案例准备

按照普通方式模拟一个微服务之间的调用,后续一步一步使用spring cloud组件对案例进行改造。
image.png
完整的业务流程:
image.png

数据库环境:

商品表:
image.png

工程搭建

image.png

  • 父工程实现

    1. <!-- 父工程的打包方式 -->
    2. <packaging>pom</packaging>
    3. <!--spring boot 父启动器依赖-->
    4. <parent>
    5. <groupId>org.springframework.boot</groupId>
    6. <artifactId>spring-boot-starter-parent</artifactId>
    7. <version>2.1.6.RELEASE</version>
    8. </parent>
    9. <dependencies>
    10. <!--web依赖-->
    11. <dependency>
    12. <groupId>org.springframework.boot</groupId>
    13. <artifactId>spring-boot-starter-web</artifactId>
    14. </dependency>
    15. <!--日志依赖-->
    16. <dependency>
    17. <groupId>org.springframework.boot</groupId>
    18. <artifactId>spring-boot-starter-logging</artifactId>
    19. </dependency>
    20. <!--测试依赖-->
    21. <dependency>
    22. <groupId>org.springframework.boot</groupId>
    23. <artifactId>spring-boot-starter-test</artifactId>
    24. <scope>test</scope>
    25. </dependency>
    26. <dependency>
    27. <groupId>junit</groupId>
    28. <artifactId>junit</artifactId>
    29. <scope>test</scope>
    30. </dependency>
    31. <!--lombok工具-->
    32. <dependency>
    33. <groupId>org.projectlombok</groupId>
    34. <artifactId>lombok</artifactId>
    35. <version>1.18.4</version>
    36. <scope>provided</scope>
    37. </dependency>
    38. <dependency>
    39. <groupId>org.springframework.boot</groupId>
    40. <artifactId>spring-boot-starter-actuator</artifactId>
    41. </dependency>
    42. <!--热部署-->
    43. <dependency>
    44. <groupId>org.springframework.boot</groupId>
    45. <artifactId>spring-boot-devtools</artifactId>
    46. <optional>true</optional>
    47. </dependency>
    48. </dependencies>
    49. <build>
    50. <plugins>
    51. <!--编译插件-->
    52. <plugin>
    53. <groupId>org.apache.maven.plugins</groupId>
    54. <artifactId>maven-compiler-plugin</artifactId>
    55. <configuration>
    56. <source>8</source>
    57. <target>8</target>
    58. <encoding>utf-8</encoding>
    59. </configuration>
    60. </plugin>
    61. <!--打包插件-->
    62. <plugin>
    63. <groupId>org.springframework.boot</groupId>
    64. <artifactId>spring-boot-maven-plugin</artifactId>
    65. <executions>
    66. <execution>
    67. <goals>
    68. <goal>repackage</goal>
    69. </goals>
    70. </execution>
    71. </executions>
    72. </plugin>
    73. </plugins>
    74. </build>

案例代码的问题

上述代码在页面静态化微服务中使用RestTemplate调用商品微服务的商品状态接口(Restful API接口)。在微服务分布式集群环境下会存在什么问题呢?

存在的问题:

  1. 在服务消费者中,我们把URL地址硬编码到代码中,不方便后期维护。
  2. 服务提供者只有一个服务,即便服务提供者形成集群,服务消费者还需要自己实现负载均衡
  3. 在服务消费者中,不清楚服务提供者的状态
  4. 服务消费者调用服务提供者时候,如果出现故障能否及时发现不向用户抛出异常页面
  5. RestTemplate这种请求调用方式是否还有优化空间?能不能类似于Dubbo那样?
  6. 这么多微服务统一认证如何实现
  7. 篇日志文件每次都修改好多个很麻烦
  8. …..

在微服务的架构中提供的解决方案:

  1. 服务管理:自动注册与发现、状态监管
  2. 服务负载均衡
  3. 熔断
  4. 远程过程调用
  5. 网关拦截、路由转发
  6. 统一认证
  7. 集中式配置管理,配置信息实时自动更新

第一代SpringCloud核心组件

之前提到的网关组件Zuul性能一般,未来将退出spring cloud生态圈,所以网关组件讲解GateWay.

image.png

Eureka服务注册中心

常用的服务注册中心:Eureka、Nacos、Zookeeper、Consul

服务注册中心本质上就是为了解耦合服务提供者和服务消费者。服务消费者 —-> 服务注册中心 —->服务提供者
对于任何一个微服务,原则上都应该存在或者支持多个提供者(比如商品微服务部署多个实例),这是由微服务的分布式属性决定的。
更进一步,为了支持弹性扩容和索容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。在单体应用阶段常用的静态LB机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。

注册中心实现原理

image.png

服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方法获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息。

  1. 服务注册中心先启动
  2. 服务提供者相关服务主动注册到注册中心
  3. 服务消费者注册到注册中心
  4. 服务消费者获取服务注册信息:
    1. pull 模式:服务消费者可以主动拉取可用的服务提供者清单
    2. push 模式:服务消费者订阅服务,当服务提供者有变化时,注册中心也会主动推送更新后的服务清单给消费者
  5. 服务消费者直接调用服务提供者
  6. 注册中心也需要完成服务提供者的健康监控,一般通过心跳机制,当然不同的服务注册中心组件实现不同,当发现服务提供者失效时需要及时剔除。

主流服务中心对比

  • Zookeeper

    Zookeeper通常和dubbo一起使用。Zookeeper是一个分布式服务框架,主用用来 解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。Zookeeper的本质= 存储 + 监听通知。

Zookeeper用来做服务注册中心,主要因为它具有节点变更通知功能,只要客户端监听相关服务节点,服务节点的所有变更,都能够及时的通知到监听客户端。Zookeeper的可用性也可以,因为只要半数以上的选举节点存活,整个集群就是可用的,最少节点数为3.

  • Eureka

由Netfix开源,冰杯Pivatal集成到springcloud体系中,它是基于RestfulAPI风格开发的服务注册与发现组件

  • Consul

Consul是由HashiCorp基于Go语言开发的支持多数据中心分布式高可用的服务分布和注册服务软件,采用Raft算法的一致性,且支持健康检查

  • Nacos

Nacos 是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台,Nacos就是注册中心 + 配置中心的组合,帮助我们解决微服务开发必会涉及到服务注册与发现,服务配置,服务管理等问题,Nacos是Spring Cloud Alibaba核心组件之一,负责服务注册与发现还有配置

image.png

CAP 定理又称为CAP原则,指的是在一个分布式系统中,Consistency(一致性)、Availability 可用性、Partition tolerance 分区容错性,最多只能同时三个特性中的两个,三者不可兼得。
分区容错性:分布式系统在遇到某节点或网络分区故障的时候,必须满足C或者A中的一个。

Eureka 基础架构与交互流程

  • 基础架构

image.png

  • 交互原理

image.png

  1. 图中us-east-xxx 代表不同的区也是不同的机房
  2. 图中每一个Eureka Server都是一个集群
  3. Application Service作为服务提供者向Eureka Server中注册服务,Eureka Server接收到注册事件会在集群和分区中进行数据同步。
  4. 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30S,默认90S还没有续约的进行剔除)以续约自己的信息
  5. 每个Eureka Server同时也是EurekaClient 多个Eureka Server之间通过赋值的方式完成服务注册列表的同步
  6. Eureka Client会缓存Eureka Server的信息,即使所有的EurekaServer节点都宕掉,也可以从缓存中找到服务提供者

Eureka 通过心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性,可伸缩性和高可用性

搭建单实例Eureka Server

  • 基于上述的案例项目,父工程引入spring cloud依赖

      <dependencyManagement>
          <dependencies>
              <dependency>
                  <groupId>org.springframework.cloud</groupId>
                  <artifactId>spring-cloud-dependencies</artifactId>
                  <version>Greenwich.RELEASE</version>
                  <type>pom</type>
                  <scope>import</scope>
              </dependency>
          </dependencies>
      </dependencyManagement>
    
  • 创建子工程 Eureka server

引入EurekaServer的依赖

<dependencies>
        <!-- Eureka Server的依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!--引入Jaxb,开始
            在父工程的pom文件中手动引入jaxb的jar,因为Jdk9之后默认没有加载该模块,Eureka Server使用到,所以需要手动导入,否则EurekaServer服务无法启动
        -->
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-core</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.2.11</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.2.10-b140310.1920</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>

Eureka 的配置application.yml

server:
  port: 9200

spring:
  application:
    name: lagou-cloud-eureka
eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    service-url: # 定义eureka server url
      defaultZone: http://localhost:9200/eureka
    register-with-eureka: false # 表示是否想Eureka 中心注册自己的信息,因为自己就是Eureka Server所以不进行注册,默认为true
    fetch-registry: false # 是否查询/拉取Eureka Server服务注册列表,默认为true
  instance:
    hostname: localhost # 当前Eureka实例的主机名
  • 在启动可标记为Eureka Server

    @SpringBootApplication
    //标识当前项目为Server
    @EnableEurekaServer
    public class EurekaApplication {
      public static void main(String[] args) {
          SpringApplication.run(EurekaApplication.class, args);
      }
    }
    
  • 访问Eureka 注册中心 localhost:9200

image.png
image.png

  • 将product服务和page服务,配置为Eureka Client

引入依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

application.yml 进行配置

eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    service-url: # 定义eureka server url
      defaultZone: http://localhost:9200/eureka
  instance:
    #    hostname: localhost # 当前Eureka实例的主机名
    # 使用IP注册,否则会使用主机名进行注册(此处考虑到对老版本的兼容,新版本经过试验都是IP)
    prefer-ip-address: true
    # 自定义实例显示格式,如加上版本号,便于多版本管理,注意是ip-address z=早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

在启动了使用EnableDiscoveryClient标记为Eureka Client,这里需要注意@EnableEurekaClient 只能在Eureka环境下使用,而@EnableDiscoveryClient 可以在所有的服务注册中心环境下使用

@SpringBootApplication
// 将当前项目作为Eureka Client注册到Eureka server ,只能在Eureka环境中使用
//@EnableEurekaClient
// 也是将当前项目标识为注册中心的客户端,向注册中心进行注册,可以在所有的服务注册中心环境下使用
@EnableDiscoveryClient
//扫描mapper接口 生成动态代理类
@MapperScan("com.prim.product.mapper")
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}

将服务启动之后,我们在访问Eureka 管理中心,就可以看到服务的实例了
image.png

搭建Eureka Server高可用集群

在互联网应用中,服务实例很少有单个的。在生产环境中,通常会配置Eureka Server集群实现高可用。Eureka Server集群中的节点通过点对点 P2P 通信的方式共享服务注册表.
在个人计算机测试环境下,很难模拟多主机的情况,需要修改电脑中的host地址模拟多个主机:

127.0.0.1 LagouCloudEurekaServerA
127.0.0.1 LagouCloudEurekaServerB

将之前我们创建的eureka-sever工程,复制一份名为:eureka-server9201,修改配置文件:
注意要将端口号修改为:9201
defaultZone : 配置问集群下的其他的Eureka-server的地址,多个地址逗号隔开。

server:
  port: 9201

spring:
  application:
    name: lagou-cloud-eureka
eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    service-url: # 定义eureka server url
      # 如果是集群的情况下,defaultZone设置为集群下的别的Eureka Server的地址,多个使用逗号隔开
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka
    register-with-eureka: true # 表示是否想Eureka 中心注册自己的信息,因为自己就是Eureka Server所以不进行注册,默认为true
    fetch-registry: true # 是否查询/拉取Eureka Server服务注册列表,默认为true
  instance:
#    hostname: LagouCloudEurekaServerB # 当前Eureka实例的主机名
    # 使用IP注册,否则会使用主机名进行注册(此处考虑到对老版本的兼容,新版本经过试验都是IP)
    prefer-ip-address: true
    # 自定义实例显示格式,如加上版本号,便于多版本管理,注意是ip-address z=早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

修改:eureka-server 9200 配置文件:
将其defaultZone修改为9201Eureka-server 的地址

server:
  port: 9200

spring:
  application:
    name: lagou-cloud-eureka
eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    service-url: # 定义eureka server url
      # 如果是集群的情况下,defaultZone设置为集群下的别的Eureka Server的地址,多个使用逗号隔开
      defaultZone: http://LagouCloudEurekaServerB:9201/eureka
    register-with-eureka: true # 表示是否想Eureka 中心注册自己的信息,因为自己就是Eureka Server所以不进行注册,默认为true
    fetch-registry: true # 是否查询/拉取Eureka Server服务注册列表,默认为true
  instance:
#    hostname: LagouCloudEurekaServerA # 当前Eureka实例的主机名
    # 使用IP注册,否则会使用主机名进行注册(此处考虑到对老版本的兼容,新版本经过试验都是IP)
    prefer-ip-address: true
    # 自定义实例显示格式,如加上版本号,便于多版本管理,注意是ip-address z=早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

修改商品微服务和静态化页面微服务的配置:
修改defaultZone客户端可以配置多个也可以配置一个,因为EurekaServer都是同步注册的.

server:
  port: 8001 # 在微服务的集群环境中,通常会为每一个微服务叠加

spring:
  application:
    name: lagou-service-product # 定义一个名称
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springclouddata?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: 123456

eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    service-url: # 定义eureka server url
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka,http://LagouCloudEurekaServerB:9201/eureka
    # 从EurekaServer 拉取服务列表的时间
    registry-fetch-interval-seconds: 30
  instance:
    #    hostname: localhost # 当前Eureka实例的主机名
    # 使用IP注册,否则会使用主机名进行注册(此处考虑到对老版本的兼容,新版本经过试验都是IP)
    prefer-ip-address: true
    # 自定义实例显示格式,如加上版本号,便于多版本管理,注意是ip-address z=早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    # 自定义元数据 会和标准元数据一起注册到服务注册中心,可以被注册中心所有的Client获取
    metadata-map:
      name: yuan
      age: 18
      master: laa
      password: 123
    # 续约间隔
    lease-renewal-interval-in-seconds: 30
    # 租约到期的时间
    lease-expiration-duration-in-seconds: 90

然后让静态页面微服务调用商品微服务的信息,通过DiscoveryClient获取商品微服务在服务注册中心的服务列表.

    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/getProduct/{id}")
    public Products getProduct(@PathVariable Integer id) {
        //获得lagou-server-product在服务注册中心注册的服务列表
        List<ServiceInstance> instances = discoveryClient.getInstances("lagou-service-product");
        //商品服务并没有集群,获得商品服务列表中的第一个即可
        ServiceInstance serviceInstance = instances.get(0);
        // 获取自定义的元数据
        Map<String, String> metadata = serviceInstance.getMetadata();
        //获得商品微服务的主机地址
        String host = serviceInstance.getHost();
        //获得商品微服务的端口号
        int port = serviceInstance.getPort();
        //调用相关的接口
        String url = "http://" + host + ":" + port + "/product/query/" + id;
        System.out.println(url);

        //发送HTTP请求给商品服务。将id传递过去获取锁对应Products
//        String url = "http://localhost:8001/product/query/" + id;
        //HTTP的远程调用
        Products products = restTemplate.getForObject(url, Products.class);
        return products;
    }

然后我们进行测试,首先启动eureka9200和eureka9201的服务注册中心,然后再启动商品微服务和静态化页面微服务。测试静态化页面微服务能否调用成功商品微服务

Eureka 细节

  1. Eureka 元数据

标准元数据:主机名、IP地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用
自定义元数据:可以使用eureka.instance.metadata-map

    # 自定义元数据 会和标准元数据一起注册到服务注册中心,可以被注册中心所有的Client获取
    metadata-map: 
      name: yuan
      age: 18
      master: laa
      password: 123

在使用该服务时,可以被所有client获取

ServiceInstance serviceInstance = instances.get(0);
Map<String, String> metadata = serviceInstance.getMetadata();

Eureka 客户端详解

服务提供者-也就是Eureka客户端,要向EurekaServer 注册服务,并完成服务续约等工作。
服务提供者:

  • 导入eureka-client依赖坐标,配置Eureka服务注册中心地址
  • 服务在启动时会向注册中心注册请求,携带服务元数据信息
  • Eureka注册中心会把服务的信息保存在Map中

服务续约:
服务每隔30秒会向注册中心续约(心跳)一次,如果没有续约,租约在90秒后到期,然后服务会被失效,每隔30秒的续约操作我们称之为心跳检测。
image.png
如下配置需要在eureka-client进行配置,一般不会修改默认的配置。

eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    # 从EurekaServer 拉取服务列表的时间
    registry-fetch-interval-seconds: 30
  instance:
    # 续约间隔
    lease-renewal-interval-in-seconds: 30
    # 租约到期的时间
    lease-expiration-duration-in-seconds: 90

Eureka服务剔除和自我保护机制

  • 服务下线
    • 当服务正常关闭操作时,会发送服务下线的REST请求给EurekaServer
    • 服务中心接受到请求后,将该服务置为下线状态
  • 失效剔除:Eureka Server 会定时(默认60S)进行检查,如果发现实例在一定时间(有客户端设置默认为90S)没有收到心跳,则会注销此实例
  • 自我保护机制:自我保护模式正式一种针对网络异常波动的安全保护措施,使用自我保护模式能使能使Eureka集群更加的健壮,稳定的运行。如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制:
    • Eureka 不再从注册列表中移除因为长时间没有收到心跳而应该过期的服务
    • Eureka Server 仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上,保证当前节点依然可用
    • 当网络稳定时,当前EurekaServer新的注册信息会被同步到其他节点中
    • 因此EurekaServer 可以很好的应对网络故障导致部分节点失联 的情况,而不会像Zookeeper(也是服务注册中心)那样如果有一半不可用的情况会导致整个集群不可用而变成瘫痪。
    • image.png
    • 自我保护机制是默认开启的,建议在生产环境下打开自我保护机制。
      server:
      enable-self-preservation: true # 默认true开启自我保护机制
      

      Ribbon 负载均衡

      负载均衡一般分为服务器端负载均衡和客户端负载均衡。
      服务器端负载均衡:比如Nginx F5这些,请求到达服务器之后由这些负载均衡器根据一定的算法将请求路由到目标服务器处理。
      客户端负载均衡:比如Ribbon,服务消费者客户端会有一个服务器地址列表,调用方在请求前通过一定的负载均衡算法选择一个服务器进行访问,负载均衡算法的执行是在请求客户端进行的。

      Ribbon 是Netfix发布的负载均衡器。Eureka一般配置Ribbon进行使用,Ribbon利用Eureka中读取到服务信息,在调用服务提供者提供的服务时,会根据一定的算法进行负载。

还是以上述的案例,如下:
image.png

Ribbon 的使用

Ribbon的依赖已经在eureka-client依赖中引入了。
image.png
复制一个商品微服务9001.创建一个controller

@RestController
@RequestMapping("/server")
public class ServerInfoController {

    //获取yml文件的配置
    @Value("${server.port}")
    private String port;

    @GetMapping("/port")
    public String getPort() {
        return port;
    }

}

由页面静态微服务配置负载均衡:

  1. 开启Ribbon负载均衡,添加@LoadBalanced 即可

    @SpringBootApplication
    //@EnableEurekaClient
    @EnableDiscoveryClient
    public class PageApplication {
     public static void main(String[] args) {
         SpringApplication.run(PageApplication.class, args);
     }
    
     //向容器中注入 RestTemplate 封装了HTTPclient
     @Bean
     @LoadBalanced //启用请求的负载均衡Ribbon
     public RestTemplate restTemplate() {
         return new RestTemplate();
     }
    }
    
  2. 调用商品微服务,如下代码直接写微服务的名称即可,不用再去注册中心获取服务列表这样的繁琐操作了

     @GetMapping("/getPort")
     public String getServerPort() {
    //        List<ServiceInstance> instances = discoveryClient.getInstances("lagou-service-product");
    //        ServiceInstance serviceInstance = instances.get(0);
    //        String host = serviceInstance.getHost();
    //        int port = serviceInstance.getPort();
         //直接写微服务的名称即可 不用再写上面的写法 直接由Ribbon走负载均衡算法
         String url = "http://lagou-service-product/server/port";
         String result = restTemplate.getForObject(url, String.class);
         return result;
     }
    

    负载均衡策略

    image.png

修改负载均衡策略,一般不需要修改直接默认策略即可。

# 针对的被调用方微服务名称,不加就是全局生效
lagou-service-product:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 采用随机策略

Ribbon 核心源码剖析

image.png
image.png
image.png

Hystrix熔断器

Hystrix熔断器是一种容错机制。

微服务中的雪崩效应:在微服务中,一个请求可能需要多个微服务接口才能实现,会形成复杂的调用链路。

服务的雪崩效应

服务雪崩效应:是一种因“服务提供者的不可用”导致“服务调用者不可用”,并将不可用逐渐放大的现象。
image.png
扇入:表示该微服务被调用的次数,扇入大,说明该模块复用性好
扇出:表示该服务调动其他微服务的个数,扇出大,说明业务逻辑复杂。
扇入大是好事,扇出大不一定是好事

服务雪崩的过程可以分为三个阶段:

  1. 服务提供者不可用
  2. 重试加大请求流量
  3. 服务调用者不可用

image.png

雪崩效应解决方案

从可用性可靠性,为防止系统的整体缓慢甚至崩溃,采用的技术手段

  • 服务熔断

熔断机制是应对雪崩效应的一种微服务链路保护机制。在微服务架构中,熔断机制也是起着类似的
作用,当扇出链路的某个微服务不可用或者响应时间太长时,熔断该节点微服务的调用,
进行服务的降级,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。

服务熔断重点在”“,切断对下游服务的调用。
服务熔断和服务降级往往是一起使用的,Hystrix就是这样。

  • 服务降级

    服务降级:通俗来说就是整体资源不够用了,先将一些不关紧要的服务停掉,调用我的时候,给你返回一个 预留值,也是兜底数据。待度过难关高峰过去,再把那些服务打开。

服务降级一般是从整体考虑,就是当某个服务熔断之后,服务器将不再被调用,此刻客户端可以自己准备一个本地的
fallback回调,返回一个缺省值,这样的话,虽然服务水平下降,但是好歹可用,比直接挂掉强。

  • 服务限流

服务降级是当服务出现问题或者影响到了核心流程的性能时,暂时将服务屏蔽掉,待高峰或者问题解决后再打开;但是有些场景并不能用服务降级来解决,比如秒杀业务这样的核心功能,你不能直接将秒杀服务停掉,这个时候可以结合服务限流来限制这些场景的并发/请求量。
限流的措施比如:

  • 限制总并发数(比如数据库连接池、线程池)
  • 限制瞬时并发数(比如Nginx限制瞬时并发连接数)
  • 限制时间窗口内的平均速率(如Guava的RateLimiter nginx的limit_req模块,限制每秒的平均速率)
  • 限制远程接口调用速率、限制MQ消费速率等。

Hystrix

Hystrix 翻译豪猪。“defend your application”是由Netfix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性,Hystrix主要通过以下几点实现延迟和容错:

  • 包裹请求:使用HystrixCommand包裹对依赖的调用逻辑。页面静态化微服务方法(@HystrixCommand添加Hystrix控制)
  • 跳闸机制:当某服务的错误了超过了一定的阈值时,Hystrix可以跳闸,停止请求该服务一段时间。
  • 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(舱壁模式)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
  • 监控 :Hystrix 可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。
  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值。
  • 自我修复:断路器打开一段时间后,会自动进入”半开”状态 (会探测服务是否可用,如果还是不可用,再次退回打开状态)

Hystrix的应用:

  1. 熔断处理

例如:商品微服务长时间没有响应,页面静态化微服务快速失败给用户提示。

  • 注意由服务消费者工程-静态化微服务中引入Hystrix依赖坐标。也可以添加在父工程中

          <!-- Hystrix 熔断依赖 -->
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
          </dependency>
    
  • 开启熔断:在服务消费者工程-静态化微服务的启动类中添加熔断器开启注解

    @SpringBootApplication
    //@EnableEurekaClient 启用服务客户端
    @EnableDiscoveryClient
    //启用熔断服务
    @EnableCircuitBreaker
    public class PageApplication {
      public static void main(String[] args) {
          SpringApplication.run(PageApplication.class, args);
      }
    
      //向容器中注入 RestTemplate 封装了HTTPclient
      @Bean
      @LoadBalanced //启用请求的负载均衡Ribbon
      public RestTemplate restTemplate() {
          return new RestTemplate();
      }
    }
    
  • 定义服务熔断处理方法:在具体的业务方法上使用@HystrixCommand

      /**
       * 模拟服务超时熔断处理
       * 针对熔断处理,Hystrix默认维护一个线程池,默认大小为10
       *
       * @return
       */
      @HystrixCommand(
              threadPoolKey = "getPort2", //每个方法维护一个线程池,如果不写该配置 默认所有的请求共同维护一个线程池,实际开发中每个方法维护一个线程池
              threadPoolProperties = {
                      @HystrixProperty(name = "coreSize", value = "1"),//并发线程数
                      @HystrixProperty(name = "maxQueueSize", value = "20"), //默认线程队列值是-1 默认不开启。
              }, // 每一个属性对应的都是HystrixProperty
              //超时时间的设置
              commandProperties = {
                      //设置请求的超时时间,一旦请求超时按照超时处理,默认超时时间1000ms
                      @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
              }
      )
      @GetMapping("/getPort2")
      public String getServerPort2() {
          //直接写微服务的名称即可 不用再写上面的写法 直接由Ribbon走负载均衡算法
          String url = "http://lagou-service-product/server/port";
          String result = restTemplate.getForObject(url, String.class);
          return result;
      }
    
  • 服务提供者:商品微服务模拟超时操作,在静态化微服务中,我们设置了请求超时的熔断机制为2000ms ```java @RestController @RequestMapping(“/server”) public class ServerInfoController {

    //获取yml文件的配置 @Value(“${server.port}”) private String port;

    @GetMapping(“/port”) public String getPort() {

      try {
          Thread.sleep(5000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
      return port;
    

    }

}

测试确实无法请求商品服务了,但是直接报了500的错误,一般在生产环境是不允许的,所以我们需要做降级处理,返回一个缺省值。

2. 降级处理

继续修改静态化微服务的controller,通过`@HystrixCommand` 中的`fallbackMethod` 的设置回退方法,直接填方法名即可,当调用商品服务出现超时熔断就会调用该方法,返回。
```java
 /**
     * 服务降级是在服务熔断之后的兜底操作
     */
    @HystrixCommand(
            //超时时间的设置
            commandProperties = {
                    //设置请求的超时时间,一旦请求超时按照超时处理,默认超时时间1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
            },
            //设置回退的方法
            fallbackMethod = "getProductServerPortFallback"
    )
    @GetMapping("/getPort3")
    public String getServerPort3() {
        //直接写微服务的名称即可 不用再写上面的写法 直接由Ribbon走负载均衡算法
        String url = "http://lagou-service-product/server/port";
        String result = restTemplate.getForObject(url, String.class);
        return result;
    }

    /**
     * 定义回退方法,当请求发生熔断后执行,补救措施
     * 1. 方法的形参和原方法保持一致
     * 2. 方法的返回值与原方法保持一致
     */
    public String getProductServerPortFallback() {
        return "-1";
    }

服务熔断和降级是配合使用的。

Hystrix舱壁模式

Hystrix舱壁模式即:线程池隔离策略。

如果不进行任何设置,所有熔断方法使用一个Hystrix线程池(10个线程),那么这样的话会导致问题,这个问题并不是扇出链路不可用导致的,而是因为Hystrix的线程机制导致的,如果把方法A的请求把10个线程都用了没有释放,那么方法B请求处理的时候压根都访问不了,因为没有线程可用,并不是B服务不可用

image.png
所以为了避免上述的问题请求过多导致正常服务无法访问,Hystrix不是采用增加线程数,而是单独的为每一个控制方法创建一个线程池的方式,这种模式就是”舱壁模式”,也是线程隔离的手段。
设置方法如下:

 @HystrixCommand(
            threadPoolKey = "getPort2", //每个方法维护一个线程池,如果不写该配置 默认所有的请求共同维护一个线程池,实际开发中每个方法维护一个线程池
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize", value = "1"),//并发线程数
                    @HystrixProperty(name = "maxQueueSize", value = "20"), //默认线程队列值是-1 默认不开启。
            }, // 每一个属性对应的都是HystrixProperty
    )

Hystrix工作流程

image.png

  1. 当调用出现问题时,开启一个时间窗 10S
  2. 在这个时间窗内,统计调用次数是否达到最小请求数
    1. 如果没有达到,则重置统计信息,回到第一步
    2. 如果达到了,则统计失败的请求数占所有请求数的百分比,是否达到阈值
      1. 如果达到,则跳闸 不再请求对应服务
      2. 如果没有达到,则重置统计信息,回到第一步
  3. 如果跳闸,则会开启一个活动窗口 默认5S,每隔5S,Hystrix会让一个请求通过,到这哪个问题服务,看是否调用成功,如果成功,重置断路器回到第一步,如果失败,回到第三步

设置如下:

            commandProperties = {
                    // 统计窗口时间的设置
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "8000"),
                    // 统计窗口内的最小请求数
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "2"),
                    // 统计窗口内的错误请求阈值的设置 50%
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
                    // 自我修复的活动窗口时间
                    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "3000")
            },

同时也可以在配置文件中设置,进行全局应用

# hystrix 熔断策略配置
hystrix:
  command:
    default:
      circuitBreaker:
        # 强制打开熔断器,如果该属性设置为true,强制断路器进入打开状态,将会拒绝所有的请求。
        # 默认false关闭的
        forceOpen: false
        # 触发熔断错误比例阈值,默认值50%
        errorThresholdPercentage: 50
        # 熔断后休眠时长,默认值5秒
        sleepWindowInMilliseconds: 3000
        # 熔断触发最小请求次数,默认值是20
        requestVolumeThreshold: 2
      execution:
        isolation:
          thread:
            # 熔断超时设置,默认为1秒
            timeoutInMilliseconds: 2000

基于springboot的健康检查观察跳闸状态

# springboot 中暴露健康检查等断点接口  /actuator/health
management:
  endpoints:
    web:
      exposure:
        include: "*"
  # 暴露的细节
  endpoint:
    health:
      show-details: always

http://localhost:9100/actuator/health

Hystrix 线程池队列配置案例

maxQueueSize属性单独设置不起作用,通过查看官方文档发现Hystrix还有一 个queueSizeRejectionThreshold属性,这个属性是控制队列最大阈值的,而Hystrix默认只配置了5个, 因此就算我们把maxQueueSize的值设置再大,也是不起作用的。两个属性必须同时配置。

hystrix:
  threadpool:
    default:
      coreSize: 10 # 并发执行的最大线程数,默认是10
      maxQueueSize: 1500 # BlockingQueue的最大队列数,默认值 -1
      queueSizeRejectionThreshold: 800 # 即使maxQueueSize没有达到,到达了queueSizeRejectionThreshold的值后,请求也会被拒绝,默认值5

Feign 远程调用组件

Feign是Netfix开发的一个轻量级RESTful的HTTP服务客户端 用它来发起请求,远程调用的。 Feign 类似于Dubbo,服务消费者拿到服务提供者的接口,然后像调用本地接口方法一样去调用,实际发出的是远程的请求。更符合面向接口化的编程习惯。 Feign可帮助我们更加便捷,优雅的调用HTTP API:不需要像之前去拼接URL调用RestTemplate的api,在spring cloud中,使用Feign非常简单,创建一个接口(在消费者,服务调用方)并在接口上添加一些注解即可。 Feign = RestTemplate + Ribbon + Hystrix

Feign 的应用

还是以开始的案例:

  • 由服务消费工程(页面静态化服务)引入Feign依赖

          <!-- feign 依赖 远程调用组件 -->
          <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-openfeign</artifactId>
          </dependency>
    
  • 在启动类添加注解@EnableFeignClients 添加Feign支持 :::tips 注意要去掉Hystrix熔断的支持注解,因为Feign会自动引入 :::

    @SpringBootApplication
    //@EnableEurekaClient server client 服务发现
    @EnableDiscoveryClient
    //启用熔断服务
    //@EnableCircuitBreaker
    // 开启Feign客户端功能 feign 支持熔断所以可以注释掉hystrix熔断即可
    @EnableFeignClients
    public class PageApplication {
      public static void main(String[] args) {
          SpringApplication.run(PageApplication.class, args);
      }
    
      //向容器中注入 RestTemplate 封装了HTTPclient
      @Bean
      @LoadBalanced //启用请求的负载均衡Ribbon
      public RestTemplate restTemplate() {
          return new RestTemplate();
      }
    }
    
  • 创建Feign接口 需要注解接口中的方法要和服务提供者在controller中定义的一致 :::tips 注意:一个微服务提供者,最好只有一个接口,因为在spring boot 2.1 之后,同名的nam会报错 :::

    @FeignClient(name = "lagou-service-product",fallback = ProductFeignFallback.class)
    public interface ProductFeign {
    
      /**
       * 通过商品id查询商品的对象
       * @param id
       * @return
       */
      //GetMapping url 要是全路径
      @GetMapping("/product/query/{id}")
       Products query(@PathVariable Integer id);
    
      /**
       * 获取端口号的信息
       * @return
       */
      @GetMapping("/server/port")
      String getPort();
    }
    
  1. @FeignClient 注解的name属性用于指定要调用的服务提供者名称和服务提供者yml文件中的spring.application.name 保持一致. fallback 属性设置的就是调用服务熔断后的处理类
  2. 接口中的接口方法,就好比是远程服务提供者的Controller中的方法可以使用@PathVariable @RequestParam 等Feign对Spring MVC注解的支持
  • 配置熔断触发之后的逻辑,我们需要新建一个类去实现ProductFeign 在实现的方法中对每个方法的熔断做处理即可

    @Component
    public class ProductFeignFallback implements ProductFeign {
      @Override
      public Products query(Integer id) {
          return null;
      }
    
      @Override
      public String getPort() {
          return "这是getPort的兜底数据";
      }
    }
    
  • 改造原来的PageController类

    @RestController
    @RequestMapping("/page")
    public class PageController {
    
      @Autowired
      private ProductFeign productFeign;
    
      @GetMapping("/getProduct/{id}")
      public Products getProduct(@PathVariable Integer id) {
          Products products = productFeign.query(id);
          return products;
      }
    
      @GetMapping("/getPort")
      public String getServerPort() {
          String result = productFeign.getPort();
          return result;
      }
    }
    

    Feign对负载均衡的支持

    Feign 本省已经集成了Ribbon依赖和自动配置,所以我们不需要额外的引入依赖,可以通过ribbon.xx 来进行全局配置等
    其实在之前我们学习Ribbon的时候的配置就是生效的

    # 针对的被调用方微服务名称,不加就是全局生效
    lagou-service-product:
    ribbon:
      # 请求连接超时时间
      ConnectTimeout: 2000
      # 请求处理超时时间
      ReadTimeout: 15000
      # 对所有操作都进行重试
      OkToRetryOnAllOperations: true
      # 根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例
      MaxAutoRetries: 0 # 对当前选中实例重试次数,不包括第一次调用
      MaxAutoRetriesNextServer: 0 # 切换实例的重试次数
      NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 采用随机策略
    

Feign对熔断器的支持

application.yml 中开启Feign对熔断器的支持,那么Hystrix的配置就按照我们之前学习的Hystrix的配置即可

# 开启feign熔断功能
feign:
  hystrix:
    enabled: true
 # hystrix 熔断策略配置
hystrix:
  threadpool:
    default:
      coreSize: 10 # 并发执行的最大线程数,默认是10
      maxQueueSize: 1500 # BlockingQueue的最大队列数,默认值 -1
      queueSizeRejectionThreshold: 800 # 即使maxQueueSize没有达到,到达了queueSizeRejectionThreshold的值后,请求也会被拒绝,默认值5
  command:
    default:
      circuitBreaker:
        # 强制打开熔断器,如果该属性设置为true,强制断路器进入打开状态,将会拒绝所有的请求。
        # 默认false关闭的
        forceOpen: false
        # 触发熔断错误比例阈值,默认值50%
        errorThresholdPercentage: 50
        # 熔断后休眠时长,默认值5秒
        sleepWindowInMilliseconds: 3000
        # 熔断触发最小请求次数,默认值是20
        requestVolumeThreshold: 2
      execution:
        isolation:
          thread:
            # 熔断超时设置,默认为1秒
            timeoutInMilliseconds: 2000

Feign对请求压缩和响应压缩的支持

Feign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

feign:
  hystrix:
    enabled: true
  # 开启请求和响应的压缩 默认是不开启的需要手动开启
  compression:
    response:
      enabled: true
    request:
      enabled: true
      # 最小的压缩的下限
#      min-request-size: 2048 # 默认值 不需要动
#      mime-types: 默认值 不需要动

GateWay 网关组件

网关:在微服务架构中的重要组成部分。 Spring Cloud GateWay是spring cloud的一个全新的项目,目标是取代Netfix Zuul,它基于Spring5.0 + SpringBoot2.0 + WebFlux 基于高性能的Reactor模式响应式通信框架Netty,异步非阻塞模型等开发技术。GateWay是Zuul的1.6倍,旨在为微服务架构提供一种简单有效的统一的API路由管理方式 Spring Cloud GateWay不仅提供统一的路由方法(反向代理)并且基于Filter 定义过滤器对请求过滤,完成一些功能,以及Filter链的方式提供了网关的基本功能,例如:鉴权、流量控制、熔断、路径重写、日志监控等。

网关在微服务的位置:
image.png

GateWay核心概念

一个请求 -> 网关根据一定的条件匹配 -> 匹配成功之后可以将请求转发到指定的服务地址;而在这个过程中,我们可以进行一些比较具体的控制(限流、日志、黑白名单)

  • 路由(route):网关最基础的部分,也是网关比较基础的工作单元。路由由一个ID、一个目标URL、一系列的断言(匹配条件判断)和Filter(过滤器)精细化控制组成
  • 断言(predicates):参考了Java8中的java.util.function.Predicate 开发人员可以匹配HTTP请求中的所有内容(包括请求头、请求参数等)如果断言与请求相匹配则路由。
  • 过滤器(filter): 一个标准的Spring webFilter 使用过滤器,可以在请求之前或者之后执行业务逻辑

image.png

Spring的官方介绍: 客户端向Spring Cloud GateWay发出请求,然后在GateWay Handler Mapping中找到与请求相匹配的路由,将其发送到GateWay Web Handler,Handler在通过指定的过滤器链来讲请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或者之后(post)执行业务逻辑。 Filter 在“pre”类型过滤器中可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在“post”类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等。

GateWay 应用

网关与原有的微服务依赖最好独立。

  • 创建gateway项目工程导入依赖 ```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">
    
    4.0.0

    com.prim lagou-cloud-gateway

    1.0-SNAPSHOT 8 8 org.springframework.boot spring-boot-starter-parent 2.1.6.RELEASE org.springframework.cloud spring-cloud-dependencies Greenwich.RELEASE pom import org.springframework.cloud spring-cloud-commons org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-test test org.projectlombok lombok 1.18.4 provided com.sun.xml.bind jaxb-core 2.2.11 javax.xml.bind jaxb-api com.sun.xml.bind jaxb-impl 2.2.11 org.glassfish.jaxb jaxb-runtime 2.2.10-b140310.1920 javax.activation activation 1.1.1 org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools true org.springframework.cloud spring-cloud-starter-sleuth org.springframework.cloud spring-cloud-starter-zipkin
<build>
    <plugins>
        <!--编译插件-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>8</source>
                <target>8</target>
                <encoding>utf-8</encoding>
            </configuration>
        </plugin>
        <!--打包插件-->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>


- `application.yml` 配置文件进行配置,将我们的网关服务注册到Eureka服务中心,方便调用其他的微服务
```yaml
server:
  port: 9300

spring:
  application:
    name: lagou-cloud-gateway
  cloud:
    # 网关的配置
    gateway:
      routes: # 配置路由
        - id: service-page-router
#          uri: http://127.0.0.1:9100
          # 因为我们的网关已经 在eureka服务中心注册了 所以可以使用动态路由 根据微服务的名称调用
          uri: lb://lagou-service-page
          predicates: # 断言 当断言成功后,交给某一个微服务处理,使用的转发
            - Path=/page/**
        - id: service-product-router
#          uri: http://127.0.0.1:9000
          uri: lb://lagou-service-product
          predicates:
            - Path=/product/**
          filters: # 过滤器设置
            - StripPrefix=1 # 去掉uri中的第一部分例如uri是:http://127.0.0.1:9000/product/service/port
            # http://localhost:9300/product/product/query/1  controller:product/query/1 例如访问商品的接口要这样写
            # -> http://127.0.0.1:9000/service/port 这才是真正的uri 在实际开发重要注意在controller中设置的RequestMapping是否和Path有冲突


eureka:
  client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
    service-url: # 定义eureka server url
      # 客户端可以配置多个也可以配置一个,因为EurekaServer都是同步注册的
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka,http://LagouCloudEurekaServerB:9201/eureka
  instance:
    #    hostname: localhost # 当前Eureka实例的主机名
    # 使用IP注册,否则会使用主机名进行注册(此处考虑到对老版本的兼容,新版本经过试验都是IP)
    prefer-ip-address: true
    # 自定义实例显示格式,如加上版本号,便于多版本管理,注意是ip-address z=早期版本是ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

image.png

GateWay 过滤器

image.png
自定义全局过滤器实现IP访问限制 黑白名单

 @Slf4j
@Component //扫描注入到IOC容器中
public class BlackListFilter implements GlobalFilter, Ordered {

    //模拟黑名单列表 实例可以去数据库或者Redis中查询
    private static List<String> blackList = new ArrayList<>();

    static {
        blackList.add("0:0:0:0:0:0:0:1");
        blackList.add("127.0.0.1");
    }

    /**
     * 过滤器的核心方法
     * @param exchange 封装了request和response对象的上下文
     * @param chain 网关过滤器链 包含全局过滤器和单路由过滤器
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("----------BlackListFilter-----------");
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        // 从request对象获取客户端IP地址
        String clienIp = request.getRemoteAddress().getHostString();
        if (blackList.contains(clienIp)){
            //存在黑名单中拒绝访问 返回状态数据
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            log.info("=====>IP:"+clienIp+" 在黑名单中,拒绝访问!");
            String data = "Request be denied!";
            DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
            return response.writeWith(Mono.just(wrap));
        }
        // 合法请求执行 后续的过滤器
        return chain.filter(exchange);
    }

    /**
     * 返回值表示当前过滤器的顺序 优先级,数值越小,优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

GateWay 高可用

网关是非常核心的组件,如果挂掉,那么所有请求都可能无法路由处理,所以GateWay需要实现高可用。 可以启动多个GateWay实例来实现高可用,在GateWay的上游使用Nginx等负载均衡进行负载转发以达到高可用的目的 。

在Nginx的配置文件中,进行配置:
image.png

Spring Cloud Config 分布式配置中心

在微服务架构中,因为我们的分布式集群环境中可能有多个微服务,我们不可能一个一个去修改配置然后重启生效,在一定的场景下我们还需要在运行期间动态调整配置信息,比如:根据各个微服务的负载情况,动态调整数据源连接池大小,当配置内容发生变化的时候,微服务可以自动更新。

  1. 集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的
  2. 不同环境不同配置,比如数据源配置在不同环境(开发dev,测试test,生成prod)中是不同的
  3. 运行期间可动态调整。例如可根据各个微服务的负载情况,动态调整数据源连接池大小等配置修改后可自动更新
  4. 如配置内容发生变化,微服务可以自动更新配置。

所以需要对配置文件进行集中式管理,这也是分布式配置中的作用。

Spring Cloud Config 是一个分布式配置管理方案,包含了Server端和Client端两部分。
image.png

  • Server端:提供配置文件的存储、以接口的形式将配置文件的内容提供出去,通过使用@EnableConfigServer 注解在Spring Boot应用中非常简单的嵌入。
  • Client 端:通过接口获取配置数据并初始化自己的应用

Config Server是集中式的配置服务,用于集中管理应用程序各个环境下的配置。默认使用Git存储配置文件内容,也可以是SVN。

例如:我们对案例中的“页面静态化微服务”的application.yml进行管理 注意区分环境:dev test prod

  1. 登录GitHub 创建项目test-config
  2. 上传yml配置文件,命名规则:{application}-{profile}.yml . 其中application为应用名称,profile是环境。例如:lagou-server-page-dev.yml
  3. 创建Config Server工程,引入坐标依赖,将配置服务注册到Eureka中

     <dependencies>
         <!--eureka client 客户端依赖引入-->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
         </dependency>
         <!--config配置中心服务端-->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-config-server</artifactId>
         </dependency>
     </dependencies>
    
  4. 配置启动类

    @SpringBootApplication
    @EnableDiscoveryClient //发现服务
    @EnableConfigServer //启用配置中心
    public class ConfigApplication {
     public static void main(String[] args) {
         SpringApplication.run(ConfigApplication.class, args);
     }
    }
    
  5. application.yml 配置

    server:
    port: 9400
    spring:
    application:
     name: lagou-server-config
    cloud:
     config:
       server:
         git:
           uri: https://github.com/JakePrim/test-config.git
           username: XXX
           password: XXXX
           search-paths:
             - test-config
       # 读取分支
       label: master
    # 注册服务到服务中心
    eureka:
    client: # Eureka server本身也是Eureka的一个客户端,因为在集群下需要与其他Eureka Server 进行数据同步
     service-url: # 定义eureka server url
       # 客户端可以配置多个也可以配置一个,因为EurekaServer都是同步注册的
       defaultZone: http://LagouCloudEurekaServerA:9200/eureka,http://LagouCloudEurekaServerB:9201/eureka
    instance:
     #    hostname: localhost # 当前Eureka实例的主机名
     # 使用IP注册,否则会使用主机名进行注册(此处考虑到对老版本的兼容,新版本经过试验都是IP)
     prefer-ip-address: true
     # 自定义实例显示格式,如加上版本号,便于多版本管理,注意是ip-address z=早期版本是ipAddress
     instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    

    运行配置中心服务,然后访问:http://localhost:9400/master/lagou-page-server-application-dev.yml 查看是否可以访问成功配置文件,访问结果如下:
    image.png

  6. 在页面静态化微服务中添加config client依赖,表示为配置中心客户端

         <!-- 配置中心客户端 -->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-config-client</artifactId>
         </dependency>
    
  7. 将页面静态化微服务的配置application.yml 修改为bootstrap.yml 它是系统级别的,优先级比application.yml高,应用启动时会检查这个配置文件,在这个配置文件中指定配置中心的服务地址,会自动拉取所有应用配置并且启用

    spring:
    # 配置中心服务配置
    cloud:
     config:
       # config客户端配置,和ConfigServer通信,并告知ConfigServer希望获取的配置信息在哪个文
       #件中
       name: lagou-page-server-application
       profile: dev # 后缀名称
       label: master # 分支名称
       uri: http://localhost:9400
    
  8. 修改GitHub上的配置文件,添加两个自定义属性

    mysql: 
    user: JakePrim
    person:
    name: 楼的话
    
  9. 编写Controller获取自定义属性,并返回

    @RestController
    @RequestMapping("/config")
    public class ConfigController {
     @Value("${mysql.user}")
     private String mysqlUser;
    
     @Value("${person.name}")
     private String personName;
    
     @RequestMapping("/remote")
     public String findRemoteConfig() {
         return mysqlUser + "  " + personName;
     }
    }
    

    重新运行页面静态化微服务,测试:http://localhost:9300/page/config/remote
    返回结果:JakePrim 楼的话

Config 配置手动刷新

在上述的例子中,需要重启微服务才会拉取配置文件,那么是否可以不用重启微服务,只需要手动的做一些其他的操作(比如访问某个地址)刷新配置信息,之后再访问即可。 客户端使用post去触发refresh,获取最新的配置信息。即手动刷新配置。

  1. 在client客户端添加依赖springboot-starter-actuator
  2. 在client客户端bootstrap.yml 添加配置

    # springboot 中暴露健康检查等断点接口  /actuator/health
    management:
    endpoints:
     web:
       exposure:
         include: refresh # 也可以使用 "*" 暴露所有端口
    
  3. 在客户端使用到的配置信息的类上添加注解@RefreshScope

    @RestController
    @RequestMapping("/config")
    @RefreshScope //手动刷新
    public class ConfigController {
     @Value("${mysql.user}")
     private String mysqlUser;
    
     @Value("${person.name}")
     private String personName;
    
     @RequestMapping("/remote")
     public String findRemoteConfig() {
         return mysqlUser + "  " + personName;
     }
    }
    
  4. 重启页面静态化微服务,发起post请求:刷新配置

修改配置文件:

mysql: 
  user: JakePrim-2
person:
  name: 楼的话-2

image.png
访问:http://localhost:9300/page/config/remote
image.png
手动刷新避免了服务重启,那么能否实现自动刷新呢?当配置文件更新时,自动刷新配置。

消息总线Bus

在微服务架构中,可以结合消息总线(Bus)实现分布式配置的自动更新(Spring Cloud Config + Spring Cloud Bus) 所谓消息总线Bus,是我们经常使用MQ消息代理构建一个共用的Topic,通过这个Topic连接各个微服务实例,MQ广播消息会被所有在注册中心的微服务实例监听和消费。换言之就是通过一个Topic连接各个微服务,打通脉络。 Spring Cloud Bus(基于MQ的,支持RabbitMQ/Kafka) 是SpringCloud的消息总线方案。

image.png

实现自动刷新

这里MQ消息代理,我们选择使用RabbitMQ,ConfigServer和ConfigClient都添加消息总线的支持以及与RabbitMQ的链接信息.
首先启动Linux虚拟机,开启RabbitMQ服务。
如果你没有安装RabbitMQ看这篇文章进行安装,启动:
RabbitMQ 入门实战

  1. Config 服务端和客户端添加消息总线支持

         <!-- 消息总线支持 -->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-bus-amqp</artifactId>
         </dependency>
    
  2. Config 服务端和客户端添加配置

    spring:
    # MQ配置
    rabbitmq:
     host: 172.16.160.130
     port: 5672
     username: prim # 注意用户名和密码 需要你在安装RabbitMQ的时候 手动创建账户
     password: 123456
    
  3. Config Server 微服务暴露端口

    management:
    endpoints:
     web:
       exposure:
         include: "*" # bus-refresh 建议暴露所有的端口
    
  4. 重启各个服务,更改配置之后,向配置中心服务端发送post请求,各个客户端配置

即可自动刷新http://127.0.0.1:9400/actuator/bus-refresh

  1. Config Client 测试

访问接口:post http://localhost:9400/page/config/remote,在广播模式下实现了一次请求,处处更新,如果定向更新呢?
在发起刷新请求的是时候:
post 请求,实现定向更新某个微服务的配置
http://localhost:9400/actuator/bus-refresh/lagou-service-page:9100

所有代码地址: