Spring Cloud:Eureka,Ribbon,Hystrix,Feign,GateWay,Config

网关组件 Zuul 性能一般,未来将退出 Spring Cloud 生态圈,所以直接使用 GateWay,把 GateWay 划分到第一代 Spring Cloud 核心组件中。

各组件整体结构如下:

Gateway:所有服务的入口;日志、黑白名单、鉴权。

Ribbon:负载均衡。

Hystrix:熔断器,服务熔断,服务降级。

Feign:远程调用。

Eureka:服务注册与发现;微服务名称、IP、端口号。

Config:搭建配置中心微服务;实现对配置文件的统一管理,配置自动刷新 - bus。

Actor
---> Gateway 网关
--转发--> 
{
  [搜索微服务,搜索微服务],
  [商品微服务,商品微服务],
  [支付微服务,支付微服务],
  [秒杀微服务,秒杀微服务],
  RestTemplate + Ribbon + Hystrix 或 Feign
}
---->
{
  服务注册中心 Eureka,
  配置中心 Config
}

从形式上来说,Feign 一个顶三,Feign = RestTemplate + Ribbon + Hystrix

Eureka 服务注册中心

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

关于服务注册中心

注意:服务注册中心本质上是为了解耦服务提供者和服务消费者。

服务消费者 -> 服务注册中心 -> 服务提供者。

对于任何一个微服务,原则上都应存在或者支持多个提供者(比如商品微服务部署多个实例),这是由微服务的分布式属性决定的。

更进一步,为了支持弹性扩、缩容特性,一个微服务的提供者的数量和分布往往是动态变化的,也是无法预先确定的。因此,原本在单体应用阶段常用的静态 LoadBalance 机制就不再适用了,需要引入额外的组件来管理微服务提供者的注册与发现,而这个组件就是服务注册中心。

注册中心实现原理
1. 启动:
容器 --> 服务注册中心
容器 --> 服务提供者
容器 --> 服务消费者

2. 注册:
服务消费者 --> 服务注册中心
服务提供者 --> 服务注册中心

3. 获取服务信息:
服务消费者 <--获取服务信息--> 服务注册中心

4. Invoke:
服务消费者 --负载均衡-熔断机制--> 服务提供者

分布式微服务架构中,服务注册中心用于存储服务提供者地址信息、服务发布相关的属性信息,消费者通过主动查询和被动通知的方式获取服务提供者的地址信息,而不再需要通过硬编码方式得到提供者的地址信息。消费者只需要知道当前系统发布了那些服务,而不需要知道服务具体存在于什么位置,这就是透明化路由。

1)服务提供者启动。

2)服务提供者将相关服务信息主动注册到注册中心。

3)服务消费者获取服务注册信息:Pull 模式 - 服务消费者可以主动拉取可用的服务提供者清单;Push 模式 - 服务消费者订阅服务,当服务提供者有变化时,注册中心也会主动推送更新后的服务清单给消费者。

4)服务消费者直接调用服务提供者。

另外,注册中心也需要完成服务提供者的健康监控,当发现服务提供者失效时需要及时剔除。

主流服务中心对比

Zookeeper:

Dubbo + Zookeeper

zookeeper = 存储 + 监听通知

Zookeeper 是一个分布式服务框架,是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

Zookeeper 用来做服务注册中心,主要是因为它具有节点变更通知功能,只要客户端监听相关服务节点,服务节点的所有变更,都能及时的通知到监听客户端,这样作为调用方只要使用 Zookeeper 的客户端就能实现服务节点的订阅和变更通知功能了,非常方便。另外,Zookeeper 可用性也可以,因为只要半数以上的选举节点存活,整个集群就是可用的,最少节点数为 3。

Eureka:

Eureka 由 Netflix 开源,并被 Pivatal 集成到 Spring Cloud 体系中,它是基于 RestfulAPI 风格开发的服务注册与发现组件。

Consul:

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

Nacos:

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

CAP 定理:

CAP 定理又称 CAP 原则,指的是在一个分布式系统中,Consistency 一致性、 Availability 可用性、Partition tolerance 分区容错性,最多只能同时三个特性中的两个,三者不可兼得。

P:分区容错性 - 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务(一定的要满足的)。

C:数据一致性 - all nodes see the same data at the same time.

A:高可用 - Reads and writes always succeed.

CAP 不可能同时满足三个,要么是 AP,要么是 CP。

对比:

  • Eureka - Java - AP - HTTP
  • Consul - Go - CP - HTTP / DNS
  • Zookeeper - Java - CP - 客户端
  • Nacos - Java - 支持 AP / CP 切换 - HTTP

服务注册中心组件 Eureka

服务注册中心的一般原理、对比了主流的服务注册中心方案,目光聚焦 Eureka。

Eureka 基础架构:

Eureka Server 需要手动搭建一个工程,并引入相关依赖,进行对应的配置文件设置。

Renew 心跳 / 心跳检测:服务注册默认 30s 续约,默认 90s 没有收到续约就会将 Client 实例从服务列表中剔除。

ApplicationService 服务提供者 
----> Eureka Client 
--注册服务--> Eureka Server 注册中心

Eureka Server 注册中心 
--服务列表-缓存--> Eureka Client 
----> ApplicationClient 客户端消费者

ApplicationClient 客户端消费者 --调用服务--> ApplicationService 服务提供者

Eureka 交互流程及原理:

不同的 Eureka Server 会互相同步复制(Replicate)服务实例列表;
每个 Eureka Server 是一个集群;
Eureka Server 搭建集群来保持高可用服务注册中心;
每个 Eureka Server 可能处于不同地域不同的机房。

Eureka 服务注册中心:[Eureka Server 1, Eureka Server 2, Eureka Server 3]

Appllication Service 服务提供者 - 含有 Eureka Client
Application Client 服务消费者 - 含有 Eureka Client

服务提供者可以进行 Register, Renew, Cancel, Get Registry 于服务注册中心。
服务消费者可以进行 Get Registry 于服务注册中心。

服务消费者 Make Remote Call 于服务提供者。

Eureka 包含两个组件 - Eureka Server 和 Eureka Client:

  • Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互;Eureka Server 提供服务发现的能力。

  • 各个微服务启动时,会通过 Eureka Client 向 Eureka Server 进行注册自己的信息(如网络信息),Eureka Server 会存储该服务的信息。

  • Application Service 作为服务提供者向 Eureka Server 中注册服务,Eureka Server 接受到注册事件会在集群和分区中进行数据同步,Application Client 作为消费端(服务消费者)可以从 Eureka Server 中获取到服务注册信息,进行服务调用。

  • 微服务启动后,会周期性地向 Eureka Server 发送心跳以续约自己的信息; Eureka Server 默认心跳续约周期为 30s,默认 90s 后会将还没有续约的 Client 给剔除。

  • 如果 Eureka Server 在一定时间内没有接收到某个微服务节点的心跳,将会注销该微服务节点(默认 90 秒)。

  • 每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过复制的方式完成服务注册列表的同步。

  • Eureka Client 会缓存 Eureka Server 中的信息。即使所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。

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

搭建单例 Eureka Server 服务注册中心

实现过程:

  1. 单实例 Eureka Server -> 访问管理界面。
  2. 服务提供者(商品微服务注册到集群)。
  3. 服务消费者(页面静态化微服务注册到 Eureka / 从 Eureka Server 获取服务信息)。
  4. 完成调用 。

1)搭建 Eureka Server 服务 lagou-cloud-eureka

lagou-parent 中增加 Spring Cloud 版本号依赖管理;Spring Cloud 是一个综合的项目,下面有很多子项目,比如 eureka 子项目;Greemwich 对应的 Spring Boot 是 2.0.x 版本。

<dependencyManagement>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-dependenciesartifactId>
            <version>Greenwich.RELEASEversion>
            <type>pomtype>
            <scope>importscope>
        dependency>
    dependencies>
dependencyManagement>

2)lagou-cloud-eureka 工程 pom.xml 中引入依赖

<dependencies>
    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
    dependency>
dependencies>

注意:在父工程的 pom 文件中手动引入 jaxb 的 jar,因为 Jdk 9 之后默认没有加载该模块,但是 Eureka Server 使用到,所以需要手动导入,否则 Eureka Server 服务无法启动。

父工程 lagou-parent 的 pom 中增加依赖:


<dependency>
    <groupId>com.sun.xml.bindgroupId>
    <artifactId>jaxb-coreartifactId>
    <version>2.2.11version>
dependency>
<dependency>
    <groupId>javax.xml.bindgroupId>
    <artifactId>jaxb-apiartifactId>
dependency>
<dependency>
    <groupId>com.sun.xml.bindgroupId>
    <artifactId>jaxb-implartifactId>
    <version>2.2.11version>
dependency>
<dependency>
    <groupId>org.glassfish.jaxbgroupId>
    <artifactId>jaxb-runtimeartifactId>
    <version>2.2.10-b140310.1920version>
dependency>
<dependency>
    <groupId>javax.activationgroupId>
    <artifactId>activationartifactId>
    <version>1.1.1version>
dependency>

3)在 application.yml 文件中配置 Eureka server 服务端口,服务名等信息:

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

4)编写启动类,声明当前服务为 Eureka 注册中心

package com.renda.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * @author Renda Zhang
 * @since 2020-11-01 16:36
 */
@SpringBootApplication
@EnableEurekaServer // 表示当前项目为 Eureka Server
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }

}

5)访问 http://localhost:9200/,如果出现 Eureka 注册中心后台页面,则表明 Eureka Server 发布成功。

6)商品微服务和页面静态化微服务注册到 Eureka。

两个微服务的 POM 文件中都添加 Eureka Client 依赖:


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>

两个微服务的 application.yml 都配置 Eureka 服务端信息:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:9200/eureka/
  instance:
    # 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
    prefer-ip-address: true
    # 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

修改两个微服务的启动类,加上注解:

// 将当前项目作为 Eureka Client 注册到 Eureka Server, 只能在 Eureka 环境中使用
@EnableEurekaClient

或者使用可以应用在所有服务注册中心环境的注解:

// 将当前项目表示为注册中心的客户端,向注册中心进行注册,可以在所有的服务注册中心环境下使用
@EnableDiscoveryClient

7)刷新 Eureka 注册中心后台页面,发现新增了两个微服务信息。

搭建 Eureka Server 高可用集群

在互联网应用中,服务实例很少有单个的。

如果 EurekaServer 只有一个实例,该实例挂掉,正好微服务消费者本地缓存列表中的服务实例也不可用,那么这个时候整个系统都受影响。

在生产环境中,会配置 Eureka Server 集群实现高可用。

Eureka Server 集群之中的节点通过点对点(P2P)通信的方式共享服务注册表。

开启两台 Eureka Server 以搭建集群。

修改个人电脑中 host 地址:

127.0.0.1 LagouCloudEurekaServerA
127.0.0.1 LagouCloudEurekaServerB

lagou-cloud-eureka 复制一份为 lagou-cloud-eureka2

lagou-cloud-eureka 的 application.yml:

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

lagou-cloud-eureka2 的 application.yml:

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

商品微服务的 application.xml:

server:
  # 微服务的集群环境中,通常会为每一个微服务叠加。
  port: 9000
spring:
  application:
    name: lagou-service-product
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renda01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: password
eureka:
  client:
    service-url:
      # 把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka, http://LagouCloudEurekaServerB:9201/eureka
  instance:
    # 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
    prefer-ip-address: true
    # 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

页面静态化微服务 application.xml:

server:
  # 后期该微服务多实例,端口从 9100 递增(10 个以内)
  port: 9100
Spring:
  application:
    name: lagou-service-page
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renda01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: password
eureka:
  client:
    service-url:
      # 把 eureka 集群中的所有 url 都填写了进来,也可以只写一台,因为各个 eureka server 可以同步注册表
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka, http://LagouCloudEurekaServerB:9201/eureka
  instance:
    # 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
    prefer-ip-address: true
    # 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

服务消费者调用服务提供者

改造页面静态化微服务:之前是直接通过 RestTemplate 写死 URL 进行调用,现在通过 Eureka 方式进行调用。

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/getProduct/{id}")
    public Products getProduct(@PathVariable Integer id) {
        // 1.获得 Eureka 中注册的 lagou-service-product 实例集合
        List<ServiceInstance> instances = discoveryClient.getInstances("lagou-service-product");
        // 2.获得实例集合中的第一个
        ServiceInstance instance = instances.get(0);
        // 3.根据实例信息拼接 IP 地址
        String host = instance.getHost();
        int port = instance.getPort();
        String url = "http://" + host + ":" + port + "/product/query/" + id;
        // 调用并返回
        return restTemplate.getForObject(url, Products.class);
    }

}

启动注册中心集群和微服务并使用 Postman 进行测试:

GET http://localhost:9100/page/getProduct/1

Eureka 细节详解

Eureka 元数据详解

Eureka 的元数据有两种:标准元数据和自定义元数据。

标准元数据:主机名、IP 地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。

自定义元数据:可以使用 eureka.instance.metadata-map 配置,符合 KEY / VALUE 的存储格式;这些元数据可以在远程客户端中访问。

eureka:
  instance:
    # 使用 ip 注册,否则会使用主机名注册了(此处考虑到对老版本的兼容,新版本经过实验都是 ip)
    prefer-ip-address: true
    # 自定义实例显示格式,加上版本号,便于多版本管理,注意是 ip-address,早期版本是 ipAddress
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
    # 自定义元数据,会和标准元数据一起注册到服务注册中心
    metadata-map:
      ip: 192.168.186.128
      port: 10000
      user: RendaZhang
      pwd: 123456

可以在程序中可以使用 DiscoveryClient 获取指定微服务的所有元数据信息:

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/getProduct/{id}")
    public Products getProduct(@PathVariable Integer id) {
        // 1.获得 Eureka 中注册的 lagou-service-product 实例集合
        List<ServiceInstance> instances = discoveryClient.getInstances("lagou-service-product");
        // 2.获得实例集合中的第一个
        ServiceInstance instance = instances.get(0);
        // 3.根据实例信息拼接 IP 地址
        String host = instance.getHost();
        int port = instance.getPort();
        // 获取打印自定义元数据
        Map<String, String> metadata = instance.getMetadata();
        Set<Map.Entry<String, String>> entries = metadata.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
        String url = "http://" + host + ":" + port + "/product/query/" + id;
        // 调用并返回
        return restTemplate.getForObject(url, Products.class);
    }

}
Eureka 客户端详解

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

服务注册详解(服务提供者):

1)当导入了 eureka-client 依赖坐标,配置 Eureka 服务注册中心地址;

2)服务在启动时会向注册中心发起注册请求,携带服务元数据信息;

3)Eureka 注册中心会把服务的信息保存在 Map 中。

服务续约详解(服务提供者):

服务每隔 30 秒会向注册中心续约 (心跳) 一次(也称为报活),如果没有续约,租约在 90 秒后到期,然后服务会被失效。每隔 30 秒的续约操作称之为心跳检测。

  • Eureka Client - 30s 续约一次,在 Eureka Server 更新自己的状态(Client 端进行配置)。
  • Eureka Server - 90s 还没有进行续约,将该微服务实例从服务注册表(Map)剔除(Client端进行配置)。
  • Eureka Client - 30s 拉取服务最新的注册表并缓存到本地(Client 端进行配置)。
  • 往往不需要调整这两个配置。
# 向 Eureka 服务中心集群注册服务
eureka:
  instance:
    # 租约续约间隔时间,默认 30 秒
    lease-renewal-interval-in-seconds: 30
    # 租约到期,服务时效时间,默认值 90 秒, 服务超过 90 秒没有发生心跳,EurekaServer 会将服务从列表移除
    lease-expiration-duration-in-seconds: 90

获取服务列表(服务注册表)详解(服务消费者):

每隔 30 秒服务会从注册中心中拉取一份服务列表,这个时间可以通过配置修改;往往不需要调整。

# 向 Eureka 服务中心集群注册服务
eureka:
  client:
    service-url:
    # 每隔多久拉取一次服务列表
    registry-fetch-interval-seconds: 30

1)服务消费者启动时,从 Eureka Server 服务列表获取只读备份,缓存到本地。

2)每隔 30 秒,会重新获取并更新数据。

3)每隔 30 秒的时间可以通过配置 eureka.client.registry-fetch-interval-seconds 修改。

Eureka 服务端详解

服务下线:

1)当服务正常关闭操作时,会发送服务下线的 REST 请求给 Eureka Server。

2)服务中心接受到请求后,将该服务置为下线状态

失效剔除:

Eureka Server会定时(间隔值是 eureka.server.eviction-interval-timer-in-ms,默认 60s)进行检查,如果发现实例在在一定时间(此值由客户端设置的 eureka.instance.lease-expiration-duration-in-seconds 定义,默认值为 90s)内没有收到心跳,则会注销此实例。

自我保护机制:

自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使 Eureka 集群更加的健壮、稳定的运行。

自我保护机制的工作机制是:如果在 15 分钟内超过 85% 的客户端节点都没有正常的心跳,那么 Eureka 就认为客户端与注册中心出现了网络故障,Eureka Server 自动进入自我保护机制,此时会出现以下几种情况:

  1. Eureka Server 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。
  2. Eureka Server 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
  3. 当网络稳定时,当前 Eureka Server 新的注册信息会被同步到其它节点中。

默认情况下,如果 Eureka Server 在一定时间内(默认 90 秒)没有接收到某个微服务实例的心跳,Eureka Server 将会移除该实例。但是当网络分区故障发生时,微服务与 Eureka Server 之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。

  • 因此 Eureka Server 可以很好的应对因网络故障导致部分节点失联的情况,而不会像 Zookeeper 那样如果有一半不可用的情况会导致整个集群不可用而变成瘫痪。

  • 经验:建议生产环境打开自我保护机制

在单机测试的时候很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制(默认开启),则服务注册中心维护的服务实例就不是那么准确了,此时通过修改 Eureka Server 的配置文件来关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除(不推荐)。

eureka:
  server:
    # 关闭自我保护模式(默认为 true)
    enable-self-preservation: false

Ribbon 负载均衡

关于负载均衡

负载均衡一般分为服务器端负载均衡和客户端负载均衡。

所谓服务器端负载均衡,比如 Nginx、F5(硬件负载) 这些,请求到达服务器之后由这些负载均衡器根据一定的算法将请求路由到目标服务器处理。

所谓客户端负载均衡,比如 Ribbon,服务消费者客户端会有一个服务器地址列表,调用方在请求前通过一定的负载均衡算法选择一个服务器进行访问,负载均衡算法的执行是在请求客户端进行。

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

Ribbon 高级应用

需求:

复制商品微服务 lagou-service-product,命名为 lagou-service-product2,端口号 9001;在商品微服务中定义接口,返回当前服务实例占用的端口号。

页面静态化微服务通过负载均衡策略调用商品微服务的 controller。

Eureka Server --服务实例列表--> 页面静态化微服务

页面静态化微服务 
--Ribbon--> 负载均衡算法 
----> [商品微服务 9000,商品微服务 9001,商品微服务 9002]

在微服务中使用 Ribbon 不需要额外导入依赖坐标,微服务中引入过 eureka-client 相关依赖,会自动引入 Ribbon 相关依赖坐标。

代码中使用如下,在服务消费者的 RestTemplate 上添加对应注解即可:

@SpringBootApplication
@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();
    }
}

lagou-serivce-product 中创建一个 Controller,定义方法返回当前微服务所使用的容器端口号:

com.renda.product.controller.ServiceInfoController

@RestController
@RequestMapping("/service")
public class ServiceInfoController {

    @Value("${server.port}")
    private String port;

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

}

然后按照 lagou-serivce-product 复制创建 lagou-serivce-product2 微服务;端口改为 9001,除了 spring.applicaton.name 保持为 lagou-service-product,其它地方改为 lagou-service-product2ProductApplication 改为 Product2Application;最后在父工程下手动加入新增的模块。最后如果 IDEA 没有显示新增模块,就删掉父工程的新增模块引入,同步一次后,再把新增模块引入代码加回去并同步。

在页面静态化微服务中调用 lagou-server-product 下的资源路径:http://lagou-server-product/server/query

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/getProduct/{id}")
    public Products getProduct(@PathVariable Integer id) {
        String url = "http://lagou-service-product/product/query/" + id;
        return restTemplate.getForObject(url, Products.class);
    }

    @GetMapping("/loadProductServicePort")
    public String getProductServerPort() {
        return restTemplate.getForObject("http://lagou-service-product/service/port", String.class);
    }

}

Ribbon 负载均衡策略

Ribbon 内置了多种负载均衡策略,内部负责复杂均衡的顶级接口为 com.netflix.loadbalancer.IRule,接口简介:

package com.netflix.loadbalancer;

/**
 * Interface that defines a "Rule" for a LoadBalancer. A Rule can be thought of
 * as a Strategy for loadbalacing. Well known loadbalancing strategies include
 * Round Robin, Response Time based etc.
 * 
 * @author stonse
 * 
 */
public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

IRule 的子实现类:

IRule <--实现-- AbstractLoadBalancerRule

AbstractLoadBalancerRule <--继承-- [RandomRule, RoundRobinRule, ClientConfigEnabledRoundRobinRule, RetryRule]

RoundRobinRule <--继承-- WeightedResponseTimeRule 

ClientConfigEnabledRoundRobinRule 
<--继承-- [PredicateBasedRule, BestAvailableRule]

PredicateBasedRule 
<--继承-- [AvailabilityFilteringRule, ZoneAvoidanceRule]
  • RoundRobinRule - 轮询策略:

默认超过 10 次获取到的 server 都不可用,会返回一个空的 server。

  • RandomRule - 随机策略:

如果随机到的 server 为 null 或者不可用的话,会 while 不停的循环选取。

  • RetryRule - 重试策略:

一定时限内循环重试。默认继承 RoundRobinRule,也支持自定义注入,RetryRule 会在每次选取之后,对选举的 server 进行判断,是否为 null,是否 alive,并且在 500ms 内会不停的选取判断。而 RoundRobinRule 失效的策略是超过 10 次,RandomRule 是没有失效时间的概念,只要 serverList 没都挂。

  • BestAvailableRule - 最小连接数策略:

遍历 serverList,选取出可用的且连接数最小的一个 server。该算法里面有一个 LoadBalancerStats 的成员变量,会存储所有 server 的运行状况和连接数。如果选取到的 server 为 null,那么会调用 RoundRobinRule 重新选取。

  • AvailabilityFilteringRule - 可用过滤策略:

扩展了轮询策略,会先通过默认的轮询选取一个 server,再去判断该 server 是否超时可用,当前连接数是否超限,都成功再返回。

  • ZoneAvoidanceRule - 区域权衡策略(默认策略):

扩展了轮询策略,继承了 2 个过滤器:ZoneAvoidancePredicate 和 AvailabilityPredicate,除了过滤超时和链接数过多的 server,还会过滤掉不符合要求的 zone 区域里面的所有节点, 在一个区域/机房内的服务实例中轮询;先过滤再轮询。

修改负载均衡策略:

# 针对的被调用方微服务名称,不加就是全局生效
lagou-service-product:
  ribbon:
    # 请求连接超时时间
    ConnectTimeout: 2000
    # 请求处理超时时间
    ReadTimeout: 15000
    # 对所有操作都进行重试
    OkToRetryOnAllOperations: true
    ## 根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由 MaxAutoRetries 配置),
    ## 如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由 MaxAutoRetriesNextServer 配置),
    ## 如果依然不行,返回失败信息。
    # 对当前选中实例重试次数,不包括第一次调用
    MaxAutoRetries: 2
    # 切换实例的重试次数
    MaxAutoRetriesNextServer: 2
    # 负载策略调整
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.AvailabilityFilteringRule

Ribbon 核心源码剖析

Ribbon 工作原理:

Actor ----> 拦截器 ----> 负载均衡算法(Ribbon)----> 目标微服务

Ribbon:按照一定算法选取服务实例

SpringCloud 充分利用了 SpringBoot 的自动装配特点。

引入的 Maven 依赖 --> spring-cloud-commons-2.1.0.RELEASE.jar --> META-INF --> spring.factories 配置文件:

# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.client.CommonsClientAutoConfiguration,\
org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration,\
org.springframework.cloud.client.hypermedia.CloudHypermediaAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.AsyncLoadBalancerAutoConfiguration,\
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration,\

...

点击配置文件中的 LoadBalancerAutoConfiguration 跳转到其源码文件:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

    @LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

    ...

	@Configuration
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {
		@Bean
		public LoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final LoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
		}
	}

    ...
}
  • @Configuration 标识当前类为配置类

  • @ConditionalOnClass(RestTemplate.class) 表示只有存在 RestTemplate 类时该配置才会装配生效。

  • @ConditionalOnBean(LoadBalancerClient.class) 表示只有存在 LoadBalancerClient 类时改配置才生效。

  • restTemplates 集合会自动注入添加了 @LoadBalanced 注解的 RestTemplate 对象。

  • LoadBalancerInterceptorConfigrestTemplateCustomizerrestTemplates 集合添加了拦截器;该拦截器就是后续拦截请求进行负载处理的。

Hystrix 熔断器

Hystrix 熔断器属于一种容错机制。

微服务中的雪崩效应

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

服务雪崩效应:是一种因“服务提供者的不可用”导致“服务调用者不可用”,并将不可用逐渐放大的现象。

[MQ 微服务,MQ 微服务,MQ 微服务] ----> 静态化微服务 ----> 商品微服务

站在静态化微服务角度来看:
扇入 - 上游服务对该服务的调用
扇出 - 该服务对下游服务的调用
  • 扇入:代表着该微服务被调用的次数,扇入大,说明该模块复用性好。

  • 扇出:该微服务调用其他微服务的个数,扇出大,说明业务逻辑复杂。

扇入大是一个好事,扇出大不一定是好事。

在微服务架构中,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。

最下游商品微服务响应时间过长,大量请求阻塞,大量线程不会释放,会导致服务器资源耗尽,最终导致上游服务甚至整个系统瘫痪。

形成原因 - 服务雪崩的过程可以分为三个阶段:

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

服务雪崩的每个阶段都可能由不同的原因造成:

服务提供者不可用:硬件故障、程序 Bug、缓存击穿、用户大量请求。

重试加大请求流量:用户重试、代码逻辑重试。

服务调用者不可用:同步等待造成的资源耗尽。

雪崩效应解决方案

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

介绍三种技术手段应对微服务中的雪崩效应,这三种手段都是从系统可用性、可靠性角度出发,尽量防止系统整体缓慢甚至瘫痪。

服务熔断

熔断机制是应对雪崩效应的一种微服务链路保护机制。在各种场景下都会接触到熔断。高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,熔断该节点微服务的调用,进行服务的降级,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。

注意:

1)服务熔断重点在“断”,切断对下游服务的调用。

2)服务熔断和服务降级往往是一起使用的,Hystrix 就是这样。

服务降级

通俗讲就是整体资源不够用了,先将一些不关紧的服务停掉(调用的时候,返回一个预留的值,也叫做兜底数据),待渡过难关高峰过去,再把那些服务打开。

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

服务限流

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

限流措施:

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

Hystrix 简介

Hystrix - defend your application 是由 Netflix 开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。

Hystrix 主要通过以下几点实现延迟和容错:

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

Hystrix 应用

熔断处理

目的:商品微服务长时间没有响应,服务消费者 --> 页面静态化微服务快速失败给用户提示。

引入依赖:服务消费者工程(静态化微服务)中引入Hystrix依赖坐标(也可以添加在父工程中)


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>

开启熔断:服务消费者工程(静态化微服务)的启动类中添加熔断器开启注解 @EnableCircuitBreaker

@SpringBootApplication
@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 的 fallbackMethod 属性关联到服务降级处理方法。

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    RestTemplate restTemplate;

    ...

    /**
     * 模拟服务超时,熔断处理
     * 针对熔断处理,Hystrix 默认维护一个线程池,默认大小为 10。
     */
    @HystrixCommand(
            //只有是在 @HystrixCommand 中定义了 threadPoolKey,就意味着开启了舱壁模式(线程隔离),该方法就会自己维护一个线程池。
            // 默认所有的请求共同维护一个线程池,实际开发:每个方法维护一个线程池
            threadPoolKey = "getProductServerPort2",
            // 每一个属性对应的都是一个 HystrixProperty
            threadPoolProperties = {
                    // 并发线程数
                    @HystrixProperty(name = "coreSize", value = "1"),
                    // 默认线程队列值是 -1,默认不开启
                    @HystrixProperty(name = "maxQueueSize", value = "20")
            },
            // 超时时间的设置
            commandProperties = {
                    // 设置请求的超时时间,一旦请求超过此时间那么都按照超时处理,默认超时时间是 1s
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
            }
    )
    @GetMapping("/loadProductServicePort2")
    public String getProductServerPort2() {
        return restTemplate.getForObject("http://lagou-service-product/service/port", String.class);
    }

}

商品微服务模拟超时操作:

@RestController
@RequestMapping("/service")
public class ServiceInfoController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/port")
    public String getPort(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return port;
    }

}

使用 Postman 测试:

GET http://localhost:9100/page/loadProductServicePort2

测试返回超时错误信息:getProductServerPort2 timed-out and fallback failed.

因为没有降级处理,所以只得到 500 超时错误信息。

降级处理

配置 @HystrixCommand 注解,定义降级处理方法:

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    RestTemplate restTemplate;
    
    ...

    /**
     * 服务降级演示:是在服务熔断之后的兜底操作
     */
    @HystrixCommand(
            // 超时时间的设置
            commandProperties = {
                    // 设置请求的超时时间,一旦请求超过此时间那么都按照超时处理,默认超时时间是 1s
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
                    // 统计窗口时间的设置
                    @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")
            },
            // 设置回退方法
            fallbackMethod = "getProductServerPortFallBack"
    )
    @GetMapping("/loadProductServicePort3")
    public String getProductServerPort3() {
        return restTemplate.getForObject("http://lagou-service-product/service/port", String.class);
    }

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

}

使用 Postman 测试:

GET http://localhost:9100/page/loadProductServicePort3

Hystrix 舱壁模式

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

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

----> 带有 @HystrixCommand 注解方法1 
--10个请求--> Hystrix 默认的线程池
----> A 服务

----> 带有 @HystrixCommand 注解方法2 
--10个请求--> Hystrix 默认的线程池
----> A 服务

所有加有 @HystrixCommand 的方法会共同维护一个线程池;
默认线程池有 10 个线程,默认队列值为 -1;
如果不进行任何设置,
Hystrix 默认的线程池实现不适合在生产环境中使用。

为了避免问题服务请求过多导致正常服务无法访问,Hystrix 不是采用增加线程数,而是单独的为每一个控制方法创建一个线程池的方式,这种模式叫做“舱壁模式",也是线程隔离的手段;即在 @HystrixCommand 中设置 threadPoolProperties 属性

Hystrix 工作流程与高级应用

出现调用错误 ----> 是否达到最小请求数 

没有达到最小请求数 ----> 没有遇到问题
达到最小请求数 ----> 错误数量是否达到阈值

错误数量没有达到阈值 ----> 没有遇到问题
错误数量达到阈值 ----> 跳闸

跳闸 ----> 远程服务是否已经正常

远程服务不正常 ----> 跳闸
远程服务正常 ----> 重置断路器,调用可以通过

10 秒时间窗口:检测是否达到最小请求数和错误数量是否达到阈值。
5 秒活动窗口:检测远程服务是否已经正常。

1)当调用出现问题时,开启一个时间窗(10s)

2)在这个时间窗内,统计调用次数是否达到最小请求数?如果没有达到,则重置统计信息,回到第 1 步;如果达到了,则统计失败的请求数占所有请求数的百分比。然后统计是否达到阈值? 如果达到,则跳闸(不再请求对应服务);如果没有达到,则重置统计信息,回到第 1 步。

3)如果跳闸,则会开启一个活动窗口(默认 5s),每隔 5s,Hystrix 会让一个请求通过,到达那个问题服务,看是否调用成功,如果成功,重置断路器回到第 1 步,如果失败,回到第 3 步。

可以自定义的断路器行为:

  • 出现错误时,时间窗口的长度。
  • 最小请求数。
  • 错误请求的百分比。
  • 跳闸后,活动窗口的长度。
@HystrixCommand(
    // 超时时间的设置
    commandProperties = {
        // 设置请求的超时时间,一旦请求超过此时间那么都按照超时处理,默认超时时间是 1s
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
        // 统计窗口时间的设置
        @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:
  command:
    default:
      circuitBreaker:
        # 强制打开熔断器,如果该属性设置为 true,强制断路器进入打开状态,将会拒绝所有的请求。 默认 false 关闭的
        forceOpen: false
        # 触发熔断错误比例阈值,默认值 50%
        errorThresholdPercentage: 50
        # 熔断后休眠时长,默认值 5 秒
        sleepWindowInMilliseconds: 3000
        # 熔断触发最小请求次数,默认值是 20
        requestVolumeThreshold: 2
      execution:
        isolation:
          thread:
            # 熔断超时设置,默认为 1 秒
            timeoutInMilliseconds: 2000

基于 spring boot 的健康检查观察跳闸状态(自动投递微服务暴露健康检查细节):

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

使用 Postman 访问健康检查接口:

GET http://localhost:9100/actuator/health

Hystrix 线程池队列配置案例

有一次在生产环境,突然出现了很多笔还款单被挂起,后来排查原因,发现是内部系统调用时出现了 Hystrix 调用异常。在开发过程中,因为核心线程数设置的比较大,没有出现这种异常。放到了测试环境,偶尔有出现这种情况。

后来调整 maxQueueSize 属性,确实有所改善。可没想到在生产环境跑了一段时间后却又出现这种了情况,此时去查看 maxQueueSize 属性,可是 maxQueueSize 属性是设置值了。

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

hystrix:
  threadpool:
    default:
      # 并发执行的最大线程数,默认 10;建议和服务器的核数一样
      coreSize: 10
      # BlockingQueue 的最大队列数,默认值 -1
      maxQueueSize: 1000
      # 即使 maxQueueSize 没有达到,达到 queueSizeRejectionThreshold 该值后,请求也会被拒绝,默认值 5
      queueSizeRejectionThreshold: 800

改进后的配置案例 - 将核心线程数调低,最大队列数和队列拒绝阈值的值都设置大一点:

hystrix:
  threadpool:
    default:
      coreSize: 10
      maxQueueSize: 1500
      queueSizeRejectionThreshold: 1000

Feign 远程调用组件

在之前的案例中,服务消费者调用服务提供者的时候使用 RestTemplate 技术。

Feign 简介

Feign 是 Netflix 开发的一个轻量级 RESTful 的 HTTP 服务客户端(用它来发起请求,远程调用的),是以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用,Feign 被广泛应用在 Spring Cloud 的解决方案中。

  • 类似于 Dubbo,服务消费者拿到服务提供者的接口,然后像调用本地接口方法一样去调用,实际发出的是远程的请求。

Feign 更加便捷,优雅的调用 HTTP API:不需要去拼接 url 然后调用 restTemplate 的 api,在 Spring Cloud 中,使用 Feign 非常简单,创建一个接口(在消费者 - 服务调用方这一端),并在接口上添加一些注解,代码就完成了。

Spring Cloud 对 Feign 进行了增强,使 Feign 支持了 Spring MVC 注解 - OpenFeign。

本质:封装了 Http 调用流程,更符合面向接口化的编程习惯,类似于 Dubbo 的服务调用。

Feign 配置应用

在服务调用者工程(消费)创建接口(添加注解)。

效果:Feign = RestTemplate + Ribbon + Hystrix。

1)服务消费者工程(页面静态化微服务)中引入Feign依赖(或者父类工程):


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-openfeignartifactId>
dependency>

2)服务消费者工程(静态化微服务)启动类使用注解 @EnableFeignClients 添加 Feign 支持:

@SpringBootApplication
@EnableDiscoveryClient
// @EnableCircuitBreaker // 启用熔断服务
@EnableFeignClients // 开启 Feign 客户端
public class PageApplication {

    public static void main(String[] args) {
        SpringApplication.run(PageApplication.class,args);
    }

}

注意:此时去掉 Hystrix 熔断的支持注解 @EnableCircuitBreaker,因为 Feign 会自动引入熔断服务。

3)在消费者微服务中创建 Feign 接口:

com.renda.page.feign.ProductFeign

/**
 * 自定义 Fegin 接口,
 * 调用 Product 微服务的所有接口方法都在此进行定义
 *
 * @author Renda Zhang
 * @since 2020-11-02 17:01
 */
@FeignClient(name = "lagou-service-product")
public interface ProductFeign {

    @GetMapping("/product/query/{id}")
    public Products queryById(@PathVariable Integer id);

    @GetMapping("/service/port")
    public String getPort();

}

注意:

1)@FeignClient 注解的 name 属性用于指定要调用的服务提供者名称,和服务提供者 yml 文件中 spring.application.name 保持一致。

2)接口中的接口方法,就好比是远程服务提供者 Controller 中的 Handler 方法(只不过如同本地调用了),那么在进行参数绑定的时,可以使用 @PathVariable@RequestParam@RequestHeader 等,这也是 OpenFeign 对 SpringMVC 注解的支持,但是需要注意 value 必须设置,否则会抛出异常。

  1. @FeignClient(name = "lagou-service-product"),name 在消费者微服务中只能出现一次。升级 Spring Boot 2.1.0,Spring Cloud Greenwich.M1 版本后,在 2 个 Feign 接口类内定义相同的名字,@FeignClient(name = "相同的名字") 就会出现报错,在之前的版本不会提示报错。所以最好将调用一个微服务的信息都定义在一个 Feign 接口中。

改造 PageController 中原有的调用方式:

@RestController
@RequestMapping("/page")
public class PageController {

    @Autowired
    private ProductFeign productFeign;

    @GetMapping("/getProduct/{id}")
    public Products getProduct(@PathVariable Integer id) {
        return productFeign.queryById(id);
    }

    @GetMapping("/loadProductServicePort")
    public String getProductServerPort() {
        return productFeign.getPort();
    }

    ...

}

Feign 对负载均衡的支持

Feign 本身已经集成了 Ribbon 依赖和自动配置,因此不需要额外引入依赖,可以通过 ribbon.xx 来进行全局配置,也可以通过服务名 .ribbon.xx 来对指定服务进行细节配置配置(参考之前 Ribbon 的配置,此处略)。

Feign 默认的请求处理超时时长 1s,有时候业务确实执行的需要一定时间,那么这个时候,就需要调整请求处理超时时长,Feign 自己有超时设置,如果配置 Ribbon 的超时,则会以 Ribbon 的为准。

# 针对的被调用方微服务名称,不加就是全局生效
lagou-service-product:
  ribbon:
    # 请求连接超时时间
    ConnectTimeout: 2000
    # 请求处理超时时间
    ReadTimeout: 15000
    # 对所有操作都进行重试
    OkToRetryOnAllOperations: true
    ## 根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由 MaxAutoRetries 配置),
    ## 如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由 MaxAutoRetriesNextServer 配置),
    ## 如果依然不行,返回失败信息。
    # 对当前选中实例重试次数,不包括第一次调用
    MaxAutoRetries: 2
    # 切换实例的重试次数
    MaxAutoRetriesNextServer: 2
    # 负载策略调整
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.ZoneAvoidanceRule

Feign 对熔断器的支持

1)在 Feign 客户端工程配置文件 application.yml 中开启 Feign 对熔断器的支持:

feign:
  # 开启 Feign 对熔断器支持
  hystrix:
    enabled: true

Feign 的超时时长设置那其实就上面 Ribbon 的超时时长设置。

Hystrix 超时设置就按照之前 Hystrix 设置的方式就 OK 了。

注意:

1)开启 Hystrix 之后,Feign 中的方法都会被进行一个管理了,一旦出现问题就进入对应的回退逻辑处理

2)针对超时这一点,当前有两个超时时间设置(Feign / Hystrix),熔断的时候是根据这两个时间的最小值来进行的,即处理时长超过最短的那个超时时间了就熔断进入回退降级逻辑。

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

2)自定义 FallBack 处理类(需要实现 FeignClient 接口)

com.renda.page.feign.ProductFeignFallBack

@Component
public class ProductFeignFallBack implements ProductFeign {

    @Override
    public Products queryById(Integer id) {
        return null;
    }

    @Override
    public String getPort() {
        return "-1";
    }
}

com.renda.page.feign.ProductFeign

@FeignClient(name = "lagou-service-product", fallback = ProductFeignFallBack.class)
public interface ProductFeign {

    @GetMapping("/product/query/{id}")
    public Products queryById(@PathVariable Integer id);

    @GetMapping("/service/port")
    public String getPort();

}

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

Feign 支持对请求和响应进行 GZIP 压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

feign:
  # 开启 Feign 对熔断器支持
  hystrix:
    enabled: true
  # 开启请求和响应的压缩设置,默认是不开启的
  compression:
    request:
      enabled: true
      # 默认值
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048
    response:
      enabled: true

GateWay 网关组件

网关:微服务架构中的重要组成部分。

局域网中就有网关这个概念,局域网接收或者发送数据出去通过这个网关,比如用 Vmware 虚拟机软件搭建虚拟机集群的时候,往往需要选择 IP 段中的一个 IP 作为网关地址。

Spring Cloud GateWay 只是众多网关解决方案中的一种。

GateWay 简介

Spring Cloud GateWay 是 Spring Cloud 的一个全新项目,目标是取代 Netflix Zuul,它基于 Spring 5.0 + SpringBoot 2.0 + WebFlux(基于高性能的 Reactor 模式响应式通信框架 Netty,异步非阻塞模型)等技术开发,性能高于 Zuul(Zuul 1 是阻塞模型,Zuul 2 是非阻塞模型,但是 Zuul 2 已经停止维护)。官方测试,GateWay 是 Zuul 的 1.6 倍,旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud GateWay 不仅提供统一的路由方式(反向代理)并且基于 Filter(定义过滤器对请求过滤,完成一些功能)链的方式提供了网关基本的功能,例如:鉴权、流量控制 - 流量削峰、熔断、路径重写、日志监控等。

网关在架构中的位置:

客户端群 ----> 负载均衡 ----> 网关集群

客户端群:[浏览器,移动端,IOT,外部接口]

网关集群:[GateWay, GateWay]

网关集群 ----> [微服务A,微服务B,微服务C,微服务D]

GateWay 核心概念

Spring Cloud GateWay 天生就是异步非阻塞的,基于 Reactor 模型(同步非阻塞的 I/O 多路复用机制)。

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

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

GateWay 如何工作

GateWay Client 客户端 
<----> Gateway Handler Mapping
<----> Gateway Web Handler
<----> Filter-Filter-Filter-ProxyFilter
<----> Proxied Service

Spring Cloud Gateway: 
[
  Gateway Handler Mapping, 
  Gateway Web Handler, 
  Filter-Filter-Filter-ProxyFilter
]

Gateway Handler Mapping:
网关控制器映射,
找到与请求相匹配的路由,
将其发送到 Gateway Web Handler。

Gateway Web Handler:
网关 Web 控制器,
通过指定的过滤器将请求发送到实际的服务,
执行业务逻辑,然后返回。

Filter-ProxyFilter:
执行之前 pre,执行之后 post;
pre - 鉴权、参数校验、流量限制、日志记录
post - 响应内容、修改响应头、日志

客户端向 Spring Cloud GateWay 发出请求,然后在 GateWay Handler Mapping 中找到与请求相匹配的路由,将其发送到 GateWay Web Handler;Handler 再通过指定的过滤器链来将请求发送到实际的服务执行业务逻辑,然后返回。过滤器可能会在发送代理请求之前(pre)或者之后(post)执行业务逻辑。

Filter 在 pre 类型过滤器中可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 post 类型的过滤器中可以做响应内容、响应头的修改、日志的输出、流量监控等。

GateWay 应用

使用网关对静态化微服务进行代理(添加在它的上游,相当于隐藏了具体微服务的信息,对外暴露的是网关)。

GateWay 不需要使用 web 模块,它引入的是 WebFlux(类似于 SpringMVC)

创建工程 lagou-cloud-gateway-server。

导入依赖,不需要依赖父工程 lagou-parent,独立一个工程:


<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">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.rendagroupId>
    <artifactId>lagou-cloud-gateway-serverartifactId>
    <version>1.0-SNAPSHOTversion>

    
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.6.RELEASEversion>
    parent>

    <dependencyManagement>
        
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>Greenwich.RELEASEversion>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-commonsartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webfluxartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-loggingartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.4version>
            <scope>providedscope>
        dependency>
        
        <dependency>
            <groupId>com.sun.xml.bindgroupId>
            <artifactId>jaxb-coreartifactId>
            <version>2.2.11version>
        dependency>
        <dependency>
            <groupId>javax.xml.bindgroupId>
            <artifactId>jaxb-apiartifactId>
        dependency>
        <dependency>
            <groupId>com.sun.xml.bindgroupId>
            <artifactId>jaxb-implartifactId>
            <version>2.2.11version>
        dependency>
        <dependency>
            <groupId>org.glassfish.jaxbgroupId>
            <artifactId>jaxb-runtimeartifactId>
            <version>2.2.10-b140310.1920version>
        dependency>
        <dependency>
            <groupId>javax.activationgroupId>
            <artifactId>activationartifactId>
            <version>1.1.1version>
        dependency>
        
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-sleuthartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-zipkinartifactId>
        dependency>
    dependencies>

    <build>
        <plugins>
            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <configuration>
                    <source>11source>
                    <target>11target>
                    <encoding>utf-8encoding>
                configuration>
            plugin>
            
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

注意:不要引入 starter-web 模块,需要引入 web-flux。

application.yml 配置文件内容:

server:
  port: 9300
eureka:
  client:
    serviceUrl: # eureka server的路径
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka,http://LagouCloudEurekaServerB:9201/eureka
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
  application:
    name: lagou-cloud-gateway
  # 网关的配置
  cloud:
    gateway:
      routes: # 配置路由
        - id: service-page-router
          # 动态路由:从注册中心获取对应服务的实例
          # lb - load balance 负载均衡
          uri: lb://lagou-service-page
          # 当断言成功后,交给某一个微服务处理时使用的是转发
          predicates:
            - Path=/page/**
        - id: service-product-router
          uri: lb://lagou-service-product
          predicates:
            - Path=/product/**
          filters:
            # 断言成功后,交给具体的 uri 对应的微服务处理,将 uri 的第一个参数去掉
            - StripPrefix=1

- StripPrefix=1 将 uri 第一段去掉,product/service/port --> service/port

启动类 com.renda.gateway.GateWayServerApplication

@SpringBootApplication
@EnableDiscoveryClient
public class GateWayServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(GateWayServerApplication.class,args);
    }

}

使用 Postman 进行测试:

通过网关访问商品微服务 - GET http://localhost:9300/product/product/query/1

通过网关访问商品微服务实例的端口 - GET http://localhost:9300/product/service/port

通过网关访问页面静态化服务 - GET http://localhost:9300/page/getProduct/1

GateWay 路由规则详解

Spring Cloud GateWay 内置了很多 Predicates 功能,实现了各种路由匹配规则(通过 Header、请求参数等作为条件)匹配到对应的路由。

RoutePredicateFactory 路由断言工厂:
[
DateTime 时间类断言 - 根据请求时间在配置时间之前/之后/之间,
Cookie 类断言 - 指定 Cookie 正则匹配指定值,
Header 请求头类断言 - 指定 Header 正则匹配指定值/请求头中是否包含某个属性,
Host 请求主机类断言 - 请求 Host 匹配指定值,
Method 请求方式类断言 - 请求 Method 匹配指定请求方式,
Path 请求路径类断言 - 请求路径正则匹配指定值,
QueryParam 请求参数类断言 - 查询参数正则匹配指定值,
RemoteAddr 远程地址类断言 - 请求远程地址匹配指定值
]
时间点后匹配
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - After=2017-01-20T17:42:47.789-07:00[America/Denver]
时间点前匹配
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
时间区间匹配
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
指定 Cookie 正则匹配指定值
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Cookie=chocolate, ch.p
指定 Header 正则匹配指定值
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Header=X-Request-Id, \d+
请求 Host 匹配指定值
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Host=**.somehost.org,**.anotherhost.org
请求 Method 匹配指定请求方式
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Method=GET,POST
请求路径正则匹配
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Path=/red/{segment},/blue/{segment}
请求包含某参数
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Query=green
请求包含某参数并且参数值匹配正则表达式
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - Query=red, gree.
远程地址匹配
spring:
  cloud:
    gateway:
      routes:
        - id: after_route
          uri: https://example.org
          predicates:
            - RemoteAddr=192.168.1.1/24

GateWay 动态路由详解

GateWay 支持自动从注册中心中获取服务列表并访问,即所谓的动态路由。

实现步骤如下:

1)pom.xml 中添加注册中心客户端依赖(因为要获取注册中心服务列表,eureka 客户端已经引入)。

2)动态路由配置:

spring:
  application:
    name: lagou-cloud-gateway
  cloud:
    gateway:
      routes:
        - id: service-page-router
          # 动态路由:从注册中心获取对应服务的实例
          # lb - load balance
          uri: lb://lagou-service-page
          predicates:
            - Path=/page/**
        - id: service-product-router
          uri: lb://lagou-service-product
          predicates:
            - Path=/product/**
          filters:
            - StripPrefix=1

注意:动态路由设置时,uri 以 lb:// 开头(lb 代表从注册中心获取服务),后面是需要转发到的服务名称。

GateWay 过滤器

GateWay 过滤器简介

从过滤器生命周期(影响时机点)的角度来说,主要有两个 pre 和 post:

  • pre - 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择 请求的微服务、记录调试信息等。
  • post - 这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

从过滤器类型的角度,Spring Cloud GateWay 的过滤器分为 GateWayFilter 和 GlobalFilter 两种:

  • GateWayFilter - 应用到单个路由路由上。
  • GlobalFilter - 应用到所有的路由上。

Gateway Filter 可以去掉 url 中的占位后转发路由,比如:

predicates:
  - Path=/product/**
filters:
  - StripPrefix=1

注意:GlobalFilter 全局过滤器是使用比较多的过滤器。

自定义全局过滤器实现IP访问限制(黑白名单)

请求过来时,判断发送请求的客户端的ip,如果在黑名单中,拒绝访问。

自定义 GateWay 全局过滤器时,实现 Global Filter 接口即可,通过全局过滤器可以实现黑白名单、限流等功能。

package com.renda.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * 通常情况下进行网关自定义过滤器时,
 * 需要实现两个接口:
 * GlobalFilter,
 * Ordered (指定过滤器的执行顺序)
 *
 * @author Renda Zhang
 * @since 2020-11-02 22:23
 */
@Slf4j // lombok 自动生成 logger
@Component // 让容器扫描到,等同于注册了
public class BlackListFilter implements GlobalFilter, Ordered {

    /**
     * 加载黑名单列表
     * MySQL ->  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");
    }

    /**
     * GlobalFilter 过滤器的核心逻辑:
     * 获取客户端 ip,判断是否在黑名单中,在的话就拒绝访问,不在的话就放行
     *
     * @param exchange 封装了 request 和 response 上下文
     * @param chain    网关过滤器链
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求和响应对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 获取来访者的 IP 地址
        String clientIP = request.getRemoteAddress().getHostString();
        // 判断是否在黑名单中
        if (blackList.contains(clientIP)) {
            // 如果是黑名单拒绝访问,设置状态码为没有授权
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            log.info("=====>IP:" + clientIP + " 在黑名单中,将被拒绝访问!");
            String data = "request has been denied";
            DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());
            return response.writeWith(Mono.just(wrap));
        }
        // 合法请求,放行,执行后续的过滤器
        return chain.filter(exchange);
    }

    /**
     * Ordered, 定义过滤的顺序,
     * getOrder()返回值的大小决定了过滤器执行的优先级,
     * 越小优先级越高
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

以上代码使用了 Lombok 的 @Slf4j 来自动生成类的 logger,然后可以直接使用 log.info(...) 来把类的运行信息输出到控制台。

GateWay 高可用

网关作为非常核心的一个部件,如果挂掉,那么所有请求都可能无法路由处理,因此需要做 GateWay 的高可用。

GateWay 的高可用很简单:可以启动多个 GateWay 实例来实现高可用,在 GateWay 的上游使用 Nginx 等负载均衡设备进行负载转发以达到高可用的目的。

启动多个 GateWay 实例(假如说两个,一个端口 9002,一个端口 9003),剩下的就是使用 Nginx 等完成负载代理即可。示例如下:

# 配置多个 GateWay 实例
upstream gateway {
    server 127.0.0.1:9002;
    server 127.0.0.1:9003; 
} 
location / {
    proxy_pass http://gateway; 
}

Spring Cloud Config 分布式配置中心

分布式配置中心应用场景

往往使用配置文件管理一些配置信息,比如 application.yml

单体应用架构,配置信息的管理、维护并不会显得特别麻烦,手动操作就可以,因为就一个工程。

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

场景总结如下:

1)集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的(一次修改、到处生效)。

2)不同环境不同配置,比如数据源配置在不同环境(开发 dev,测试 test,生产 prod)中是不同的。

3)运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等配置修改后可自动更新

4)如配置内容发生变化,微服务可以自动更新配置。

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

Spring Cloud Config

Config 简介

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

[
微服务 A + config client,
微服务 B + config client,
微服务 C + config client,
]
-----> config server ----> Git/SVN

Server 端:提供配置文件的存储、以接口的形式将配置文件的内容提供出去,通过使用 @EnableConfigServer 注解在 Spring Boot 应用中非常简单的嵌入。

Client 端:通过接口获取配置数据并初始化自己的应用。

Config 分布式配置应用

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

比如,要对静态化微服务或者商品微服务的 application.yml 进行管理(区分开发环境 dev、测试环境 test、生产环境 prod)。

1)登录 GitHub 或者 Gitee,创建项目 lagou-config。

2)上传 yml 配置文件,命名规则:{application}-{profile}.yml 或者 {application}-{profile}.properties。 其中,application 为应用名称,profile 指的是环境(用于区分开发环境,测试环境、生产环境等)。示例:lagou-service-page-dev.ymllagou-service-page-test.ymllagou-service-page-prod.yml

application-dev.yml

mysql:
  user: root
person:
  name: renda

3)构建 Config Server 统一配置中心:

新建 SpringBoot 工程,引入依赖坐标(需要注册自己到 Eureka)。


<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>lagou-parentartifactId>
        <groupId>com.rendagroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>lagou-cloud-configartifactId>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-config-serverartifactId>
        dependency>
    dependencies>

project>

配置启动类,使用注解 @EnableConfigServer 开启配置中心服务器功能。

package com.renda.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;

/**
 * @author Renda Zhang
 * @since 2020-11-02 23:03
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer // 开启配置服务器功能
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }

}

application.yml 配置。

server:
  port: 9400
# 注册到 Eureka 服务中心
eureka:
  client:
    service-url:
      defaultZone: http://LagouCloudEurekaServerA:9200/eureka, http://LagouCloudEurekaServerB:9201/eureka
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@
spring:
  application:
    name: lagou-service-config
  cloud:
    config:
      server:
        # git 配置:uri、用户名、密码、分支....
        git:
          # 配置 git 地址
          uri: https://YOUR_GIT_ADDRESS/lagou-config.git
          username: USERNAME
          password: PASSWORD
          search-paths:
            - lagou-config
      label: master

# springboot 中暴露所有的端口
management:
  endpoints:
    web:
      exposure:
        include: "*"

使用 Postman 进行测试:

GET http://127.0.0.1:9400/master/application-dev.yml

4)构建 Client 客户端(在已有页面静态化微服务基础上)

在 lagou-service-page 微服务中动态获取 config server 的配置信息。

已有工程中添加依赖坐标:

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-config-clientartifactId>
dependency>

application.yml 修改为 bootstrap.yml 配置文件。

bootstrap.yml 是系统级别的,优先级比 application.yml 高,应用启动时会检查这个配置文件,在这个配置文件中指定配置中心的服务地址,会自动拉取所有应用配置并且启用。

主要是把与统一配置中心连接的配置信息放到 bootstrap.yml

注意:需要统一读取的配置信息,从配置中心获取。

bootstrap.yml(部分):

server:
  port: 9100
Spring:
  application:
    name: lagou-service-page
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renda01?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: password
  cloud:
    # config 客户端配置,和 ConfigServer 通信,并告知 ConfigServer 希望获取的配置信息在哪个文件中
    config:
      # application-dev.yml
      name: application
      # 后缀名称
      profile: dev
      # 分支名称
      label: master
      # ConfigServer 配置中心地址
      uri: http://localhost:9400

...

com.renda.page.controller.ConfigClientController

@RestController
@RequestMapping("/config")
public class ConfigClientController {

    @Value("${mysql.user}")
    private String user;

    @Value("${person.name}")
    private String name;

    @RequestMapping("/query")
    public String getRemoteConfig() {
        return user + ", " + name;
    }

}

使用 Postman 测试:

GET http://127.0.0.1:9100/config/query

Config 配置手动刷新

不用重启微服务,只需要手动的做一些其他的操作(访问一个地址 /refresh)刷新,之后再访问即可。

此时,客户端取到了配置中心的值,但当我们修改 GitHub 上面的值时,服务端(Config Server)能实时获取最新的值,但客户端(Config Client)读的是缓存,无法实时获取最新值。Spring Cloud 已经解决了这个问题,那就是客户端使用 post 去触发 refresh,获取最新数据。

1)Client 客户端添加依赖 springboot-starter-actuator(已添加)。

2)Client 客户端 bootstrap.yml 中添加配置(暴露通信端点)。

management:
  endpoints:
    web:
      exposure:
        include: refresh

# 也可以暴露所有的端口
management:
  endpoints:
    web:
      exposure:
        include: "*"

3)Client 客户端使用到配置信息的类上添加 @RefreshScope

@RestController
@RequestMapping("/config")
@RefreshScope // 手动刷新
public class ConfigClientController {

    @Value("${mysql.user}")
    private String user;

    @Value("${person.name}")
    private String name;

    @RequestMapping("/query")
    public String getRemoteConfig() {
        return user + ", " + name;
    }

}

4)手动向 Client 客户端发起 POST 请求,http://localhost:9100/actuator/refresh,刷新配置信息。

响应的信息:

[
    "config.client.version",
    "person.name",
    "mysql.user"
]

注意:手动刷新方式避免了服务重启。

思考:可否使用广播机制,一次通知,处处生效,方便大范围配置自动刷新。

Config 配置自动更新

实现一次通知,处处生效。

在微服务架构中,可以结合消息总线 Bus 实现分布式配置的自动更新 Spring Cloud Config + Spring Cloud Bus。

消息总线 Bus

所谓消息总线 Bus,即经常会使用 MQ 消息代理构建一个共用的 Topic,通过这个 Topic 连接各个微服务实例,MQ 广播的消息会被所有在注册中心的微服务实例监听和消费。换言之就是通过一个主题连接各个微服务,打通脉络。

Spring Cloud Bus(基于 MQ 的,支持 RabbitMq / Kafka) 是 Spring Cloud 中的消息总线方案,Spring Cloud Config + Spring Cloud Bus 结合可以实现配置信息的自动更新。

---bus-refresh---> config server 
----> RabbitMQ bus 
---AcceptMsg---> [微服务A, 微服务B]

[微服务A, 微服务B] ---RequestMsg---> config server
Spring Cloud Config + Spring Cloud Bus 实现自动刷新

MQ 消息代理,选择使用 RabbitMQ,ConfigServer 和 ConfigClient 都添加都消息总线的支持以及与 RabbitMQ 的连接信息。

1)Config Server 服务端 lagou-cloud-config 和客户端 lagou-service-page 都添加消息总线支持。

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>

2)Config Server 和客户端都添加配置

Spring:
  rabbitmq:
    host: 192.168.186.128
    port: 5672
    username: renda
    password: 123456

3)Config Server 微服务暴露端口

management:
  endpoints:
    web:
      exposure:
        include: bus-refresh

# 也可以暴露所有的端口
management:
  endpoints:
    web:
      exposure:
        include: "*"

4)重启各个服务,更改配置之后,向配置中心服务端发送 post 请求,各个客户端配置即可自动刷新。

使用 Postman 发起请求:POST http://127.0.0.1:9400/actuator/bus-refresh

5)Config Client 测试

使用 Postman 测试:GET http://localhost:9100/config/query

如此便在在广播模式下实现了一次请求,处处更新;

如果只想定向更新,在发起刷新请求的时候为最后面跟上要定向刷新的实例的服务名和端口号即可:POST http://localhost:9400/actuator/bus-refresh/lagou-service-page:9100

想了解更多,欢迎关注我的微信公众号:Renda_Zhang

你可能感兴趣的:(软件开发)