Featured image of post SpringCloudAlibaba相关框架

SpringCloudAlibaba相关框架

SpringCloudAlibaba相关框架,如:Nacos、OpenFeign、GateWay、Sentinel等等

SpringCloudAlibaba相关框架,如:Nacos、OpenFeign、GateWay、Sentinel等等

学习目标

  • 掌握RestTemplate的使用
  • 知道什么是SpringCloud
  • 掌握搭建Eureka注册中心
  • 了解Ribbon的负载均衡
  • 理解Hystrix的熔断原理

系统架构演变

​ 随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此也不断的演进、升级、迭代。从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构,还有Google带领下来势汹涌的Service Mesh。我们到底是该乘坐微服务的船只驶向远方,还是偏安一隅得过且过? ​ 其实生活不止眼前的苟且,还有诗和远方。所以我们今天就回顾历史,看一看系统架构演变的历程;把握现在,学习现在最火的技术架构;展望未来,争取成为一名优秀的Java工程师。

1.1 单体架构

​ 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。单体架构就是将所有的功能、数据库、文件都部署在一台机器上,俗称All-In-One

1682643185881

1.2 单体集群

单体集群,就是将单体架构复制几份

1682643241371

1.3 分布式服务

分布式,是指将整个系统按照功能拆分,部署到不同的服务器上,这样即适合为某个功能做集群,又合适重复功能的复用

1682643296902

1.4 服务治理架构(SOA)

SOA,全称Service-Oriented Architecture,面向服务的架构. 它把项目拆成独立的服务,对外提供.服务的提供方和消费方由企业总线统一管理

1682643384098

以前出现了什么问题?

  • 服务越来越多,需要管理每个服务的地址
  • 调用关系错综复杂,难以理清依赖关系
  • 服务过多,服务状态难以管理,无法根据服务情况动态管理

服务治理要做什么?

  • 服务注册中心,实现服务自动注册和发现,无需人为记录服务地址
  • 服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系
  • 动态监控服务状态监控报告,人为控制服务状态

缺点:

  • 服务间会有依赖关系,一旦某个环节出错会影响较大
  • 服务关系复杂,运维、测试部署困难

1.5 微服务

微服务架构,是SOA的升华,在SOA的基础上更加细粒度的服务化,组件化.

1682643473026

因此两者非常容易混淆,但其实有一些差别: 微服务的特点:

  • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责
  • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。
  • 面向服务:面向服务是说每个服务都要对外暴露Rest风格服务HTTP接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的HTTP接口即可。
  • 自治:自治是说服务间互相独立,互不干扰
    • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
    • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
    • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动段开发不同接口
    • 数据库分离:每个服务都使用自己的数据源
    • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护

服务调用方式

2.1 RPC和HTTP

无论是微服务还是SOA,都面临着服务间的远程调用。那么服务间的远程调用方式有哪些呢? 常见的远程调用方式有以下2种:

  • RPC: Remote Produce Call远程过程调用,类似的还有RMI(remote method invoke)。自定义数据格式,基于原生TCP通信,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表。
  • Http: http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。 现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的Rest风格,就可以通过http协议来实现。

如果你们公司全部采用Java技术栈,那么使用Dubbo作为微服务架构是一个不错的选择。 相反,如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。在我们的项目中,我们会选择SpringCloud套件,因此我们会使用Http方式来实现服务间调用。

2.2 Http客户端工具

既然微服务选择了Http,那么我们就需要考虑自己来实现对请求和响应的处理。不过开源世界已经有很 多的http客户端工具,能够帮助我们做这些事情,例如:

  • HttpClient
  • OKHttp
  • URLConnection

RestTemplate

RestTemplate是Spring Boot封装好的Http客户端工具。

image-20200523091717293

步骤:

1. 添加服务提供者,提供Rest HTTP接口
2. 添加服务调用者,通过RestTemplate访问服务提供者的接口
3. 服务提供者和调用者都放到一个父工程中

3.1 搭建父工程

为了方便项目管理,这里把服务提供者和服务调用者放到同一个工程中

新建父工程srping-cloud-parent-demo

image-20200731101824059

父工程不需要写代码,可以将src目录删除

添加Spring Boot依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.8.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>	

3.2 搭建服务提供者

项目结构如下:

image-20200903093502816

开发步骤:

  1. 添加子模块user-service,添加web依赖

    image-20200731102045743

    image-20200903091209721

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
  2. 添加配置文件application.yml

    server:
      port: 8001
    spring:
      application:
        # 当前应用的服务名称(也叫服务ID)
        name: user-service
    
  3. 添加启动类

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class UserApplication {
        public static void main(String[] args) {
            SpringApplication.run(UserApplication.class, args);
        }
    }
    
  4. 添加实体

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.util.Date;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private Date updateTime;
    }
    
  5. 编写Controller

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        /**
         * 根据用户ID获取用户
         * 接口地址: http://localhost:8001/user/1
         * @param id
         * @return
         */
        @GetMapping("/{id}")
        public User getById(@PathVariable("id") Long id){
            User user = new User(id,"tom",18,new Date());
            return user;
        }
    }
    
  6. 启动项目,打开http://localhost:8001/user/1

    image-20200910191933635

3.3 搭建服务消费者

  1. 添加子模块service-consumer,添加web依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
  2. 添加配置文件application.yml

    server:
      port: 9001
    spring:
      application:
        # 当前应用的服务名称(也叫服务ID)
        name: service-consumer
    
  3. 添加启动类

    @SpringBootApplication
    public class ConsumerApplication {
        public static void main(String[] args) {
            SpringApplication.run(ConsumerApplication.class, args);
        }
    
        @Bean   // 将RestTemplate注册成为一个Bean
        public RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
  4. 添加实体

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private Date updateTime;
    }
    
  5. 编写Controller

    @RestController
    @RequestMapping("/consumer")
    public class ConsumerController {
    
        @Autowired  // 注入RestTemplate模板工具
        private RestTemplate restTemplate;
    
        @GetMapping("/user/{id}")
        public User getUserById(@PathVariable("id") Long id) {
            // 通过远程调用user-service提供的HTTP接口
            String url = "http://110.111.11.12:8001/user/" + id;
            // 第一个参数是接口调用地址  第二个参数是返回的类型
            User user = restTemplate.getForObject(url, User.class);
            return user;
        }
    }
    
  6. 访问http://localhost:9001/consumer/user/1

    image-20200910192010174

3.4 微服务调用问题

service-provider:对外提供接口服务

service-consumer: 通过RestTemplate访问 http://locahost:8081/ 调用接口服务

存在问题:

  • 在consumer中,我们把url地址硬编码到了代码中,不方便后期维护
  • consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效
  • consumer不清楚user-service的状态,服务宕机也不知道
  • user-service只有1台服务,不具备高可用性
  • 即便user-service形成集群, consumer还需自己实现负载均衡

其实上面说的问题,概括一下就是分布式服务必然要面临的问题:

  • 服务管理
    • 如何自动注册和发现服务
    • 如何实现服务状态监管
    • 如何实现服务动态路由
  • 服务如何实现负载均衡
  • 服务如何解决容灾问题
  • 服务如何实现统一配置

以上的问题,我们都将在Spring Cloud中得到解决。

Spring Cloud

4.1 简介

Spring Cloud是一个基于 Spring Boot实现的微服务架构开发工具。它为微服务架构中涉及的配置管理、服务治理、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。 Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产品,还可能会新增),如下所述。

  • Spring Cloud Config:配置管理工具,支持使用Git存储配置内容,可以使用它实现应用配置的外部化存储,并支持客户端配置信息刷新、加密/解密配置内容等。
  • Spring Cloud Netflix:核心组件,对多个 Netflix OSS开源套件进行整合
    • Eureka:服务治理组件,包含服务注册中心、服务注册与发现机制的实现。
    • Ribbon:客户端负载均衡的服务调用组件。
    • Hystrix(豪猪哥):容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。
    • Open Feign:基于 Ribbon和 Hystrix的声明式服务调用组件。
    • Gateway: 网关组件,提供智能路由、访问过滤等功能。
  • Spring Cloud Bus: 事件、消息总线,用于传播集群中的状态变化或事件,以触发后续的处理,比如用来动态刷新配置等。

官网:https://spring.io/projects/spring-cloud

4.2 版本说明

Spring Cloud不像 Spring社区其他一些项目那样相对独立,它是一个拥有诸多子项目的大型综合项目,可以说是对微服务架构解决方案的综合套件组合,其包含的各个子项目也都独立进行着内容更新与迭代,各自都维护着自己的发布版本号。因此每一个Spring Cloud的版本都会包含多个不同版本的子项目b j\ ,为了管理每个版本的子项目清单,避免 Spring Cloud的版本号与其子项目的版本号相混淆,没有采用版本号的方式,而是通过命名的方式。

这些版本的名字采用了伦敦地铁站的名字,根据字母表的顺序来对应版本时间顺序,比如最早的 Release版本为 Angel,第二个 Release版本为 Brixton

当一个版本的 Spring Cloud项目的发布内容积累到临界点或者一个严重bug解决可用后,就会发布一个“service releases”版本,简称SRX版本,其中X是一个递增的数字,所以 Brixton.SR5就是 Brixton的第5个 Release版本

Spring Cloud Version Spring Boot Version
2022.0.x aka Kilburn 3.0.x
2021.0.x aka Jubilee 2.6.x, 2.7.x (Starting with 2021.0.3)
2020.0.x aka Ilford 2.4.x, 2.5.x (Starting with 2020.0.3)
Hoxton 2.2.x
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x

服务治理

5.1 认识Eureka

首先我们来解决第一问题,服务的管理

问题分析

在刚才的案例中, user-service对外提供服务,需要对外暴露自己的地址。而consumer(调用者)需要 记录服务提供者的地址。将来地址出现变更,还需要及时更新。这在服务较少的时候并不觉得有什么, 但是在现在日益复杂的互联网环境,一个项目肯定会拆分出十几,甚至数十个微服务。此时如果还人为 管理地址,不仅开发困难,将来测试、发布上线都会非常麻烦。

解决方案-网约车

这就好比是 网约车出现以前,人们出门叫车只能叫出租车。一些私家车想做出租却没有资格,被称为黑车。而很多人想要约车,但是无奈出租车太少,不方便。私家车很多却不敢拦,而且满大街的车,谁知 道哪个才是愿意载人的。一个想要,一个愿意给,就是缺少引子,缺乏管理啊。 此时滴滴这样的网约车平台出现了,所有想载客的私家车全部到滴滴注册,记录你的车型(服务类 型),身份信息(联系方式)。这样提供服务的私家车,在滴滴那里都能找到,一目了然。 此时要叫车的人,只需要打开APP,输入你的目的地,选择车型(服务类型),滴滴自动安排一个符合 需求的车到你面前,为你服务,完美!

Eureka能做什么?

Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己 的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。 同时,服务提供方与Eureka之间通过 “心跳” 机制进行监控,当某个服务提供方出现问题, Eureka自然 会把它从服务列表中剔除。这就实现了服务的自动注册、发现、状态监控。

5.2 基本架构

架构图:

image-20200523223318698

基本概念

  • Eureka-Server:就是服务注册中心(可以是一个集群),对外暴露自己的地址。
  • 提供者:启动后向Eureka注册自己信息(地址,服务名称等),并且定期进行服务续约
  • 消费者:服务调用方,会定期去Eureka拉取服务列表,然后使用负载均衡算法选出一个服务进行调用。
  • 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态

5.3 快速入门

步骤:

0. 在父工程中添加SpringCloud的版本依赖管理
1. 搭建注册中心
	a. 新建工程,添加Eureka server依赖
	b. 配置注册中心服务地址
	c. 添加启动类
2. 服务提供者到注册中心注册
	a. 添加Eureka client 依赖
	b. 配置文件中添加注册中心地址
	c. 在启动类上添加服务发现注解
	d. 编写自己需要提供的接口
3. 服务调用者到注册中心拉取注册信息
	a. 添加Eureka client 依赖
	b. 配置文件中添加注册中心地址
	c. 在启动类上添加服务发现注解
	d. 采用服务的方式调用服务提供者的接口

5.3.1 搭建服务注册中心

  1. 在父工程pom.xml中添加Spring Cloud的依赖管理
<properties>
    <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2.在spring-cloud-parent-demo父项目中创建一个Spring Boot工程,命名为eureka-server,并在pom.xml中添加eureka依赖

1682857622828

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.添加配置文件application.yml

server:
  port: 1111
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:${server.port}/eureka
    # 注册中心的职责是维护服务实例,不需要去检索服务
    fetch-registry: false
    # 默认设置下,注册中心会将自己作为客户端来尝试注册自己,设置为false代表不向注册中心注册自己
    register-with-eureka: false

4.添加启动类

@SpringBootApplication
// 启动注册中心
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class,args);
    }
}

启动应用并访问:http://localhost:1111 可以看到Eureka界面

image-20200523225115951

5.3.2 服务注册

注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。

1.在user-service中添加Eureka客户端依赖:

<!-- Eureka客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2.在启动类上 通过添加 @EnableDiscoveryClient来开启Eureka客户端功能

@SpringBootApplication
// 开启Eureka客户端发现功能
@EnableEurekaClient
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}

3.配置文件中添加注册中心地址

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:1111/eureka

这里spring.application.name属性指定应用名称,将来会作为服务的id使用。

此时再打开注册中心的地址:http://localhost:1111,可以看到服务已经注册上去了

image-20200623114627269

5.3.3 服务发现与消费

接下来我们修改service-consumer,尝试从EurekaServer获取服务。 方法与消费者类似,只需要在项目中添加EurekaClient依赖,就可以通过服务名称来获取信息了

1.在service-consumer中添加Eureka客户端依赖:

<!-- Eureka客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2.在启动类上 通过添加 @EnableDiscoveryClient来开启Eureka客户端功能

@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

3.配置文件中添加注册中心地址

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:1111/eureka

4.修改代码,用DiscoveryClient类的方法,根据服务名称,获取服务实例

/*
    动态获取服务地址:
    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    DiscoveryClient的导包:
    import org.springframework.cloud.client.discovery.DiscoveryClient;
    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    注意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    1. 注入DiscoveryClient
    2. 使用DiscoveryClient 获取服务列表
    3. 从服务列表中选择一个服务实例
    4. 实例中包含了调用地址及端口信息
 */
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/user/{id}")
public User getUserById(@PathVariable("id") Long id) {
    // 使用DiscoveryClient 获取服务列表 参数为服务ID ,也就是在配置文件中定义的spring.application.name
    // spring.application.name 建议使用 -  来分隔,不要使用下划线
    List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
    // 获取具体的实例
    ServiceInstance instance = instances.get(0);
    // 获取服务的地址和端口
    String host = instance.getHost();
    int port = instance.getPort();
    // 通过远程调用user-service提供的HTTP接口
    String url = "http://" + host + ":" + port + "/user/" + id;
    // 第一个参数是接口调用地址  第二个参数是返回的类型
    User user = restTemplate.getForObject(url, User.class);
    return user;
}

5.启动项目后,可以在注册中心页面看到有两个服务

image-20200910193602866

6.调用消费者的页面http://localhost:9001/consumer/user/1

image-20200910193620800

5.4 Eureka详解

接下来我们详细讲解Eureka的原理及配置。

5.4.1 基础架构

Eureka架构中的三个核心角色:

  • 服务注册中心 Eureka的服务端应用,提供服务注册发现功能,就是刚刚我们建立的eureka-server
  • 服务提供者 提供服务的应用,可以是Spring Boot应用,也可以是其它任意技术实现,只要对外提供的是Rest 风格服务即可。本例中就是我们实现的order-service-provider
  • 服务消费者 消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中 就是我们实现的pay-service-consumer

image-20200708140407596

服务提供者

服务提供者要向Eureka Server注册服务,并且完成服务续约等工作。

服务注册

服务提供者在启动时,会检测配置属性中的:

eureka.client.register-with-erueka=true

参数表示是否注册到eureka注册中心 事实上默认就是true。如果值确实为true,则会向Eureka Server发起一个Rest请求,并携带自己的元数据信息, Eureka Server会把这些信息保存到一个双层Map结构中。

Map<String,Map<String,Object>>
Map<"user-sercice",Map<主机名:服务名:端口等,服务实例对象>>
  • 第一层Map的Key就是服务id,一般是配置中的 spring.application.name属性
  • 第二层Map的key是服务的实例,一般host+ serviceId + port,例如: localhost:user-service:8001
  • 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。

服务消费者

获取服务

启动服务消费者的时候,它会发送一个REST请求给服务注册中心,来获取上面注册的服务清单。

5.4.3 Eureka属性配置(了解)

服务端配置

失效剔除

有些时候,我们的服务实例并不一定会正常下线,正常下线会给注册中心发送一个HTTP请求,告诉注册中心服务下线,可能由于内存溢出、网络故障等原因使得服务不能正常工作,而服务注册中心并未收到“服务下线”的请求。为了从服务列表中将这些无法提供服务的实例剔除, Eureka Server在启动的时候会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除出去。

# 剔除任务执行时间,默认为60秒
eureka:
  server:
    eviction-interval-timer-in-ms: 60000

服务端的默认配置可以在EurekaServerConfigBean中查看

服务提供者正常关闭会立刻在注册中心下线

image-20200910194505294

接下来模拟服务提供者非正常关闭:

image-20200910194523927

此时发现user-service还在注册中心。

自我保护

当我们在本地调试基于 Eureka的程序时,基本上都会碰到这样一个问题,在服务注册中心的信息面板中出现类似下面的红色警告信息:

image-20200524151524851

该警告就是触发了Eureka Server的自我保护机制。之前我们介绍过,服务注册到 Eureka Server之后,会维护一个心跳连接,告诉 Eureka Server自己还活着.Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致), Eureka Server会将当前的实例注册信息保护起来让这些实例不会过期,尽可能保护这些注册信息。但是,在这段保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败的情况,所以客户端必须要有容错机制,比如可以使用请求重试、断路器等机制。 由于本地调试很容易触发注册中心的保护机制,这会使得注册中心维护的服务实例不那么准确。所以,我们在本地进行开发的时候,可以使用 eureka.server.enable-self-preservation=false参数来关闭保护机制,以确保注册中心可以将不可用的实例正确剔除。

关闭自我保护机制:

eureka:
  client:
    # 注册中心提供服务的地址
    service-url:
      defaultZone: http://127.0.0.1:${server.port}/eureka
    # 注册中心的职责是维护服务实例,不需要去检索服务
    fetch-registry: false
    # 默认设置下,注册中心会将自己作为客户端来尝试注册自己,设置为false代表不向注册中心注册自己
    register-with-eureka: false
  server:
    # 关闭自我保护机制
    enable-self-preservation: false

启动后会显示自我保护机制关闭

image-20200708143545361

客户端属性配置

服务续约

在注册完服务之后,服务提供者会维护一个心跳用来持续告诉 Eureka Server:“我还活着”,以防止 Eureka Server的剔除任务将该服务实例从服务列表中排除出去,我们称该操作为服务续约(Renew) 关于服务续约有两个重要属性,我们可以关注并根据需要来进行调整:

eureka:
  instance:
    # 服务失效的时间,默认为90秒,告知服务器器 如果90秒内都没有心跳包,可以把我踢出
    lease-expiration-duration-in-seconds: 90
    # 服务续约任务的调用间隔时间,默认为30秒,心跳包
    lease-renewal-interval-in-seconds: 30

这个设置与注册中心服务端的失效剔除配合使用

比如Eureka-server中设置

#  每5秒钟剔除失效的服务
eureka:
  server:
    eviction-interval-timer-in-ms: 5000

在user-service中设置

eureka:
  # 当前实例的配置
  instance:
    # 服务失效的时间,默认为90秒
    lease-expiration-duration-in-seconds: 15
    # 服务续约任务的调用间隔时间,默认为30秒
    lease-renewal-interval-in-seconds: 10

意思是服务提供端每10秒发送一次服务续约,服务的失效时间为15秒,即如果服务提供端下线,15秒后eureka server将其服务剔除。

服务拉取

启动服务消费者的时候,它会发送一个REST请求给服务注册中心,来获取上面注册的服务清单。为了性能考虑, Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次。 获取服务是服务消费者的基础,所以必须确保 eureka.client.fetch-registry=true参数没有被修改成 false,该值默认为true。

若希望修改缓存清单的更新时间,可以通过 eureka.client.registry-fetch-interval=30参数进行修改,该参数默认值为30,单位为秒。

Eureka实例配置

user-service默认注册时使用的是主机名或者域名,如果我们想用ip进行注册,可以在user-service的 application.yml添加配置:

eureka:
  instance:
    # 更倾向于使用ip,而不是host名
    prefer-ip-address: true

如果么有使用ip,可以看到提示如下:

image-20200708144930721

改成ip后:

image-20200708145052817

5.5 搭建高可用的Eureka Server集群

Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个Eureka Server,事实上 Eureka Server也可以是一个集群,形成高可用的Eureka中心。

5.5.1 服务同步

多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点 时,该节点会把服务的信息同步给集群中的每个节点,从而实现高可用集群。因此,无论客户端访问到 Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。 而作为客户端,需要把信息注册到每个Eureka中:

image-20200731141015665

如果有三个Eureka,则每一个Eureka Server都需要注册到其它几个Eureka服务中,例如:有三个分别 为1111、 1112、 1113,则:

  • 1111要注册到1112和1113上
  • 1112要注册到1111和1113上
  • 1113要注册到1111和1112上

5.5.2 搭建高可用的Eureka Server

​ 所谓的高可用注册中心,其实就是把Eureka Server自己也作为一个服务,注册到其它Eureka Server上,这样多个Eureka Server之间就能互相发现对方,从而形成集群。因此我们做了以下修改: 把service-url的值改成了另外一台Eureka Server的地址,而不是自己

我们假设要搭建两台Eureka Server的集群,端口分别为: 1112和1113

1.添加配置文件application-p1.yml

server:
  port: 1111
spring:
  application:
    name: eureka-server
eureka:
  client:
    # 注册中心提供服务的地址
    service-url:
      defaultZone: http://127.0.0.1:1112/eureka,http://127.0.0.1:1113/eureka
    # 从其他的注册中心检索服务
    fetch-registry: true
    # 向其他注册中心注册自己
    register-with-eureka: true

2.添加配置文件application-p2.yml

server:
  port: 1112
spring:
  application:
    name: eureka-server
eureka:
  client:
    # 注册中心提供服务的地址
    service-url:
      defaultZone: http://127.0.0.1:1111/eureka,http://127.0.0.1:1113/eureka
    # 从其他的注册中心检索服务
    fetch-registry: true
    # 向其他注册中心注册自己
    register-with-eureka: true

3.添加配置文件application-p3.yml

server:
  port: 1113
spring:
  application:
 name: eureka-server
eureka:
  client:
    # 注册中心提供服务的地址
    service-url:
      defaultZone: http://127.0.0.1:1111/eureka,http://127.0.0.1:1112/eureka
    # 从其他的注册中心检索服务
    fetch-registry: true
    # 向其他注册中心注册自己
    register-with-eureka: true

5.5.4.启动三台Eureka Server

在IDEA中启动

编辑配置

image-20200910194702785

复制配置

image-20200910194824067

修改名称及Active profiles,配置完成后会弹出RunDashboard

image-20200731141530634

p2,p3节点也按此配置,然后启动即可。

image-20200708141150474

在启动的过程中,可能会出现下面的错误信息

image-20200708141245153

原因是其他的节点还没有启动,此时去注册就会出现连接超时,这个流程是正常的。

命令行启动将项目打包,需要添加打包插件

   <build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-maven-plugin</artifactId>
           </plugin>
       </plugins>
   </build>

在IDEA右侧选择Maven Projects,双击package

image-20200524100514457

打包后的文件会生成在target目录

image-20200524100805815

找到目录执行cmd打开命令行窗口

image-20200524101049987

执行以下命令:

java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=p1

再打开另外一个命令行窗口,执行命令:

java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=p2
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=p3
  1. 客户端注册服务到集群

    修改user-service,因为Eureka Server不止一个,因此注册服务的时候, service-url参数需要变化:

    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:1111/eureka,http://127.0.0.1:1112/eureka,http://127.0.0.1:1113/eureka
    

image-20200910194231047

负载均衡Ribbon

Ribbon是一个客户端负载均衡工具.[负载均衡工具]

在刚才的案例中,我们启动了一个user-service,然后通过DiscoveryClient来获取服务实例信息,获取ip和端口来访问。 但是实际环境中,我们往往会开启很多个user-service的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢? 这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。

6.1 启动两个服务实例

在user-service中添加打包插件

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

为了方便查看哪个服务被调用了,做如下改造

@RestController
@RequestMapping("/user")
public class UserController {

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

    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) {
        User user = new User(id, "tom"+" from "+port, 18, new Date());
        return user;
    }
}  

首先我们启动两个user-service实例,一个8001,一个8002。

在IDEA中可以通过如下配置来启动多个实例

image-20200910195802064

也可以通过控制台+参数来启动

java -jar user-service-0.0.1-SNAPSHOT.jar --server.port=8001
java -jar user-service-0.0.1-SNAPSHOT.jar --server.port=8002

查看Eureka控制面板

image-20200910195845921

手动实现负载均衡:

// 定义静态变量
static Integer index = 0;

@GetMapping("user/{id}")
public User getUserById(@PathVariable Long id) {
    // 通过发现客户端获取服务的列表
    // 参数是服务ID,也就是在user-service项目配置文件中定义的spring.application.name
    List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
    // 获取实例的数量
    int size = instances.size();
    // 手动实现负载均衡
    // 第一次进来index = 0 ,instances.get(index) 返回第一个服务实例
    // 第二次请求 index+1 = 1 ,instances.get(index) 返回第二个服务实例
    // 第三次请求 index+1 = 2,将index对服务实例总数做取模操作index = 2mod2 = 0
    index = index + 1;
    index = index % size;
    // 获取具体的实例
    ServiceInstance instance = instances.get(index);
    // 通过实例获取访问地址
    String host = instance.getHost();
    int port = instance.getPort();
    String url = "http://"+host+":"+port+"/user/" + id;
    // 使用RestTemplate调用user-service的服务接口
    User user = restTemplate.getForObject(url, User.class);
    return user;
}

6.2 开启负载均衡

SpringCloud 中已经帮我们集成了负载均衡组件: Ribbon

image-20201024115559030

接下来,我们就来使用Ribbon实现负载均衡。

因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖。直接修改代码: 在RestTemplate的配置方法上添加 @LoadBalanced注解:

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}

修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:

@GetMapping("user/{id}")
public User getUserById(@PathVariable Long id) {
    // String url = "http://localhost:8001/user/" + id;
    // String url = "http://"+host+":"+port+"/user/" + id;
    // 使用服务ID替换真实地址
    String url = "http://user-service" + "/user/" + id;
    User user = restTemplate.getForObject(url, User.class);
    return user;
}

访问页面,查看结果,多次刷新页面可以看到不同的返回。

image-20200910200316367

image-20200910200329840

6.3 负载均衡原理

为什么我们只输入了service名称就可以访问了呢?

关键就在于@LoadBalanced注解,源码如下

image-20200524161026269

找到LoadBalancerClient

image-20200524161506121

继承自ServiceInstanceChooser

image-20200524161533852

LoadBalancerClient所在的包中可以找到自动配置类LoadBalancerAutoConfiguration

image-20200708153841700

在该自动化配置类中,主要做了下面三件事:

  • 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  • 创建了一个RestTemplateCustomizer的Ben,用于给 RestTemplate增加 LoadBalancerInterceptor拦截器。
  • 维护了一个被 LoadBalanced注解修饰的 RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的 RestTemplate增加 LoadBalancerInterceptor拦截器。

接下来,我们看看LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户端负载均衡的:

image-20200524163546308

execute方法回到了LoadBalancerClient,查看实现类为RibbonLoadBalancerClient

image-20200524163746450

继续跟进

image-20200524164104884

这里的rule在上面定义,RoundRobinRule即为轮询的意思

image-20200524164159784

查看实现

image-20200524164525099

可以看到这里的算法是使用了原子类,每次对这个原子类做+1后对服务器数量取模,再通过CAS设置原子类的最新值:

private AtomicInteger nextServerCyclicCounter;
public RoundRobinRule() {
    nextServerCyclicCounter = new AtomicInteger(0);
}
private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextServerCyclicCounter.get();
        int next = (current + 1) % modulo;
        if (nextServerCyclicCounter.compareAndSet(current, next))
            return next;
    }
}

CAS(V,E,N),V指要更新的变量,E指要更新的期望值,N指更新后的值,当且仅当V的值等于E时,才将V更新成N。如果V的值不等于E,就表示这个变量已经被其他的任务更新过了,此次更新失败,进入循环继续执行。

CAS操作保证同一时刻只有一个任务能够更新成功,其他的进入循环继续尝试执行更新。

再看一下IRule的实现有下面这些:

image-20200524165008102

负载均衡原理总结

1. 在RestTemplate上添加了@LoadBalanced注解后,会使用LoadBalancerClient来配置RestTemplate
2. Spring Cloud Ribbon 的自动配置类LoadBalancerAutoConfiguration中的@ConditionalOnBean(LoadBalancerClient.class)条件成立
3. 自动配置中添加了LoadBalancerInterceptor,这个拦截器会拦截请求,通过服务ID获取服务的地址列表,然后通过负载均衡算法选出一个地址进行调用

image-20200708155757838

6.4 切换负载均衡策略

Spring Cloud 可以通过如下方式对RibbonClient做个性化配置:

全局配置:

ribbon.{key}={value}

指定服务配置:

{服务名称}.ribbon.{key}={value}

这里切换为随机

pay-service: 
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

再次测试,发现结果变成了随机访问 。

6.5 Ribbon的饥饿加载和懒加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,因为创建的过程中要去做服务拉取,所以请求时间会很长。

饥饿加载则是在项目启动时创建,降低第一次访问的耗时。

  • 如图所示:
  • 可以看到有一次userservice instantiated a loadBalancer–userservice初始化负载均衡器; 初始时服务列表为空,所以会做一次PollingServerListUpdater–拉取服务; 而拉取服务的过程中就会去创建:DynamicServerListLoadBalancer。 所以就会消耗很长的时间,这就是所谓的懒加载。

在这里插入图片描述

  • 通过下面配置开启饥饿加载
ribbon:
  eager-load: 
    enabled: true # 开启饥饿加载
    clients:  # 指定饥饿加载的服务名称
      - userservice
      - xxxxService

Nacos 注册中心

1.Nacos 概述

Nacos 是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos:注册中心 + 配置中心的组件

一句话概括就是Nacos = Spring Cloud Eureka 服务中心 + Spring Cloud Config配置中心。 • 官网:https://nacos.io/zh-cn/ • 下载地址: https://github.com/alibaba/nacos/releases

2.Nacosa安装

Windows下启动 nacos-server

startup.cmd -m standalone

image-20201116174126354

启动成功效果:

image-20201116174149589

控制台登录 http://localhost:8848/nacos

账号,密码:nacos

1587539128223

控制台页面

1587539185231

3.Nacos 快速入门

Spring Cloud 可以与Nacos进行无缝对接

Spring cloud Alibaba 组件 https://spring.io/projects/spring-cloud-alibaba

在父工程中添加依赖管理

<properties>
    <com.alibaba.cloud>2.1.2.RELEASE</com.alibaba.cloud>
</properties>
<dependencyManagement>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>${com.alibaba.cloud}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencyManagement>

在user-service 和 consumer-service中添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

application.yml

spring:
  cloud:
    nacos:
      discovery:
        server-addr:  127.0.0.1:8848 # 配置nacos 服务端地址

控制台显示

image-20201116175857334

4.Nacos与Eureka的区别

  • 常见的注册中心

    Eureka ​ Eureka是Netflix公司开源的注册中心,后被整合到Spring Cloud中 Zookeeper ​ 这个说起来有点意思的是官方并没有说他是一个注册中心, ​ 但是国内Dubbo场景下很多都是使用Zookeeper来完成了注册中心的功能。 Consul(原生,GO语言开发) ​ Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配 ​ 置。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和 ​ Mac OS X)。 Nacos ​ 相对于 Spring Cloud Eureka 来说,Nacos 更强大。 ​ Nacos = Spring Cloud Eureka + Spring Cloud Config ​ Nacos 可以与 Spring, Spring Boot, Spring Cloud 集成, ​ 并能代替 Spring Cloud Eureka, Spring Cloud Config。

  • Nacos和Eureka的区别

1.部署方式不同
	euraka是需要创建springboot项目,然后将euraka服务端通过gav的方式加载进来,然后部署项目。
	nacos是直接从阿里巴巴nacos的官网下载jar包,启动服务。
2.连接方式不同
	nacos使用的是netty和服务直接进行连接,属于长连接
	eureka是使用定时发送和服务进行联系,属于短连接	
3.服务异常剔除策略不同
	eureka:Eureka中会定时向注册中心发送心跳,如果在规定时间内没有发送心跳 30,则就会直接剔除。  90
	Nacos:nacos也会向注册中心发送心跳,但是它的频率要比Eureka快。5
		Nacos中又分为临时实例和非临时实例。
		如果是临时实例的话,短期内没有发送心跳,则会直接剔除。
		如果是非临时实例长时间宕机,不会直接剔除,并且注册中心会直接主动询问并且等待非临时实例。
4.消费者拉去数据方式不同
	Eureka会定时向注册中心定时拉去服务,如果不主动拉去服务,注册中心不会主动推送。
	Nacos中注册中心会定时向消费者主动推送信息,这样就会保持数据的准时性。
5.自我保护机制不同
	Eureka保护方式:当在短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下,Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enable-self-preservation: false)
	Nacos保护方式:当域名健康实例 (Instance) 占总服务实例(Instance) 的比例小于阈值时,无论实例 (Instance) 是否健康,都会将这个实例 (Instance) 返回给客户端。这样做虽然损失了一部分流量,但是保证了集群的剩余健康实例 (Instance) 能正常工作。
6.Nacos集群默认采用AP方式也可以修改为采用CP模式;
  Eureka采用AP方式 
  C(一致性)A(高可用性)P(分区容错性)定理

5.Nacos多级服务存储模型

在这里插入图片描述

1.多级服务存储模型介绍

为了提升整个系统的容灾性,Nacos 引入了地域 (Zone) 的概念,如上图中的北京、上海和杭州。把同一个服务的多个实例部署到不同地域的机房中(鸡蛋分开不同的篮子放) ;

又把在同一个地域的机房的多个服务实例称为集群 (Cluster) 。比如,杭州机房的 2 个用户服务 user-service 称为杭州 user-service 集群。

因此,在 Nacos 的服务分级模型中,

  • 第一级是微服务 (如订单服务) ;

  • 第二级是集群 (如北京订单服务集群、上海订单服务集群等) ;

  • 第三集是实例 (如杭州服务集群的 8081 端口实例、8082 端口实例等) 。

  • spring:
      cloud:
        nacos:
    	 discovery:
    		server-addr: localhost:8848 #服务中心地址
            cluster-name: HZ # 集群名称
    

1682943142439

2.集群优先负载均衡策略

对于服务的调用,杭州的服务消费者调用哪个服务提供者的效率会更高?

肯定是调用杭州自身的服务肯定效率会比调用上海的更高,因此我们对于同一集群下的服务应该优先调用,从而降低网络延时,提高响应速度。

  • 因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例
1.给服务消费者配置所在集群
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称
2.修改负载均衡规则
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则    
3.重新启动消费者和提供者,测试调用结果    

6.服务实例的权重设置

企业中我们部署项目的时候,可能会存在这么一种情况

  • 企业里服务器设备,有一些机器性能比较好,还有一些属于是祖传设备了,性能非常的差,这个时候呢,我们肯定是希望这些性能好的机器,它承担更多的用户请求,而那些性能差一点的,自然是承担少一点的请求,正所谓能者多劳嘛。
  • 但是我们目前看来,NacosRule做到的是集群优先,而后做随机,当用户请求来了以后,它可不管你是性格好的还是差,这个身强力壮的还是老弱病残拉过来就一顿造,那这个时候那些性能差的肯定就会出问题。那么我们该怎样去控制不同服务它的一个请求量呢?于是,Nacos,给我们提供了一个权重的配置,通过修改服务实力的权重,可以控制访问频率,权重越大,访问到的频率就越高,那我们就可以把性能好的机器全都设得大一点,性能差一些呢,设置的小一点。

1682943818566

权重设置为0时,该实例就不会被访问了,也就是说权重调整0时,它压根儿就不会被访问。

7.Nacos多环境隔离-namespace

  • namespace理解

    Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离。

1682944914489

Namespace:命名空间,常用于生产环境、开发环境的区分。 Group:组,常用将业务相关程度较高的放同一个组(订单和支付)。

  • 创建命名空间

1682945006922

  • 在服务提供者的application.yml中添加:

    1682945147536

    cloud:
        nacos:
            discovery:
                namespace: 命名空间ID
    
  • 作用

    namespace用来做环境隔离。 每个namespace都有唯一id。 不同namespace下的服务不可见 。

Nacos 配置中心

1.快速入门

  • Nacos配置中心依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  • bootstrap.yml 配置文件中添加配置
server:
  port: 8001
spring:
  cloud:
    nacos:
      config: # 配置中心配置
        server-addr: localhost:8848
        file-extension: yaml
      discovery: # 注册中心配置
        server-addr: localhost:8848
  application:
    name: user-service

在配置中心添加配置

image-20201125195052675

配置如下

image-20201125195156864

Data ID默认规则是:

  • 默认情况下,会加载Data ID以 ${spring.application.name}.${file-extension}为前缀的配置信息
  • 在bootstrap.yml中配置spring.cloud.nacos.config.file-extension=yaml,会读取Data ID 为${spring.application.name}.yaml 的配置信息

启动user-service 服务,访问 http://localhost:8001/user/2

image-20201125195230068

2.配置动态刷新

在nacos中修改配置,点击发布

image-20201125195316802

会提示差别

image-20201125195329383点击发布后刷新页面,会看到最新的值

image-20201125195408894

3.多环境配置与共享

  • 微服务启动时会从nacos读取多个配置文件:
1[spring.application.name]-[spring.profiles.active].yaml 
	【服务名】-【环境】.yaml
	例如:userservice-dev.yaml
	例如:userservice-test.yaml
	例如:userservice-pro.yaml
	

2 [spring.application.name].yaml  
	【服务名】.yaml 
	例如:userservice.yaml 

无论profile(环境)如何变化,[spring.application.name].yaml 这个文件一定会被加载、一定会被读取,因此多环境共享的配置可以写入这个文件。

  • 配置实战

    userservice.yaml userservice-test/dev/pro.yaml

img

如图所示,我们在bootstrap.yml里面配置了 微服务名、Nacos地址、文件后缀名,由这三部分即可组成 userservice.yaml 文件配置名,也就是你在Nacos配置中心配置的名字,Nacos就是通过这种方式找到需要读取的配置文件的。

根据图片配置,可以得知 userservice-dev.yaml 配置文件也会被读取到,因为配置了dev开发环境,但是无论如何 userservice.yaml配置文件是一定会被读取的。

img

Nacos集群

1.Nacos集群架构

Nacos集群的部署过程中,至少需要3台服务器部署节点集群

安装3个以上Nacos

​ 我们可以复制之前已经解压好的nacos文件夹,分别命名为nacos,nacos1,nacos2

1683014894641

2.Nacos集群搭建步骤

2.1 第一步设置集群

nacosbin目录下打开startup.cmd设置集群

img

2.2 第二步配置集群配置文件

由于是单机演示,需要更改nacos/conf目录下application.peoperties中server.port,防止端口冲突,设置端口分别8848、8849、8850

如果服务器有多个ip也要指定具体的ip地址,如:nacos.inetutils.ip-address=x.x.x.x

例如:
 server.port=8850
 nacos.inetutils.ip-address=127.0.0.1

在所有nacos目录下conf目录下,有文件cluster.conf.exqmple,将其命名cluster.conf,并将每行配置成为ip:port.(请配置3个或者3个以上节点)

#example
127.0.0.1:8848
127.0.0.1:8849
127.0.0.1:8850

2.3 第三步初始化 nacos的数据库为mysql数据库

  • 找到/nacos/conf下的nacos-mysql.sql脚本

img

  • 在MySQL实例创建 nacos库并执行sql脚本

    img

  • 修改 Nacos 配置文件,指向MySQL实例,替换其内嵌数据库

    img

  • 在application.properties中找到如下配置,该配置默认为注释掉的,取消注释即可,修改数据库信息为实际的数据库信息后保存。其他nacos服务实例也需要做同样的修改

    1683015673779

  • 为了达到高可用,通常MySQL也需要集群,nacos的配置文件也需要指定每一个MySQL实例的信息

    1683015794539

2.4 第四步集群模式部署启动

分别执行nacos目录下的startup

startup -m cluster

如果启动失败可能的原因是:

nacos配置文件application.properties中默认的数据库连接超时时间设置较短,如下图,因为网络延时等原因,MySQL可能会连接超时导致nacos启动报错,因此只需要将超时时间适当设置长一些即可

jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC

虚拟机内存不足,由于在vmvare创建虚拟机时,只给每个虚拟分配了1G的内存,从nacos的启动脚本startup.sh中可知,nacos以集群模式启动时,默认分配的java堆内存空间为2G,因此可判断是由于虚拟机内存不足导致nacos启动报错,修改虚拟机内存为2G后可以正常启动。

1683016453763

OpenFeign 声明式服务调用

在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码:

String url = "http://user-service/user/" + id;
User user = restTemplate.getForObject(url, User.class);

如果就学到这里,你可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢? 这就是我们接下来要学的OpenFeign的功能了。

1 简介

OpenFeign可以把Rest的请求进行隐藏,伪装成类似Spring MVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。

2 快速入门 (重点)

步骤:

1. 添加openfeign依赖
2. 在启动类上添加EnableFeignClients的注解
3. 自定义feignClient接口
4. 通过feignClient调用接口
  1. 在服务消费者导入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 在启动类上添加@EnableFeignClients注解
@SpringCloudApplication
@EnableFeignClients
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

OpenFeign中已经封装了RestTemplate并集成了Ribbon负载均衡,因此不需要自己定义RestTemplate了 ,可以注释所有使用RestTemplate的地方

  1. 编写Feign客户端[接口]
@FeignClient(value = "user-service")    // 添加FeignClient,指定服务ID
public interface UserClient {
    @GetMapping("/user/{id}")
    User getById(@PathVariable("id") Long id);
}
  • 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像

  • @FeignClient ,声明这是一个Feign客户端,同时通过 value 属性指定服务名称

  • 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果

  1. 改造controller中的调用逻辑,使用HelloClient访问:
@Autowired  // 注入UserClient
private UserClient userClient;
@GetMapping("user/{id}")
public User getUserById(@PathVariable Long id) {
    User user = userClient.getById(id);
    return user;
}
  1. 测试

访问http://localhost:9001/consumer/user/2,可以看到成功返回数据

image-20200916101452222

3.OpenFeign的自定义配置

1.超时控制

Feign客户端默认等待1s,若服务端处理需要超过1s,导致Feign客户端不想等了,直接返回报错。

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

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

    @GetMapping("/{id}")
    public User getById(@PathVariable("id") Long id) throws InterruptedException {
        log.info(port + " 所在服务调用了getById...");
        Thread.sleep(2000);
        User user = new User(id, "tom" + " from " + port, 18, new Date());
        return user;
    }
}

yml文件配置OpenFeign超时控制

ribbon:
  ReadTimeout: 5000 //建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000 //建立连接所用的时间

2.日志配置

1683017944020

//此处不用添加@Configuration,不然会被作为全局配置文件共享
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}

全局配置

1.FeignConfig类上添加@Configuration就成为全局配置

2.或者在启动类的@EnableFeignClientsdefaultConfiguration指定该配置类

局部配置

//基于注解配置
@FeignClient(value = "服务名",configuration = 配置类.class)
public interface UserClient {
    @GetMapping("/user/{id}")
    User getById(@PathVariable("id") Long id);
}
//基于yaml配置
//#开启Feign的局部日志级别
feign:
  client:
    config:
      feign-provider:
        logger-level: FULL

注意

必须调整SpringBoot日志的输出级别为Debug,否则无法输出OpenFeign的调用信息:
logging:
  level:
    com.zhyp.feign: debug

4.OpenFeign优化实战

超时优化

我们介绍过OpenFeign 底层内置了 Ribbon 框架,并且使用了 Ribbon 的请求连接超时时间和请求处理超时时间作为其超时时间,而 Ribbon 默认的请求连接超时时间和请求处理超时时间都是 1s

  • 我们可以通过配置来修改:

全局配置 使用ribbon.=

ribbon:
  ReadTimeout: 2500 # 数据通信超时时长 默认为1000
  ConnectTimeout: 2500 # 连接超时时长 默认为1000
  #重试机制的优化
  OkToRetryOnAllOperations: false # 对所有的操作请求都进行重试(对于查询和修改我们可以重试,对于增删重试操作是危险的)
  MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试访问
  MaxAutoRetriesNextServer: 1 # 当前服务总是超时时,切换实例的次数

如果要指定某个服务,配置: .ribbon. =

  • 也可以通过直接修改feign的配置来实现

    feign:
     client:
      config:
       default: # 设置的全局超时时间
         connectTimeout: 2000 # 请求连接的超时时间
         readTimeout: 5000 # 请求处理的超时时间
    

请求连接优化

  • OpenFeign 底层通信组件默认使用 JDK 自带的 URLConnection 对象进行 HTTP 请求的,因为没有使用连接池,所以性能不是很好。

  • 我们可以将 OpenFeign 的通讯组件,手动替换成像 Apache HttpClient 或 OKHttp 这样的专用通信组件,这些的专用通信组件自带连接池可以更好地对 HTTP 连接对象进行重用与管理,同时也能大大的提升 HTTP 请求的效率

  • 引入依赖

    <!-- 添加 openfeign 框架依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- 添加 httpclient 框架依赖 -->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
    <!-- 添加 okhttp 框架依赖 -->
    <dependency>
      <groupId>io.github.openfeign</groupId>
      <artifactId>feign-okhttp</artifactId>
    </dependency>
    
  • 开启

    feign:
      okhttp:
        enabled: true
    # 或者
    feign:
      httpclient:
        enabled: true
    

数据压缩优化

  • 在 OpenFeign 中,默认并没有开启数据压缩功能。但如果你在服务间单次传递数据超过 1K 字节,强烈推荐开启数据压缩功能。
  • 默认 OpenFeign 使用 Gzip 方式压缩数据,对于大文本通常压缩后尺寸只相当于原始数据的 10%~30%,这会极大提高带宽利用率。
feign:
  compression:
    request:
      enabled: true  # 开启请求数据的压缩功能
      mime-types: text/xml,application/xml, application/json  # 压缩类型
      min-request-size: 1024  # 最小压缩值标准,当数据大于 1024 才会进行压缩
    response:
      enabled: true  # 开启响应数据压缩功能

注意: 优化并不是套路,如果是OpenFeign中就自己默认给优化了,我们需要结合项目

如果应用属于计算密集型,CPU 负载长期超过 70%,因数据压缩、解压缩都需要 CPU 运算,开启数据压缩功能反而会给 CPU 增加额外负担,导致系统性能降低,这是不可取的。这种情况 建议不要开启数据的压缩功能

负载均衡优化

  • OpenFeign 使用时默认引用 Ribbon 实现客户端负载均衡,它默认的负载均衡策略是轮询策略。那如何设置 Ribbon 默认的负载均衡策略呢?

  • 只需在 application.yml 中调整微服务通信时使用的负载均衡类即可。

    xxx-service: #服务提供者的微服务ID
      ribbon:
        #设置对应的负载均衡类
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    

    1683020760909

日志级别优化

OpenFeign 提供了日志增强功能,它的日志级别有以下几个:

  • NONE: 默认的,不显示任何日志。
  • BASIC: 仅记录请求方法、URL、响应状态码及执行时间。
  • HEADERS: 除了 BASIC 中定义的信息之外,还有请求和响应的头信息。
  • FULL: 除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。

全局配置+指定服务配置

注意: springboot默认输出info级别的信息,OpenFeign输出的信息是debug级别

于是需要设置

#properties配置文件
logging.level.包名=debug
#yaml配置文件
logging: 
	level: 
		包名: debug

网关Gateway

1 Gateway网关-概述

  • 网关旨在为微服务架构提供一种简单而有效的统一的API管理和路由方式。

  • 在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请求,客户端可能通过调用N个微服务的接口完成一个用户请求。

  • 存在的问题:

1.客户端多次请求不同的微服务,增加客户端的复杂性
2.认证复杂,每个服务都要进行认证
3.http请求不同服务次数增加,性能不高
  • 网关就是系统的入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、缓存、负载均衡、流量管控、路由转发等
  • 在目前的网关解决方案里,有Nginx+ Lua、Netflix Zuul 、Spring Cloud Gateway等等

1587544847831

2 Gateway-快速入门

  1. 搭建网关模块

    创建api-gateway模块

  2. 引入依赖:starter-gateway

    <dependencies>
    <!--网关也需要从注册中心中拉去所有服务信息,因为他要负责路由-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <!--网关的jar-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
    </dependencies>
    注意: 这里不需要spring-boot-starter-web 因为gateway自带了
    
  3. 编写启动类

    package com.itheima.gateway;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    
    @SpringBootApplication
    public class ApiGatewayApp {
    
        public static void main(String[] args) {
            SpringApplication.run(ApiGatewayApp.class,args);
        }
    
    }
    
  4. 编写配置文件

    application.yml

    server:
      port: 8888
    spring:
      application:
        name: api-gateway
      cloud:
        # 网关配置
        gateway:
          # 路由配置:转发规则
          routes: #集合。
          # id: 唯一标识。默认是一个UUID
          # uri: 转发路径
          # predicates: 条件,用于请求网关路径的匹配规则
          - id: user-service
            uri: http://localhost:8001/  #lb://微服务id
            predicates:
            - Path=/user/**
    
  5. 启动测试 , 访问 http://localhost:8888/user/3

    image-20201124201738925

3.路由断言工厂PredicateFactory

路由是网关的基本模块。它由ID、目标URI、Predicate集合和Filter集合定义。如果聚合Predicate为真,则匹配路由。

接下来详细介绍断言,如我们刚刚的配置的predicates中的每一个配置官方都称之为路由断言工厂。它的作用就是:当请求gateway的时候,使用断言对请求进行匹配,如果匹配成功就路由转发,如果匹配失败就返回404

  • Path 路由断言工厂

    我们快速入门案例中使用的,也支持通配符,如果有多个path中间使用逗号分隔
    	例如: - Path=/user/**,/order/{oid}
    
  • Date路由断言工厂

    有After/Before/Between三种与时间相关的路由断言工厂

    Before路由断言工厂接受一个日期时间。此断言匹配在该日期时间之前发生的请求。
    	例如:- Before=2021-12-17T19:30:00+08:00[Asia/Shanghai]
    After路由断言工厂接受一个日期时间,该断言匹配该日期时间之后的请求。
    	例如:- After=2021-12-17T19:38:00.129+08:00[Asia/Shanghai]
    Between路由断言工厂接受两个日期时间。此断言匹配发生两个日期之间的请求。
    	例如:- Between=2021-12-17T19:38:00.129+08:00[Asia/Shanghai],
    2021-12-17T19:42:00.129+08:00[Asia/Shanghai]
    
  • Cookie路由断言工厂

    Cookie路由断言工厂接受两个参数,即Cookie名称和配置值的正则表达式。
    	例如:- Cookie=java,.*version.*
    
  • Header路由断言工厂

    Header路由断言工厂接受两个参数,Header名称和配置值的正则表达式。
    	例如:- Header=address,.*ok.*
    
  • Method路由断言工厂

    Method路由断言工厂接受一个或者多个参数:要匹配的HTTP请求方式。
    	例如:- Method=GET,POST
    
  • Query路由断言工厂

    Query路由断言工厂接受两个参数:必需的param和可选的regexp。匹配当前请求的参数中是否匹配该路由
    	例如:- Query=green 如果请求包含green查询参数,则路由匹配。
    	例如:- Query=foo, ba. 如果请求包含foo查询参数,且值为ba开头的三个字母,则路由匹配
    

4.过滤器工厂Filter Factory

gateway 里面的过滤器和 Servlet 里面的过滤器,功能差不多,路由过滤器可以用于修改进入Http 请求和返回 Http 响应

4.1 过滤器-概述

  • Gateway 支持过滤器功能,对请求或响应进行拦截,完成一些通用操作。

  • Gateway 提供两种过滤器方式:“pre”和“post”

    pre 过滤器,在转发之前执行,可以做参数校验、权限校验、流量监控、日志输出、协议转换等。 ​ post 过滤器,在后端微服务响应之后并且给前端响应之前执行,可以做响应内容、响应头的修改,日志的输出,流量监控等。

  • Gateway 还提供了两种类型过滤器 ​ GatewayFilter:局部过滤器,针对单个路由 ​ GlobalFilter :全局过滤器,针对所有路由

1587546321584

4.2-局部过滤器GatewayFilter

  • GatewayFilter 局部过滤器,是针对单个路由的过滤器。
  • 在Spring Cloud Gateway 组件中提供了大量内置的局部过滤器,对请求和响应做过滤操作。
  • 遵循约定大于配置的思想,只需要在配置文件配置局部过滤器名称,并为其指定对应的值,就可以让其生效。

修改配置 application.yml

server:
  port: 8888
spring:
  application:
    name: api-gateway
  cloud:
    # 网关配置
    gateway:
      # 路由配置:转发规则
      routes: #集合。
      # id: 唯一标识。默认是一个UUID
      # uri: 转发路径
      # predicates: 条件,用于请求网关路径的匹配规则
      - id: user-service
        uri: lb://user-service
        predicates:
        - Path=/user/**
        filters:
        - AddRequestParameter=name,admin
    nacos:
      discovery:
        server-addr: localhost:8848

在user-service模块的UserController中添加方法

@GetMapping("/name")
public User getUser(String name) {
    User user = new User(1L, name + " from " + port, 18, new Date());
    return user;
}

访问http://localhost:8001/user/name

image-20201124204113931

通过网关访问http://localhost:8888/user/name

image-20201124204135300

1683029414852

1683029448410

1683029487803

4.3-全局过滤器GlobalFilter

  • GlobalFilter 全局过滤器,不需要在配置文件中配置,系统初始化时加载,并作用在每个路由上。
  • Spring Cloud Gateway 核心的功能也是通过内置的全局过滤器来完成。

1587546455139

4.4 全局过滤器案例

统计请求次数 限流 token 的校验 ip 黑名单拦截 跨域(filter) 我们可以自定义全局过滤

自定义全局过滤器步骤:

  1. 定义类实现 GlobalFilter 和 Ordered接口
  2. 复写方法
  3. 完成逻辑处理

==需求: 自定义全局过滤,要求访问的请求中要包含token字段,否则不允许访问==

package com.heima.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Service
public class MyFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 需求 如果请求头中带有token,则放行,否则提示未授权状态(401)
        System.out.println("自定义全局过滤器执行了~~~");
        // 获取请求对象
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst("token");
        if(StringUtils.isEmpty(token)){
            // 获取响应对象
            ServerHttpResponse response = exchange.getResponse();
            // 返回响应码
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            // 完成响应
            return response.setComplete();
        }
        return chain.filter(exchange);//放行
    }

    /**
     * 过滤器排序
     * @return 数值越小 越先执行
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

启动测试 , 使用postman来请求 ,访问 http://localhost:8888/user/3

image-20201124204812200

在header中添加token

image-20201124204857395

5.过滤器-限流实战

5.1 限流介绍

  • 限流:

    是通过对并发访问/请求进行限速或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可由拒绝服务,就是定向到错误页或友好的展示页,排队或等待

    在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。

  • 常见的限流算法

    ==计数器算法==

    计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”

    ==漏桶算法==

    漏桶算法为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

    img

    在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。

    这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

    ==令牌桶算法==

    从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

    img

实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。

5.2 Gateway限流和自定义限流

在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,基于Redis和Lua脚本实现了令牌桶的方式限流。

  • 由于基于Redis实现的,所以需要引入Redis依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 配置Redis和限流规则

    spring:
      redis:
        host: 127.0.0.1
        port: 6379
      cloud:
        gateway:
          routes:
            - id: after_route
              uri: http://localhost:8001/
              predicates:
                - Path=/user/**
              filters:
                - name: RequestRateLimiter
                  args:
                    # 令牌桶每秒填充平均速率
                    redis-rate-limiter.replenishRate: 1
                    # 令牌桶的上限
                    redis-rate-limiter.burstCapacity: 2
                    # 使用SpEL表达式从Spring容器中获取Bean对象
                    key-resolver: "#{@pathKeyResolver}"
    
  • 自定义限流键

    @Configuration
    public class MyKeyResolver implements KeyResolver {
    
        /**
         * 返回值Mono<String>中的泛型表示令牌与根据哪个值进行绑定
         * 比如:  如果是常量值  那么表示全局限流
         * 比如:	如果是用户ip 那么表示对每个用户限流
         * 比如:  如果是接口路径 那么表示对当前接口限流
         */
        @Override
        public Mono<String> resolve(ServerWebExchange exchange) {
            String path = exchange.getRequest().getURI().getPath();
            //根据ip限流
            String hostAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
            return Mono.just(hostAddress);
        }
    }
    
  • 其他限流方式

    /**
     * 通过IP限流
     * @return
     */
    @Bean
    public KeyResolver ipKeyResolver(){
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
            }
        };
    }
    
    /**
         * 通过用户限流
         * 请求路径中必须要包含用户的唯一表示,userId参数
         */
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
    }
    
    /**
         * 通过接口限流
         * 请求地址的uri作为限流的key
         */
    @Bean
    public KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }
    

6.GateWay跨域配置

6.1 跨域理解

我们经常会在一个(A服务器)页面中发起一个请求(ajax,axios),当一个请求url的协议、域名、端口三者任意一个与当前页面url不同即为跨域。

简单的说就是发起请求的那个页面的地址 和 请求的接口地址 不在同一个域中。

浏览器不能执行其他网站的脚本,从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域。跨域是由浏览器的同源策略造成的,是浏览器施加的安全限制。a网站页面想获取b网站的资源,如果a、b的协议、域名、端口、不同,所进行的访问行动都是跨域的。

1683079850244

跨域的本质

跨域本质是浏览器基于同源策略的一种安全手段,非同源的请求可以发出,服务器也可以返回结果,但是浏览器基于同源策略不会接收返回的结果

所以跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了

6.2 GateWay的跨域配置

我们了解了跨域的概念和原因,解决跨域的方式就很简单了,只需要在服务器返回的response中添加几个头信息,通知浏览器允许接收跨域信息即可.

如果每个请求的response都去写,那么就太麻烦了,我们可以考虑一个全局的过滤器,当请求执行后在response响应给浏览器之前添加允许跨域的头信息即可.

Spring Cloud Gateway已经解决跨域问题,只需要使用一个CorsWebFilter过滤器即可

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}
spring:
  cloud:
    gateway:
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求
              - "http://localhost:8090"
              - "http://www.leyou.com"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

7. Gateway-静态路由

application.yml 中的uri是写死的,就是静态路由

server:
  port: 8888
spring:
  application:
    name: api-gateway
  cloud:
    # 网关配置
    gateway:
      # 路由配置:转发规则
      routes: #集合。
      # id: 唯一标识。默认是一个UUID
      # uri: 转发路径
      # predicates: 条件,用于请求网关路径的匹配规则
      - id: user-service
        uri: http://localhost:8001/  
        #要路由的微服务地址 静态地址  静态路由  就算微服务只有一台 也不建议这么写
        predicates:
        - Path=/user/**

8. Gateway-动态路由

添加注册中心

<!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!--</dependency>-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

启动类添加@EnableDiscoveryClient

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApp {

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

}

application.yml 中添加注册中心地址及修改uri属性:uri: lb://服务名称

server:
  port: 8888
spring:
  application:
    name: api-gateway
  cloud:
    # 网关配置
    gateway:
      # 路由配置:转发规则
      routes: #集合。
      # id: 唯一标识。默认是一个UUID
      # uri: 转发路径
      # predicates: 条件,用于请求网关路径的匹配规则
      - id: user-service
        uri: lb://user-service  
        #lb开头表示负载均衡,后面还是微服务的id,必须和nacos或者eureka注册中心的名字一模一样(大小写也)
        predicates:
        - Path=/user/**
    nacos:
      discovery:
        server-addr: localhost:8848

Sentinel微服务保护

1.初识Sentinel

1.1.雪崩问题及解决方案

1.1.1.雪崩问题

微服务中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微服务。

1533829099748

如图,如果服务提供者I发生了故障,当前的应用的部分业务因为依赖于服务I,因此也会被阻塞。此时,其它不依赖于服务I的业务似乎不受影响。

1533829198240

但是,依赖服务I的业务请求被阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞

服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,那么当前服务也就不可用了。

那么,依赖于当前服务的其它服务随着时间的推移,最终也都会变的不可用,形成级联失败,雪崩就发生了:

image-20210715172710340

1.1.2.超时处理

解决雪崩问题的常见方式有四种:

•超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待

image-20210715172820438

1.1.3.仓壁模式

方案2:仓壁模式

仓壁模式来源于船舱的设计:

image-20210715172946352

船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。

于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。

image-20210715173215243

1.1.4.断路器

断路器模式:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。

断路器会统计访问某个服务的请求数量,异常比例:

image-20210715173327075

当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断:

image-20210715173428073

1.1.5.限流

流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。

image-20210715173555158

1.1.6.总结

什么是雪崩问题?

  • 微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。

可以认为:

限流是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。

超时处理、线程隔离、降级熔断是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施。

1.2.服务保护技术对比

在SpringCloud当中支持多种服务保护技术:

早期比较流行的是Hystrix框架,但目前国内实用最广泛的还是阿里巴巴的Sentinel框架,这里我们做下对比:

Sentinel Hystrix
熔断降级策略 基于慢调用比例或异常比例 基于失败比率
实时指标实现 滑动窗口 滑动窗口(基于 RxJava)
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于 QPS,支持基于调用关系的限流 有限的支持
流量整形 支持慢启动、匀速排队模式 不支持
系统自适应保护 支持 不支持
控制台 开箱即用,可配置规则、查看秒级监控、机器发现等 不完善
隔离策略 信号量隔离 线程池隔离/信号量隔离
常见框架的适配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix

1.3.Sentinel介绍和安装

1.3.1.初识Sentinel

Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html

Sentinel 具有以下特征:

丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。

完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

1.3.2.安装Sentinel

1)下载

sentinel官方提供了UI控制台,方便我们对系统做限流设置。大家可以在GitHub下载。

课前资料也提供了下载好的jar包:

image-20210715174252531

2)运行

将jar包放到任意非中文目录,执行命令:

java -jar sentinel-dashboard-1.8.1.jar

如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:

配置项 默认值 说明
server.port 8080 服务端口
sentinel.dashboard.auth.username sentinel 默认用户名
sentinel.dashboard.auth.password sentinel 默认密码

例如,修改端口:

java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar

3)访问

访问http://localhost:8080页面,就可以看到sentinel的控制台了:

image-20210715190827846

需要输入账号和密码,默认都是:sentinel

登录后,发现一片空白,什么都没有:

image-20210715191134448

这是因为我们还没有与微服务整合。

1.4.微服务整合Sentinel

我们在sentinel-order-service中整合sentinel,并连接sentinel的控制台,步骤如下:

1)引入sentinel依赖

如果出现版本问题,不要直接去修改sentinel的版本,而是父pom中spring-cloud-alibaba-dependencies的版本

<!--sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2)配置控制台

修改application.yaml文件,添加下面内容:

server:
  port: 8088
spring:
  cloud: 
    sentinel:
      transport:
        dashboard: localhost:8080

3)访问order-service的任意端点

打开浏览器,访问http://localhost:8088/order/101,这样才能触发sentinel的监控。

然后再访问sentinel的控制台,查看效果:

image-20210715191241799

2.流量控制

雪崩问题虽然有四种方案,但是限流是避免服务因突发的流量而发生故障,是对微服务雪崩问题的预防。我们先学习这种模式。

2.1.簇点链路

当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源

默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。

例如,我们刚才访问的order-service中的OrderController中的端点:/order/{orderId}

image-20210715191757319

流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:

  • 流控:流量控制
  • 降级:降级熔断
  • 热点:热点参数限流,是限流的一种
  • 授权:请求的权限控制

2.1.快速入门

2.1.1.示例

点击资源/order/{orderId}后面的流控按钮,就可以弹出表单。

image-20210715191757319

表单中可以填写限流规则,如下:

image-20210715192010657

其含义是限制 /order/{orderId}这个资源的单机QPS为1,即每秒只允许1次请求,超出的请求会被拦截并报错。

2.1.2.练习:

需求:给 /order/{orderId}这个资源设置流控规则,QPS不能超过 5,然后测试。

1)首先在sentinel控制台添加限流规则

image-20210715192455429

2)利用jmeter测试

如果没有用过jmeter,可以参考课前资料提供的文档《Jmeter快速入门.md》

课前资料提供了编写好的Jmeter测试样例:

image-20210715200431615

打开jmeter,导入课前资料提供的测试样例:

image-20210715200537171

选择:

image-20210715200635414

20个用户,2秒内运行完,QPS是10,超过了5.

选中流控入门,QPS<5右键运行:

image-20210715200804594

注意,不要点击菜单中的执行按钮来运行。

结果:

image-20210715200853671

可以看到,成功的请求每次只有5个

2.2.流控模式

在添加限流规则时,点击高级选项,可以选择三种流控模式

  • 直接:统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
  • 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
  • 链路:统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流

image-20210715201827886

快速入门测试的就是直接模式。

2.2.1.关联模式

关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流

配置规则

image-20210715202540786

语法说明:当/write资源访问量触发阈值时,就会对/read资源限流,避免影响/write资源。

使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。

需求说明

  • 在OrderController新建两个端点:/order/query和/order/update,无需实现业务
  • 配置流控规则,当/order/ update资源被访问的QPS超过5时,对/order/query请求限流

1)定义/order/query端点,模拟订单查询

@GetMapping("/query")
public String queryOrder() {
    return "查询订单成功";
}

2)定义/order/update端点,模拟订单更新

@GetMapping("/update")
public String updateOrder() {
    return "更新订单成功";
}

重启服务,查看sentinel控制台的簇点链路:

image-20210716101805951

3)配置流控规则

对哪个端点限流,就点击哪个端点后面的按钮。我们是对订单查询/order/query限流,因此点击它后面的按钮:

image-20210716101934499

在表单中填写流控规则:

image-20210716102103814

4)在Jmeter测试

选择《流控模式-关联》:

image-20210716102416266

可以看到1000个用户,100秒,因此QPS为10,超过了我们设定的阈值:5

查看http请求:

image-20210716102532554

请求的目标是/order/update,这样这个断点就会触发阈值。

但限流的目标是/order/query,我们在浏览器访问,可以发现:

image-20210716102636030

确实被限流了。

5)总结

image-20210716103143002

2.2.2.链路模式

链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。

配置示例

例如有两条请求链路:

  • /test1 –> /common
  • /test2 –> /common

如果只希望统计从/test2进入到/common的请求,则可以这样配置:

image-20210716103536346

实战案例

需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。

步骤:

  1. 在OrderService中添加一个queryGoods方法,不用实现业务
  2. 在OrderController中,改造/order/query端点,调用OrderService中的queryGoods方法
  3. 在OrderController中添加一个/order/save的端点,调用OrderService的queryGoods方法
  4. 给queryGoods设置限流规则,从/order/query进入queryGoods的方法限制QPS必须小于2

实现:

1)添加查询商品方法

在order-service服务中,给OrderService类添加一个queryGoods方法:

public void queryGoods(){
    System.err.println("查询商品");
}

2)查询订单时,查询商品

在order-service的OrderController中,修改/order/query端点的业务逻辑:

@GetMapping("/query")
public String queryOrder() {
    // 查询商品
    orderService.queryGoods();
    // 查询订单
    System.out.println("查询订单");
    return "查询订单成功";
}

3)新增订单,查询商品

在order-service的OrderController中,修改/order/save端点,模拟新增订单:

@GetMapping("/save")
public String saveOrder() {
    // 查询商品
    orderService.queryGoods();
    // 查询订单
    System.err.println("新增订单");
    return "新增订单成功";
}

4)给查询商品添加资源标记

默认情况下,OrderService中的方法是不被Sentinel监控的,需要我们自己通过注解来标记要监控的方法。

给OrderService的queryGoods方法添加@SentinelResource注解:

@SentinelResource("goods")
public void queryGoods(){
    System.err.println("查询商品");
}

链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,会导致链路模式失效。

我们需要关闭这种对SpringMVC的资源聚合,修改order-service服务的application.yml文件:

spring:
  cloud:
    sentinel:
      web-context-unify: false # 关闭context整合
这个配置必须是sentinel-2.1.3版本或者以上

重启服务,访问/order/query和/order/save,可以查看到sentinel的簇点链路规则中,出现了新的资源:

image-20210716105227163

5)添加流控规则

点击goods资源后面的流控按钮,在弹出的表单中填写下面信息:

image-20210716105408723

只统计从/order/query进入/goods的资源,QPS阈值为2,超出则被限流。

6)Jmeter测试

选择《流控模式-链路》:

image-20210716105612312

可以看到这里200个用户,50秒内发完,QPS为4,超过了我们设定的阈值2

一个http请求是访问/order/save:

image-20210716105812789

运行的结果:

image-20210716110027064

完全不受影响。

另一个是访问/order/query:

image-20210716105855951

运行结果:

image-20210716105956401

每次只有2个通过。

2.2.3.总结

流控模式有哪些?

•直接:对当前资源限流

•关联:高优先级资源触发阈值,对低优先级资源限流。

•链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流

2.3.流控效果

在流控的高级选项中,还有一个流控效果选项:

image-20210716110225104

流控效果是指请求达到流控阈值时应该采取的措施,包括三种:

  • 快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
  • warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
  • 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长

2.3.1.warm up

阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。

warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.

例如,我设置QPS的maxThreshold为9,预热时间为3秒,那么初始阈值就是 10 / 3 ,也就是3,然后在3秒后逐渐增长到9.

image-20210716110629796

案例

需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用warm up效果,预热时长为5秒

1)配置流控规则:

image-20210716111012387

2)Jmeter测试

选择《流控效果,warm up》:

image-20210716111136699

QPS为10.

刚刚启动时,大部分请求失败,成功的只有3个,说明QPS被限定在3:

image-20210716111303701

随着时间推移,成功比例越来越高:

image-20210716111404717

到Sentinel控制台查看实时监控:

image-20210716111526480

一段时间后:

image-20210716111658541

2.3.2.排队等待

当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。

而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。

工作原理

例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。

那什么叫做预期等待时长呢?

比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:

  • 第6个请求的预期等待时长 = 200 * (6 - 1) = 1000ms 1200m 1400ms
  • 第12个请求的预期等待时长 = 200 * (12-1) = 2200ms

现在,第1秒同时接收到10个请求,但第2秒只有1个请求,此时QPS的曲线这样的:

image-20210716113147176

如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:

image-20210716113426524

平滑的QPS曲线,对于服务器来说是更友好的。

案例

需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s

1)添加流控规则

image-20210716114048918

2)Jmeter测试

选择《流控效果,队列》:

image-20210716114243558

QPS为15,已经超过了我们设定的10。

如果是之前的 快速失败、warmup模式,超出的请求应该会直接报错。

但是我们看看队列模式的运行结果:

image-20210716114429361

全部都通过了。

再去sentinel查看实时监控的QPS曲线:

image-20210716114522935

QPS非常平滑,一致保持在10,但是超出的请求没有被拒绝,而是放入队列。因此响应时间(等待时间)会越来越长。

当队列满了以后,才会有部分请求失败:

image-20210716114651137

2.3.3.总结

流控效果有哪些?

  • 快速失败:QPS超过阈值时,拒绝新的请求
  • warm up: QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机。
  • 排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝

2.4.热点参数限流

之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。

2.4.1.全局参数限流

例如,一个根据id查询商品的接口:

image-20210716115014663

访问/goods/{id}的请求中,id参数值会有变化,热点参数限流会根据参数值分别统计QPS,统计结果:

image-20210716115131463

当id=1的请求触发阈值被限流时,id值不为1的请求不受影响。

配置示例:

image-20210716115232426

代表的含义是:对hot这个资源的0号参数(第一个参数)做统计,每1秒相同参数值的请求数不能超过5

2.4.2.热点参数限流

刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都限定为5.

而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了:

image-20210716115717523

结合上一个配置,这里的含义是对0号的long类型参数限流,每1秒相同参数的QPS不能超过5,有两个例外:

•如果参数值是100,则每1秒允许的QPS为10

•如果参数值是101,则每1秒允许的QPS为15

2.4.4.案例

案例需求:给/order/{orderId}这个资源添加热点参数限流,规则如下:

•默认的热点参数规则是每1秒请求量不超过2

•给102这个参数设置例外:每1秒请求量不超过4

•给103这个参数设置例外:每1秒请求量不超过10

注意事项:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源

1)标记资源

给order-service中的OrderController中的/order/{orderId}资源添加注解:

image-20210716120033572

2)热点参数限流规则

访问该接口,可以看到我们标记的hot资源出现了:

image-20210716120208509

这里不要点击hot后面的按钮,页面有BUG

点击左侧菜单中热点规则菜单:

image-20210716120319009

点击新增,填写表单:

image-20210716120536714

3)Jmeter测试

选择《热点参数限流 QPS1》:

image-20210716120754527

这里发起请求的QPS为5.

包含3个http请求:

普通参数,QPS阈值为2

image-20210716120840501

运行结果:

image-20210716121105567

例外项,QPS阈值为4

image-20210716120900365

运行结果:

image-20210716121201630

例外项,QPS阈值为10

image-20210716120919131

运行结果:

image-20210716121220305

3.隔离和降级

限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。

而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。

线程隔离之前讲到过:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。

image-20210715173215243

熔断降级:是在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高,则熔断该业务,不允许访问该服务的提供者了。

image-20210715173428073

可以看到,不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。

而我们的微服务远程调用都是基于Feign来完成的,因此我们需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。

3.1.FeignClient整合Sentinel

SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。

注意: OpenFeign和Sentinel都要使用父POM中spring-cloud-alibaba-dependecies规定好的版本,如果没有锁定版本,那么我们写和spring-cloud-alibaba-dependecies一致版本即可

3.1.1.修改配置,开启sentinel功能

修改OrderService的application.yml文件,开启Feign的Sentinel功能:

feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持

3.1.2.编写失败降级逻辑

业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。

给FeignClient编写失败后的降级逻辑

①方式一:FallbackClass,无法对远程调用的异常做处理

②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种

这里我们演示方式二的失败降级处理。

步骤一:在feing-api项目中定义类,实现FallbackFactory:

image-20210716122403502

代码:

package cn.itcast.feign.clients.fallback;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            @Override
            public User findById(Long id) {
                log.error("查询用户异常", throwable);
                return new User();
            }
        };
    }
}

步骤二:在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean:

@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
    return new UserClientFallbackFactory();
}

步骤三:在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:

import cn.itcast.feign.clients.fallback.UserClientFallbackFactory;
import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

重启后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路:

image-20210716123705780

3.1.3.总结

Sentinel支持的雪崩解决方案:

  • 线程隔离(仓壁模式)
  • 降级熔断

Feign整合Sentinel的步骤:

  • 在application.yml中配置:feign.sentienl.enable=true
  • 给FeignClient编写FallbackFactory并注册为Bean
  • 将FallbackFactory配置到FeignClient

3.2.线程隔离(舱壁模式)

3.2.1.线程隔离的实现方式

线程隔离有两种方式实现:

  • 线程池隔离
  • 信号量隔离(Sentinel默认采用)

如图:

image-20210716123036937

线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果

信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。

两者的优缺点:

image-20210716123240518

3.2.2.sentinel的线程隔离

用法说明

在添加限流规则时,可以选择两种阈值类型:

image-20210716123411217

  • QPS:就是每秒的请求数,在快速入门中已经演示过
  • 线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。

案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试。

1)配置隔离规则

选择feign接口后面的流控按钮:

image-20210716123831992

填写表单:

image-20210716123936844

2)Jmeter测试

选择《阈值类型-线程数<2》:

image-20210716124229894

一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。

查看运行结果:

image-20210716124147820

发现虽然结果都是通过了,不过部分请求得到的响应是降级返回的null信息。

3.2.3.总结

线程隔离的两种手段是?

  • 信号量隔离
  • 线程池隔离

信号量隔离的特点是?

  • 基于计数器模式,简单,开销小

线程池隔离的特点是?

  • 基于线程池模式,有额外开销,但隔离控制更强

3.3.熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。

断路器控制熔断和放行是通过状态机来完成的:

image-20210716130958518

状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

断路器熔断策略有三种:慢调用、异常比例、异常数

3.3.1.慢调用

慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。

例如:

image-20210716145934347

解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。

案例

需求:给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5

1)设置慢调用

修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:

image-20210716150234787

此时,orderId=101的订单,关联的是id为1的用户,调用时长为60ms:

image-20210716150510956

orderId=102的订单,关联的是id为2的用户,调用时长为非常短;

image-20210716150605208

2)设置熔断规则

下面,给feign接口设置降级规则:

image-20210716150654094

规则:

image-20210716150740434

超过50ms的请求都会被认为是慢请求

3)测试

在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:

image-20210716150911004

触发了熔断,请求时长缩短至5ms,快速失败了,并且走降级逻辑,返回的null

在浏览器访问:http://localhost:8088/order/102,竟然也被熔断了:

image-20210716151107785

3.3.2.异常比例、异常数

异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。

例如,一个异常比例设置:

image-20210716131430682

解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。

一个异常数设置:

image-20210716131522912

解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。

案例

需求:给 UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5s

1)设置异常请求

首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例的熔断:

image-20210716151348183

也就是说,id 为 2时,就会触发异常

2)设置熔断规则

下面,给feign接口设置降级规则:

image-20210716150654094

规则:

image-20210716151538785

在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。

3)测试

在浏览器快速访问:http://localhost:8088/order/102,快速刷新5次,触发熔断:

image-20210716151722916

此时,我们去访问本来应该正常的103:

image-20210716151844817

4.授权规则

授权规则可以对请求方来源做判断和控制。

4.1.授权规则

4.1.1.基本规则

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。

  • 白名单:来源(origin)在白名单内的调用者允许访问
  • 黑名单:来源(origin)在黑名单内的调用者不允许访问

点击左侧菜单的授权,可以看到授权规则:

image-20210716152010750

  • 资源名:就是受保护的资源,例如/order/{orderId}
  • 流控应用:是来源者的名单,
    • 如果是勾选白名单,则名单中的来源被许可访问。
    • 如果是勾选黑名单,则名单中的来源被禁止访问。

比如:

image-20210716152349191

我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)

4.1.2.如何获取origin

Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。

public interface RequestOriginParser {
    /**
     * 从请求request对象中获取origin,获取方式自定义
     */
    String parseOrigin(HttpServletRequest request);
}

这个方法的作用就是从request对象中,获取请求者的origin值并返回。

默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。

因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin

例如order-service服务中,我们定义一个RequestOriginParser的实现类:

package cn.itcast.order.sentinel;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

@Component
public class HeaderOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 1.获取请求头
        String origin = request.getHeader("origin");
        // 2.非空判断
        if (StringUtils.isEmpty(origin)) {
            origin = "blank";
        }
        return origin;
    }
}

我们会尝试从request-header中获取origin值。

4.1.3.给网关添加请求头

既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头

这个需要利用之前学习的一个GatewayFilter来实现,AddRequestHeaderGatewayFilter。

修改gateway服务中的application.yml,添加一个defaultFilter:

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=origin,gateway
      routes:
       # ...略

这样,从gateway路由的所有请求都会带上origin头,值为gateway。而从其它地方到达微服务的请求则没有这个头。

4.1.4.配置授权规则

接下来,我们添加一个授权规则,放行origin值为gateway的请求。

image-20210716153250134

配置如下:

image-20210716153301069

现在,我们直接跳过网关,访问order-service服务:

image-20210716153348396

通过网关访问:

image-20210716153434095

4.2.自定义异常结果

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。

4.2.1.异常类型

而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:

public interface BlockExceptionHandler {
    /**
     * 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
     */
    void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}

这个方法有三个参数:

  • HttpServletRequest request:request对象
  • HttpServletResponse response:response对象
  • BlockException e:被sentinel拦截时抛出的异常

这里的BlockException包含多个不同的子类:

异常 说明
FlowException 限流异常
ParamFlowException 热点参数限流的异常
DegradeException 降级异常
AuthorityException 授权规则异常
SystemBlockException 系统规则异常

4.2.2.自定义异常处理

下面,我们就在order-service定义一个自定义异常处理类:

@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        String msg = "未知异常";
        int status = 429;

        if (e instanceof FlowException) {
            msg = "请求被限流了";
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
            status = 401;
        }

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
    }
}

重启测试,在不同场景下,会返回不同的异常消息.

限流:

image-20210716153938887

授权拦截时:

image-20210716154012736

5.规则持久化

现在,sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在生产环境下,我们必须确保这些规则的持久化,避免丢失。

5.1.规则管理模式

规则是否能持久化,取决于规则管理模式,sentinel支持三种规则管理模式:

  • 原始模式:Sentinel的默认模式,将规则保存在内存,重启服务会丢失。
  • pull模式
  • push模式

5.1.1.pull模式

pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。

image-20210716154155238

5.1.2.push模式

push模式:控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。

image-20210716154215456

5.2.实现push模式

详细步骤可以参考课后资料的《sentinel规则持久化》