SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)

SpringCloud:是一系列框架的有序集合,也是一套完整的微服务解决方案。利用SpringBoot的开发便利巧妙的简化了分布式系统基础设施的开发,如发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等都可以用SpringBoot的开发风格(屏蔽掉复杂的配置和实现原理)做到一键启动与部署,为开发者提供了一套简单易懂、易部署和易维护的分布式系统开发框架!— 摘自百度百科

SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)

1、微服务架构概述

1.1、微服务架构的提出

微服务架构:是一种新的架构形式。James Lewis 和 Martin Fowler 于2014年提出微服务完整概念。论文期刊地址:https://martinfowler.com/articles/microservices.html。

论文期刊中微服务提倡将单一应用程序划分为一组小的服务,服务之间互相协调,互相配合。为用户提供最终价值,每个服务运行在独立的进程中,服务与服务之间采用轻量级的通信机制互相协作!每个服务都围绕着具体的业务进行构建,并且能够独立的部署到生产环境中。另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言和工具进行构建与开发。

总结下来就是:

  1. 微服务是一种架构风格
  2. 一个应用拆分为一组小型服务
  3. 每个服务运行在自己的进程内,也就是可独立部署和升级
  4. 服务之间使用轻量级HTTP交互
  5. 服务围绕业务功能拆分
  6. 可以由全自动部署机制独立部署
  7. 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

因此为了实现上述效果!微服务架构技术栈已经演化成如下几种(也是我们学习的目标):
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第1张图片

1.2、微服务与与分布式的区别

简单理解就是:

1、分布式分散应用系统的压力。将不同模块部署在不同的服务器上,来减轻应用系统的处理压力。例如可以采用分布式解决网站高并发带来的问题!

2、微服务分散应用系统的能力。业务系统分解成多个独立的组件,每个组件都独立提供离散、自治、可复用的服务能力。

1.3、什么是SpringCloud?

了解了微服务与分布式的概念,那么什么又是SpringCloud?SrpingCloud又能做什么?

SpringCloud:分布式微服务架构的一站式解决方案!是多种微服务架构落地技术的集合体,俗称微服务全家桶!

SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第2张图片
如上图所示,SpringCloud扮演的角色就是"协调一切"。言外之意SpringCloud有成熟的分布式微服务架构解决方案!可以利用SpringCloud完成各服务之间互相协调、互相配合等。如下图
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第3张图片

1.4、SpringBoot与SpringCloud的版本选型

说到版本的适配关系,并没有统一标准,无论是SpringBoot还是SpringCloud版本一直都在迭代!并且两者之间存在一定的约束与依赖!需要严格参考官网给出的适配版本,我们不知道如何选择版本的时候,看一下即可!

官网地址:https://spring.io/projects/spring-cloud#overview

例如(2021-12-29查看):
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第4张图片
版本选型更详细的参考方式(访问此地址):https://start.spring.io/actuator/info
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第5张图片

2、基础环境搭建

学习微服务分布式技术,需要有一个业务场景和一个基础环境。我们学习SpringCloud技术栈也是从这个业务场景和基础环境上一步步扩充。然而整个环境的搭建过程颇为繁琐,直接给出视频地址,可自行搭建!

视频地址:https://www.bilibili.com/video/BV18E411x7eT?p=5

gitee完整代码地址:https://gitee.com/laizhenghua/spring-cloud

3、服务注册与发现

3.1、Eureka

了解Eureka之前我们先要了解注册中心的一些相关概念与知识!

1、服务治理

在传统的rpc远程调用框架中,管理每个服务与服务之间的依赖关系比较复杂,维护也不方便。所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等。实现服务的注册与发现。

2、服务注册与发现

在服务注册与发现中,有一个注册中心。当服务启动的时候,会把当前自己服务器的信息比如服务通讯地址等以别名的方式注册到注册中心上去。另一方(消费者/服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现RPC的调用。

RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关的信息或接口地址)

EurekaDubbo的架构对比
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第6张图片
3、Eureka

SpringCloud Euraka是Spring Cloud集合中一个组件,它是对Euraka的集成,用于服务注册和发现。Eureka是Netflix中的一个开源框架。它和 zookeeper、Consul一样,都是用于服务注册管理的,同样,Spring-Cloud 还集成了Zookeeper和Consul。关于Zookeeper和Consul后面介绍。

Eureka采用CSDE设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka客户端连接到Eureka Server并维持心跳连接,这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。

但是目前Eureka组件在官网中已停止更新,选择Eureka用于服务注册和发现,不是什么明智之举,但是一些思想还有设计理念还是值得我们去学习与了解。

4、构建Eureka Server

通过maven新建一个module,并引入如下依赖

pom.xml


<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>cloudartifactId>
        <groupId>com.laizhenghua.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloud_eureka_server7001artifactId>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
            <version>2.2.5.RELEASEversion>
        dependency>
        <dependency>
            <groupId>com.laizhenghua.springcloudgroupId>
            <artifactId>cloud_api_commonsartifactId>
            <version>${project.version}version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
    dependencies>
project>

application.yml

server:
  port: 7001
eureka:
  instance:
    hostname: 127.0.0.1 # eureka服务端实例名称
  client:
    register-with-eureka: false # false 不向注册中心注册自己
    fetch-registry: false # false 表示自己端就是注册中心,维护服务各服务实例,并不需要去检索服务
    service-url:
      # eureka 交互的服务查询地址,各服务都要依赖这个地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

依赖与配置文件搞定后,编写主程序类:

/**
 * @description: Eureka Server 主程序/7001
 * @date: 2022/1/2 11:51
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain.class, args);
    }
}

启动主程序,访问:http://127.0.0.1:7001/
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第7张图片
至此我们的Eureka服务端已经构建完成了。

5、构建Eureka Client

服务提供者需要有Eureka Client的依赖(starter),才能把服务注册到注册中心。在pom.xml中引入如下依赖:


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

修改application.yml

eureka:
  client:
    register-with-eureka: true # 是否注册自己的信息到EurekaServer,默认是true
    fetch-registry: true # 是否拉取其它服务的信息,默认是true
    service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:7001/eureka

主程序类添加@EnableEurekaClient注解。启动主程序测试Eureka Server有没有此服务实例。如下图效果
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第8张图片
以上就是使用Eureka作为服务注册中心,简单注册几个服务的过程。Eureka工作流程说明:
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第9张图片
6、Eureka 集群

以上Eureka Server 我们只搭建了一个7001端口的服务,显然是单机版的,如果此服务崩溃了,那么整个应用系统也将崩溃。所以使用Eureka作为服务注册与发现,都是集群部署,Eureka为我们提供了负载均衡、自我保护等等机制,可保证我们的应用系统稳定运行!

集群的搭建也和上述过程类似,就是多增加几个Eureka Server ,把配置文件改为集群部署模式。为了方便测试,一台电脑我们模拟为两台电脑,需要修改hosts文件

127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com

再使用maven新增一个module,module名称为cloud_eureka_server7002。pom.xml依赖与7001一样。
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第10张图片
修改application.yml文件,注意集群配置是相互注册、相互守望。因此服务地址要配另外一个的服务地址(例如7002服务的service-url就要配7001的地址)。7001也记得修改。

server:
  port: 7002
eureka:
  instance:
    hostname: eureka7002.com
  client:
    register-with-eureka: false # false 不向注册中心注册自己
    fetch-registry: false # false 表示自己就是注册中心
    service-url: 
      defaultZone: http://eureka7001.com:7001/eureka/

最后就是编写主程序并启动测试。7002指向了7001,如下图所示
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第11张图片
至此集群环境已经搭建完毕,接下来就是往服务里面注册服务(修改application.yml文件)!如下所示

eureka:
  client:
    register-with-eureka: true # 是否注册自己的信息到EurekaServer,默认是true
    fetch-registry: true # 是否拉取其它服务的信息,默认是true
    service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      # defaultZone: http://127.0.0.1:7001/eureka 单机版写法
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ # 集群版写法

测试是否注册成功!注册成功则可看到如下效果:
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第12张图片
7、Eureka 负载均衡

不仅Eureka Server 可以集群部署,Eureka Client 也可以集群部署,如以上支付模块cloud_payment_service8001我们还可部署一个cloud_payment_service8002。直接复制cloud_payment_service8001,配置文件端口改为8002。如下所示
在这里插入图片描述
为了区分consumer消费了那个服务,我们controller层代码新增获取端口代码:

@RestController
@RequestMapping(value = "/payment")
public class PaymentController {
    @Autowired
    private PaymentService paymentService;

    @Value("${server.port}") // 从配置文件获取端口
    private String serverPort;

    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public CommonResult<Integer> create(@RequestBody PaymentEntity entity) {
        Integer affectedRows = paymentService.create(entity);
        if (affectedRows > 0) {
            return new CommonResult<Integer>(200, "插入成功!serverPort:" + serverPort, affectedRows);
        }
        return new CommonResult<Integer>(444, "插入失败!", null);
    }

    @RequestMapping(value = "/getPaymentById/{id}", method = RequestMethod.GET)
    public CommonResult<PaymentEntity> getPaymentById(@PathVariable(value = "id", required = true) Integer id) {
        PaymentEntity entity = paymentService.getPaymentById(id);
        return new CommonResult<>(200, "查询成功!serverPort:" + serverPort, entity);
    }
}

服务提供者(支付8001/8002)修改完成后,还需修改(订单/80)服务消费者,使之支持负载均衡!修改方式如下:

  1. 更改服务调用地址:public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE/payment";
  2. 使用@LoadBalanced注解赋予RestTemplate负载均衡能力

SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第13张图片
重新访问多次http://127.0.0.1/consumer/payment/getPaymentById/1即可看到效果(端口交替出现)。备注:如果抛出Request URI does not contain a valid hostname,是因为服务别名使用下划线连接,在Eureka中服务别名如有由多个单词组成都要使用减号(-)连接。

SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第14张图片
8、Eureka 自我保护机制

概述:保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护,一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。

如果在Eureka Server的首页看到一下这段红色字体提示,则说明Eureka进入了保护模式:
在这里插入图片描述
总结下来就是:为了保证EurekaClient可以正常运行,某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存。属于CAP里面的AP分支高可用思想。

Eureka自我保护模式:默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络故障发生(延迟、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了。因为微服务本身其实是健康的,**此时不应该注销这个微服务。Eureka通过"自我保护模式"来解决这个问题。当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。

宗上,自我保护模式是一种应对网络异常的安全保护措施,它的架构哲学是宁可同时保留所有的微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。及时自我保护模式,可以让Eureka集群更加健壮、稳定。

自我保护机制默认是开启的,那么如何关闭呢?

application.yml

eureka:
  instance:
    hostname: eureka7001.com # eureka服务端实例名称
  client:
    register-with-eureka: false # false 不向注册中心注册自己
    fetch-registry: false # false 表示自己端就是注册中心,维护服务各服务实例,并不需要去检索服务
    service-url:
      # eureka 交互的服务查询地址
      defaultZone: http://eureka7002.com:7002/eureka/
  server:
    enable-self-preservation: false # false 关闭自我保护机制,保证不可用微服务及时剔除
    eviction-interval-timer-in-ms: 2000

目前Eureka已经停更!掌握这些已经足以!附上完整代码地址:https://gitee.com/laizhenghua/spring-cloud

3.2、Zookeeper

Zookeeper是一个分布式协调工具,也可以实现注册中心的功能。当时Eureka停更,zookeeper刚好成为了Eureka的代替品。

1、Zookeeper作为注册中心简单案例

docker构建zookeeper服务端:

# 拉取最新zookeeper镜像
docker pull zookeeper

# 启动zookeeper
docker run -d -p 2181:2181 --name zookeeper --restart always [imageId]

使用maven构建一个cloud_payment8003module!比添加如下依赖:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
    <version>2.2.3.RELEASEversion>
dependency>

编写配置文件

application.yml

server:
  port: 8003
spring:
  application:
    name: cloud-payment8003
  cloud:
    zookeeper:
      connect-string: 180.76.238.29:2181

编写主程序

@SpringBootApplication
@EnableDiscoveryClient // 该注解用于使用consul或者zookeeper作为注册中心时注册服务
public class Payment8003Main {
    public static void main(String[] args) {
        SpringApplication.run(Payment8003Main.class, args);
    }
}

启动主程序(确保zookeeper服务是启动的),然后在zookeeper服务中查看应用cloud-payment8003节点信息,如下图
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第15张图片
把应用注册到zookeeper,使用SpringBoot的开发风格也就这三步!但是这只是简单的注册进去,并没有完成一些高级的操作。要想完成服务之间的调用其实也不难,与上面介绍的Eureka调用方式类似,可以自行研究。

例如我们在zookeeper中注册了cloud-payment8003的服务。现在想调用此服务的某个方法我们可以这样书写:

@RestController
@RequestMapping(value = "/consumer")
public class OrderController {
    public static final String INVOKE_URL = "http://cloud-payment8003";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/getPaymentZk", method = RequestMethod.GET)
    public CommonResult<Map<String, Object>> getPaymentZkInfo() {
        return restTemplate.getForObject(INVOKE_URL + "/payment/zk", CommonResult.class);
    }
}

3.3、Consul

1、Consul简介

官网地址:https://www.consul.io/

Consul是一套开源的分布式服务发现和配置管理系统,由HashiCorp公司使用Go语言开发出来的一个产品。

Consul提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整地服务网格解决方案。

Consul具有很多优点。包括:基于raft协议,比较简介。支持健康检查,同时支持HTTP和DNS协议,也支持跨数据中心的WAN集群,提供图形界面,跨平台支持Linux、Mac、Windows。

Consul的主要功能:

  1. 服务发现:提供HTTP和DNS两种发现方式
  2. 健康监测:支持多种方式。HTTP、TCP、Docker、Shell脚本定制化等
  3. KV存储:key-value的存储方式
  4. 多数据中心:Consul支持多数据中心
  5. 可视化的Web界面

2、Consul安装与运行

官网或中午网都有详细介绍各个系统怎么安装与运行!可自行查看,这里主要以Docker进行部署!

# 查看有没有 consul
docker search consul

# 拉取 consul 容器镜像
docker pull consul

# 查看镜像
docker images

# 运行 consul 注意防火墙、服务器安全组的8500端口要开放
docker run -d -p 8500:8500 --restart always --name consul [imageId]

启动成功后,访问8500端口,即可看到如下效果:
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第16张图片
3、Consul完成服务注册与发现

还是基于以前的模式创建两个服务,一个服务是提供者(cloud_consul_payment8004),并添加几个又返回结果的方法!另一个是服务消费者(cloud_consul_order80),服务消费者执行服务提供者提供的方法,完成服务的注册与发现!

1、使用maven新建一个module名称为cloud_consul_payment8004,并添加如下依赖:

<dependency>
    <groupId>com.laizhenghua.springcloudgroupId>
    <artifactId>cloud_api_commonsartifactId>
    <version>${project.version}version>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-consul-discoveryartifactId>
    <version>2.2.5.RELEASEversion>
dependency>

2、cloud_consul_payment8004服务的application.yml

server:
  port: 8004
spring:
  application:
    name: cloud-consul-payment8004
  cloud:
    # consul 的配置
    consul:
      host: 180.76.238.29
      port: 8500
      discovery:
        service-name: ${spring.application.name}
        heartbeat:
          enabled: true

3、cloud_consul_payment8004服务的主程序

/**
 * @description: 
 * @date: 2022/1/3 19:00
 */
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8004 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain8004.class, args);
    }
}

4、至于controller层的方法,自己可以随便写点业务代码,能让服务消费者调用即可。

5、启动主程序,测试consul里面是否注册成功?
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第17张图片
至此服务提供者已经注册好了,下面搭建服务消费者,搭建方法类似。

6、使用maven新建一个module名称为cloud_consul_order80依赖与服务提供者一样。

7、cloud_consul_order80服务的application.yml

server:
  port: 80
spring:
  application:
    name: cloud-consul-order80
  cloud:
    # consul 的配置
    consul:
      host: 180.76.238.29
      port: 8500
      discovery:
        service-name: ${spring.application.name}
        heartbeat:
          enabled: true

8、编写主程序与RestTemplate的bean实例

/**
 * @description:
 * @date: 2022/1/3 19:32
 */
@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

9、编写服务之间调用的方法

/**
 * @description:
 * @date: 2022/1/3 19:37
 */
@RestController
@RequestMapping(value = "/consumer")
public class OrderController {
    public static final String INVOKE_URL = "http://cloud-consul-payment8004";
    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/hello")
    public String invokePaymentHelloMethod() {
       return restTemplate.getForObject(INVOKE_URL + "/payment/hello", String.class);
    }
}

10、测试
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第18张图片

3.4、3个组件的对比及CAP理论

关于微服务的服务注册与发现,我们已经学习了三个组件分别Eureka、Zookeeper、Consul。后面还有阿里的Nacos,Nacos也是我们学习的重点,因为Nacos经历了高并发考验,性能更加强悍!目前企业使用的也比较多。在学习Nacos之前先前面三个组件做个小结。

首先我们先要了解CPA理论。

  1. C:Consistency(强一致性)
  2. A:Availability(高可用性)
  3. P:Partition tolerance(分区容错性)

SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第19张图片
牢记CAP理论关注粒度是数据,而不是整体系统设计的策略。

CPA理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。

因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:

  1. CA:单点集群,满足一致性,可用性的系统,通常在可扩展上不太强大
  2. CP:满足一致性,分区容忍性的系统,通常性能不是特别高
  3. AP:满足可用性,分区容忍性的系统,通常可能对一致性要求低一些

了解CAP理论后,再来看这三个组件的一个对比图

组件名 语言 CAP 服务健康检查 对外暴露接口 SpringCloud集成
Eureka Java AP 配置支持 HTTP 已集成
Consul Go CP 支持 HTTP/DNS 已集成
Zookeeper Java CP 支持 客户端 已集成

4、服务调用

4.1、Ribbon

1、SpringCloud Ribbon

SpringCloud Ribbon:是基于Netflix Ribbon实现的一套客户端、负载均衡的工具。

简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时、重试等。例如配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。

官网地址:https://github.com/Netflix/ribbon

2、Ribbon 架构
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第20张图片
RIbbon在工作时分成两步:第一步选择EurekaServer,它优先选择同一个区域内负载较少的Server,第二步再根据用户指定的策略,在从Server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权等

LB负载均衡(Load Balance)补充:将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡软件还有Nginx。

Ribbon本地负载均衡与Nginx服务端负载均衡的区别:

  1. Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求,即负载均衡是由服务端实现的。
  2. Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表后缓存到JVM本地,从而在本地是RPC远程服务调用技术。

说明1:Ribbon调用机制是负载均衡 + RestTemplate完成服务的消费!!

说明2:Ribbon的坐标依赖,只要你引入以下坐标,即Ribbon将自动引入


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

可点击去查看,如下图
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第21张图片
说明3:RestTemplate常用的方法

@Configuration
public class ApplicationContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
/*
getForObject() 返回对象为响应体中数据转换成的对象,基本上可以理解为Json
getForEntity() 返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
*/

3、Ribbon 核心组件IRule

前面我们可以通过负载均衡 + RestTemplate完成Ribbon客户端服务的调用(负载均衡策略),但是Ribbon提供了多种调用策略例如轮询、随机等,我们想切换这种调用策略,又该怎么做呢?

我们可以使用RibbonIRule组件切换调用策略!IRule是一个接口,接口实现类封装了特定的选取算法,这些算法主要完成调用服务的选取!

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的接口实现类有

RoundRobinRule // 轮询
RandomRule // 随机
RetryRule // 先按照RoundRobinRule的策略获取服务,
WeightedResponseTimeRule // 对RoundRobinRule的扩展,响应速度越快的实例选取权重越大
BestAvailableRule // 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务
AvailabilityFilteringRule // 先过滤掉故障实例,再选择并发较小的实例
ZoneAvoidanceRule // 默认规则,复合判断server所在区域的性能和server的可用性选择

我们以切换为轮询策略(RoundRobinRule)为例,做个简单的切换示例。

1、还是选用前面书写的服务提供者与服务消费者,也就是说服务提供者是集群环境。

2、更改配置:官方文档明确给出了警告!这个自定义配置类不能放在@CompomentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到定制化的目的了。也就是说不能写在主程序所在的package,因为主程序的@SpringBootApplication注解包含@CompomentScan
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第22张图片
编写如下配置类:

package com.laizhenghua.ribbonRule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @description:
 * @date: 2022/1/15 11:20
 */
@Configuration
public class RibbonRule {
    @Bean
    public IRule roundRobinRule() {
        // 轮询选取策略
        return new RoundRobinRule();
    }
}

3、给主程序类添加@RibbonClient注解

/**
 * @description:
 * @date: 2022/1/1 19:55
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = {RibbonRule.class}) // 表示采用RibbonRule类配置的调用策略,调用CLOUD-PAYMENT-SERVICE
public class OrderMain {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain.class, args);
    }
}

4、启动服务提供者集群服务,调用方法,看是否能达到轮询效果
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第23张图片
4、负载均衡算法

负载均衡算法计算公式:REST接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标。每次服务重启后REST接口计数从1开始。注意这是非常重要的一个公式!

例如我们有8001和8002两个集群服务实例,集群总是为2:
在这里插入图片描述
轮询算法原理即为:

List<ServiceInstance> instanceList = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
/*
instanceList[0] = 127.0.0.1:8001
instanceList[1] = 127.0.0.1:8002

当总请求数为1时:1 % 2 = 1 对应的下标为1,获得服务地址为 127.0.0.1:8002
当总请求数为2时:2 % 2 = 0 对应的下标为0,获得服务地址为 127.0.0.1:8001
当总请求数为3时:3 % 2 = 1 对应的下标为1,获得服务地址为 127.0.0.1:8002

以此类推。。。
*/

轮询调用策略(RoundRobinRule.java)源码分析

// 由RoundRobinRule类下的此方法完成调用服务的选举(轮询策略算法)
public Server choose(ILoadBalancer lb, Object key) {
     if (lb == null) {
         log.warn("no load balancer");
         return null;
     }

     Server server = null;
     int count = 0;
     while (server == null && count++ < 10) {
         List<Server> reachableServers = lb.getReachableServers(); // 获取所有可调用服务
         List<Server> allServers = lb.getAllServers(); // 获取所有服务
         int upCount = reachableServers.size();
         int serverCount = allServers.size();

         if ((upCount == 0) || (serverCount == 0)) {
             log.warn("No up servers available from load balancer: " + lb);
             return null;
         }

         int nextServerIndex = incrementAndGetModulo(serverCount); // 通过轮询策略获取需要调用的服务列表下标
         server = allServers.get(nextServerIndex);

         if (server == null) {
             /* Transient. */
             Thread.yield();
             continue;
         }

         if (server.isAlive() && (server.isReadyToServe())) {
             return (server);
         }

         // Next.
         server = null;
     }

     if (count >= 10) {
         log.warn("No available alive servers after 10 tries from load balancer: "
                 + lb);
     }
     return server;
 }
 
 // 轮询策略实现逻辑
private int incrementAndGetModulo(int modulo) {
    for (;;) {
    	// 关于这个变量nextServerCyclicCounter,可查看RoundRobinRule类的无参构造方法
        int current = nextServerCyclicCounter.get(); // 初始值为0,服务重启后也会重新初始化为0
        int next = (current + 1) % modulo;
        if (nextServerCyclicCounter.compareAndSet(current, next)) // 自旋锁!current与next比较并交换
            return next;
    }
}

前面我们说我们很容易使用Ribbon实现自定义的负载均衡 / 轮询算法!我想到这里你应该明白了,实现的步骤也就是编写一个我们自己的IRule接口实现类(配置类),choose方法封装自定义的选举代码!然后给主程序添加上@RibbonClient即可。

例如:

@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = {RibbonRule.class})

4.2、OpenFeign

4.2.1、OpenFeign概述

Feign是一个声明式的WebService客户端。使用Feign能让编写WebService客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。并且SpringCloudFeign进行了封装,也就是OpenFeign。封装后使其支持了SpringMVC标准注解和HttpMessageConverters。当然也可以与EurekaRibbon组合使用以支持负载均衡。

Feign(停更) OpenFeign
Feign是SpringCloud组件中的一个轻量级RestFul的HTTP服务客户端。Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 OpenFeign是SpringCloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等。OpenFeign@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

Feign场景启动器


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-feignartifactId>
    <version>1.4.7.RELEASEversion>
dependency>

OpenFeign场景启动器


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

4.2.2、OpenFeign能干什么?

简单来说就是使编写Java Http客户端变得更加容易!

前面在使用Ribbon + RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于服务依赖的调用可能不止一处!往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用

所以OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在OpenFeign的实现下,我们只需要创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,与Mapper注解类似现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用SpringCloud Ribbon时,自动封装服务调用客户端的开发量。

所以说OpenFeign是集成了Ribbon,利用Ribbon维护了服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过OpenFeign只需要定义服务绑定接口却以声明式的方法,优雅而简单的实现了服务调用。

4.2.3、OpenFeign服务调用

我们还是基于前面支付与订单模块为例,完成OpenFeign服务的调用。

1、使用maven或初始化向导新建订单模块(cloud_feign_order80),集群服务为7001和7002,如下图所示
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第24张图片
并引入OpenFeign的场景启动器

pom.xml

<dependency>
    <groupId>com.laizhenghua.springcloudgroupId>
    <artifactId>cloud_api_commonsartifactId>
    <version>${project.version}version>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>

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

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

2、编写配置文件application.yml文件

server:
  port: 80
eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

3、编写主程序

/**
 * @description:
 * @date: 2022/2/11 21:21
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableFeignClients
public class OrderMain {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain.class, args);
    }
}

4、编写业务类(消费服务)注意我们的业务一定是集群环境

7001与7002服务的源码,上面基础环境搭建目录有获取地址!
在这里插入图片描述
service包下新增PaymentFeignService接口并添加新注解@FeignClient

/**
 * @description: OpenFeign接口
 * @date: 2022/2/11 21:25
 */
@Component(value = "paymentFeignService")
@FeignClient(name = "PAYMENT-SERVER") // 就是我们向注册中心注册的服务名称
@RequestMapping(path = "/payment")
public interface PaymentFeignService {
    @RequestMapping(path = "/getPaymentById/{id}", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
    public CommonResult<PaymentEntity> getPaymentEntityById(@PathVariable(value = "id") Integer id);
}

5、控制层编写调用方法

/**
 * @description: order控制层
 * @date: 2022/2/11 22:23
 */
@RestController
@RequestMapping(path = "/payment")
public class OrderController {
    @Autowired
    private PaymentFeignService paymentFeignService;

    @RequestMapping(path = "/getById/{id}", method = RequestMethod.GET)
    public CommonResult<PaymentEntity> getPaymentById(@PathVariable(value = "id") Integer id) {
        return paymentFeignService.getPaymentEntityById(id);
    }
}

6、测试,OpentFeign自带负载均衡配置项
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第25张图片

4.2.4、OpenFeign的超时控制

服务的调用,一定存在一个超时时间,比如服务一直得不到响应,我们不可能一直等待!遇到这种情况一定是需要做出特定的反馈!因此通过OpenFeign调用服务时,也允许我们设置一个服务调用的超时时间!

我们先来做个小实验!服务提供者新增一个暂停1分钟的接口,我们在通过OpenFeign去调用这个接口。

1、7001和7002服务编写暂停接口

@RequestMapping(path = "/timeoutTest", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
public CommonResult<String> timeoutTest() {
    try {
        Thread.sleep(60000);
    } catch (Exception e) {
        log.error(e.getMessage());
    }
    return new CommonResult<>(200, "timeout test", "sleep 1 minute");
}

2、订单80服务新增调用超时的接口的方法

/**
 * @description:
 * @date: 2022/2/11 21:25
 */
@Component(value = "paymentFeignService")
@FeignClient(name = "PAYMENT-SERVER")
@RequestMapping(path = "/payment")
public interface PaymentFeignService {
    @RequestMapping(path = "/getPaymentById/{id}", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
    public CommonResult<PaymentEntity> getPaymentEntityById(@PathVariable(value = "id") Integer id);

    @RequestMapping(path = "/timeoutTest", method = RequestMethod.GET, produces = {"application/json;charset=UTF-8"})
    public CommonResult<String> timeoutTest(); // 注意OpenFeign(Ribbon)的默认超时时间是1s
}

3、控制层编写调用方法

/**
 * @description:
 * @date: 2022/2/11 22:23
 */
@RestController
@RequestMapping(path = "/payment")
public class OrderController {
    @Autowired
    private PaymentFeignService paymentFeignService;

    @RequestMapping(path = "/getById/{id}", method = RequestMethod.GET)
    public CommonResult<PaymentEntity> getPaymentById(@PathVariable(value = "id") Integer id) {
        return paymentFeignService.getPaymentEntityById(id);
    }

    @RequestMapping(path = "/timeoutTest", method = RequestMethod.GET)
    public CommonResult<String> timeoutTest() {
        return paymentFeignService.timeoutTest();
    }
}

4、启动测试
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第26张图片
默认OpenFeign客户端只等待一秒钟!但是服务端处理需要一分钟,导致OpenFeign客户端不想等待了,直接返回报错!为了避免这样的情况,有时候我们需要设置OpenFeign客户端的超时时间!

设置OpenFeign客户端的超时时间,需要在yaml文件中开启配置,例如

# 设置OpenFeign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  # 建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 70000
  # 建立连接后从服务端读取到可用资源所用的时间,这里设置了可等待70s
  ConnectTimeout: 70000

再次重启测试!

4.2.5、OpenFeign的日志增强功能

OpenFeign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中HTTP请求的细节。说白了就是对Feign接口的调用情况进行监控和输出!!

Feign的日志级别

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

关于Feign日志级别的配置,我们可以选择配置yaml文件,也可以使用配置类的形式配置日志级别!!(比较推荐编写配置类)

package com.laizhenghua.order.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @description: OpenFeign的配置
 * @date: 2022/2/12 12:11
 */
@Configuration
public class OpenFeignConfig {
    @Bean
    public Logger.Level openFeignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

编写完配置类后,还需要告诉OpenFeign以什么级别监控哪个接口

application.yaml

logging:
  level:
    com.laizhenghua.order.service.PaymentFeignService: debug

重启测试即可
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第27张图片

5、服务降级与熔断

5.1、Hystrix

Hystrix官宣,停止维护与更新!!!但是它一些设计理念非常的优秀任值得我探究!

5.1.1、分布式系统面临的问题

复杂分布式系统中应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败!
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第28张图片
互相调用的时候,如果都没有出现故障,那么没有什么问题。假如C服务出现了超时!整条链路就会调用失败,最终导致服务雪崩

服务雪崩

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C有调用其他的微服务(调用链越来越长),这就是所谓的扇出。如果扇出的链路上的某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,这就叫雪崩效应

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒内饱和,比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列、县城和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统的调用失败!

通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障或者叫雪崩。

5.1.2、Hystrix概述

Hystrix是一个用于处理分布式系统的延迟容错的开源库,在分布式系统里,许多依赖不可避免的会出现调用失败情况,比如超时、异常等。Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝)向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

官方地址:https://github.com/Netflix/Hystrix
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第29张图片

5.1.3、Hystrix重要概念

1、服务降级(fallback)

服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fullback。那么那些情况会发出降级呢?

一般来说,如下四种情况会出现服务降级:

  • 程序运行异常
  • 超时
  • 服务熔断触发服务降级
  • 线程池/信号量打满也会导致服务降级

2、服务熔断(break)

就是保险丝断了。达到最大服务访问后,直接拒接访问,拉闸限电保护机器安全!然后调用服务降级的方法并返回友好提示。

3、服务限流(flowlimit)

秒杀高并发等操作,严禁一窝蜂的过来涌进来!大家排好队,一秒钟N个,有序进行。

5.1.4、Hystrix实验环境搭建1

基础环境搭建

1、使用maven或初始化向导新建cloud_hystrix_payment8001模块,并引入SpringCloud Hystrix相关依赖

 <dependencies>
    <dependency>
        <groupId>com.laizhenghua.springcloudgroupId>
        <artifactId>cloud_api_commonsartifactId>
        <version>${project.version}version>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        <version>2.2.5.RELEASEversion>
    dependency>
dependencies>

后续需要什么引入什么即可!

2、编写配置文件,注意以7001位注册中心

application.yml

spring:
  application:
    name: cloud-payment-8001
server:
  port: 8001
eureka:
  client:
    register-with-eureka: true # false 不向注册中心注册自己
    fetch-registry: true # false 表示自己就是注册中心
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

3、编写测试方法

/**
 * @description:
 * @date: 2022/2/12 17:16
 */
@Slf4j
@RestController
@RequestMapping(path = "/payment")
public class PaymentController {
    /**
     * 正常方法(不会超时,也不会出现异常)
     * @return R
     */
    @RequestMapping(path = "/testOk", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public R testOk() {
        Map<String, String> data = new HashMap<>();
        data.put("threadName", Thread.currentThread().getName());
        data.put("methodName", "testOk");
        return R.ok().put("data", data);
    }

    /**
     * 异常方法(出现超时或异常)
     * @return R
     */
    @RequestMapping(path = "/testError", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public R testError() {
        Map<String, String> data = new HashMap<>();
        data.put("threadName", Thread.currentThread().getName());
        data.put("methodName", "testError");
        try {
            Thread.sleep(3000); // 3s
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return R.ok().put("data", data);
    }
}

以上述环境为基础,演示Hystrix服务降级与熔断!!

5.1.5、Jmeter压力测试

1、打开Jmeter压力测试工具,设置线程组参数
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第30张图片
2、设置HTTP参数
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第31张图片
3、启动测试

测试要达到的效果是什么呢?我们使用40000个线程,去访问http://127.0.0.1:8001/payment/testError接口。开启200次循环后,我们再次通过浏览器访问http://127.0.0.1:8001/payment/testOk接口,发现这个接口变慢了,响应时间延迟至1~3秒了!!

4、结论

上面测试只是服务提供者8001自测!并没有消费者来消费服务,假如外部的消费者也来访问,那消费者在高并发场景下是及时得到不到响应的!最终导致消费者不满意,服务端8001直接被拖死。因此必须要有Hystrix这种组件对我们系统做服务降级、服务熔断、服务限流等操作,保证我们的系统安全可靠!

5.1.6、Hystrix实验环境搭建2

为了准确演示Hystrix组件进行服务降级、服务熔断、服务限流操作,我们新增服务消费者,进一步恶化高并发场景下,8001服务处理接口效果。

需要注意的是Hystrix可以用在客户端也可以用在服务端里面。

1、使用maven或初始化向导新建cloud_feign_hystrix_order80模块,用于消费8001提供的服务!此模块的pom.xml文件依赖如下。

<dependencies>
	<dependency>
	    <groupId>com.laizhenghua.springcloudgroupId>
	    <artifactId>cloud_api_commonsartifactId>
	    <version>${project.version}version>
	dependency>
	<dependency>
	    <groupId>org.springframework.bootgroupId>
	    <artifactId>spring-boot-starter-webartifactId>
	dependency>
	
	<dependency>
	    <groupId>org.springframework.cloudgroupId>
	    <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
	dependency>
	
	<dependency>
	    <groupId>org.springframework.cloudgroupId>
	    <artifactId>spring-cloud-starter-openfeignartifactId>
	dependency>
	
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
    dependency>
dependencies>

2、编写配置文件

application.yml

server:
  port: 80
spring:
  application:
    name: order
eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
ribbon:
  eureka:
    enabled: true
  ReadTimeout: 4000
  ConnectTimeout: 4000

3、编写主程序

/**
 * @description:
 * @date: 2022/2/13 10:01
 */
@EnableFeignClients
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class OrderMain {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain.class, args);
    }
}

4、编写测试业务类

/**
 * @description: OpenFeignService
 * @date: 2022/2/13 10:07
 */
@Component(value = "paymentFeignService")
@FeignClient(value = "CLOUD-PAYMENT-8001")
@RequestMapping(path = "/payment")
public interface PaymentFeignService {
    @RequestMapping(path = "/testOk", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public R testOk();

    @RequestMapping(path = "/testError", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public R testError();
}

5、控制层调用方法编写

/**
 * @description: orderController
 * @date: 2022/2/13 10:12
 */
@Slf4j
@RestController
@RequestMapping(path = "/order")
public class OrderController {

    @Autowired
    private PaymentFeignService paymentFeignService;

    @RequestMapping(path = "/testOk", method = RequestMethod.GET)
    public R testOk() {
        return paymentFeignService.testOk();
    }

    @RequestMapping(path = "/testError", method = RequestMethod.GET)
    public R testError() {
        return paymentFeignService.testError();
    }
}

6、测试

至此,服务消费者模块已经搭建完毕!我们如何测试效果呢?

首先,8001服务提供者必须是高并发场景下!上面Jmeter压力测试已经准备好了。然后再高并发场景下,我们使用服务消费者也就是cloud_feign_hystrix_order80,去访问我们所写的接口!看看会有效果发生呢??

我们发现,高并发场景下消费者消费服务的时候,会被拖慢也就是响应时间变长极端情况下还会报超时错误!

小结:

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)没有啥问题,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者)自己处理降级。

5.1.7、服务降级

除了配置服务降级,我们也可以从接口自身上找问题,也就是优化接口!比如设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有特定的处理机制!作为服务降级fallback。这里大家可以作为扩展知识去思考,例如@Access(auth = false, limitTime = 240)注解的使用等

服务端(8001)服务降级之超时的配置

/**
 * @description:
 * @date: 2022/2/14 20:20
 */
@Slf4j
@Service(value = "paymentService")
public class PaymentServiceImpl implements PaymentService {
    @Override
    @HystrixCommand(fallbackMethod = "paymentServiceTimeoutHandler", commandProperties = {
            // 3s以内走正常业务方法,超过3s走fallback
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    }) // 超时设置
    public Map<String, Object> testError() {
        Map<String, Object> data = new HashMap<>();
        data.put("threadName", Thread.currentThread().getName());
        data.put("methodName", "testError");
        try {
            Thread.sleep(5000); // 5s
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        // int a = 10 / 0; // 注意此行代码:程序出现异常也能触发超时配置的fallback
        log.info("method [testError] is running");
        return data;
    }

    public Map<String, Object> paymentServiceTimeoutHandler() {
        Map<String, Object> result = new HashMap<>();
        result.put("message", "系统正在努力运行,请稍等...");
        return result;
    }
}

主程序新增@EnableCircuitBreaker注解

/**
 * @description: 主程序类
 * @date: 2022/2/12 17:13
 */
@EnableEurekaClient
@EnableCircuitBreaker // 激活 hystrix 的功能
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class HystrixMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixMain8001.class, args);
    }
}

我们testError()睡眠时间改为了5s,而fallback触发时间设置了3s以后,启动测试看看会不会走fallback
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第32张图片
服务端(8001)服务降级之down机的配置

我们的Hystrix很强大,上面配置的超时fallback,程序出现异常后也能触发!!例如

/**
 * @description:
 * @date: 2022/2/14 20:20
 */
@Slf4j
@Service(value = "paymentService")
public class PaymentServiceImpl implements PaymentService {
    @Override
    @HystrixCommand(fallbackMethod = "paymentServiceTimeoutHandler", commandProperties = {
            // 3s以内走正常业务方法,超过3s走fallback
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public Map<String, Object> testError() {
        Map<String, Object> data = new HashMap<>();
        data.put("threadName", Thread.currentThread().getName());
        data.put("methodName", "testError");
        int a = 10 / 0; // 注意此行代码
        log.info("method [testError] is running");
        return data;
    }

    public Map<String, Object> paymentServiceTimeoutHandler() {
        Map<String, Object> result = new HashMap<>();
        result.put("message", "系统正在努力运行,请稍等...");
        return result;
    }
}

客户端(80)的服务降级

前面我们说过,除了服务端可以使用Hystrix客户端也可以使用!也可以更好的保护自己,自己也可以照葫芦画瓢进行客户端降级保护。

1、编写配置文件,使客户端支持Hystrix

feign:
  hystrix:
    enabled: true

2、主程序激活Hystrix的功能,添加@EnableHystrix

/**
 * @description:
 * @date: 2022/2/13 10:01
 */
@EnableHystrix
@EnableFeignClients
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class OrderMain {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain.class, args);
    }
}

3、给客户端方法配置处理超时、异常的fallback

@Slf4j
@RestController
@RequestMapping(path = "/order")
public class OrderController {

    @Autowired
    private PaymentFeignService paymentFeignService;

    @RequestMapping(path = "/testOk", method = RequestMethod.GET)
    public R testOk() {
        return paymentFeignService.testOk();
    }

    @RequestMapping(path = "/testError", method = RequestMethod.GET)
    @HystrixCommand(fallbackMethod = "orderServiceTimeoutAndExceptionHandler", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
    })
    public R testError() {
        return paymentFeignService.testError();
    }

    public R orderServiceTimeoutAndExceptionHandler() {
        return R.error(1000, "系统正在努力处理,请稍等...");
    }
}

测试
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第33张图片
Hystrix的全局服务降级defaultFallback

全局的服务降级配置主要解决,代码冗余!假设每个业务方法都配置一个fallback,代码只会越来越多!越来越远膨胀!

因此为了统一的fallback和自定义的fallback分开,Hystrix提供了注解@DefaultProperties@HystrixCommanddefaultFallback属性用于配置全局的fallback

@Slf4j
@RestController
@RequestMapping(path = "/order")
@DefaultProperties(defaultFallback = "orderServiceTimeoutAndExceptionHandler")
public class OrderController {
	@RequestMapping(path = "/getSysDate", method = RequestMethod.GET)
    @HystrixCommand // 此业务方法没有指定fallback 就用全局的 orderServiceTimeoutAndExceptionHandler
    public R getSysDate() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date = sdf.format(new Date());
        int a = 10 / 0; // 让业务方法出现异常,测试全局fallback
        return R.ok().put("sysDate", date);
    }
}

也就是说@HystrixCommand注解指定自己的fallback就会走自己的不会走全局默认的!!

测试
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第34张图片
通配服务降级feignFallback

服务的调用我们使用OpenFeign组件比较多,因此SpringCloud设计者也想到了Feign接口的服务降级!我们可以编写一个统一处理Feign接口服务降级的类,去实现程序的解耦!前面的处理方式,把业务代码和fallback写在一起了,程序耦合度比较高。

实现的方法就是两步。1、编写feign接口的接口实现类!作为统一处理服务降级的类。2、在注解@FeignClient配置接口实现类为fallback。例如

/**
 * @description: Feign接口实现类
 * @date: 2022/2/14 22:35
 */
@Component
public class PaymentFallbackServiceImpl implements PaymentFeignService {

    @Override
    public R testOk() {
    	// 这里可以自由发挥 
        return R.error(1000, "系统正在努力处理,请稍等...");
    }

    @Override
    public R testError() {
    	// 这里可以自由发挥 
        return R.error(1000, "系统正在努力处理,请稍等...");
    }
}

配置接口实现类为fallback

/**
 * @description: OpenFeignService
 * @date: 2022/2/13 10:07
 */
@Component(value = "paymentFeignService")
@FeignClient(name = "CLOUD-PAYMENT-8001", fallback = PaymentFallbackServiceImpl.class)
// @RequestMapping(path = "/payment")
public interface PaymentFeignService {
    @RequestMapping(path = "/payment/testOk", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public R testOk();

    @RequestMapping(path = "/payment/testError", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public R testError();
}

这样一来,接口实现类里的方法就成了fallback,以后我们想修改fallback就不需要更改业务代码!也就是说就算服务端已经down机了,我们做了feign的服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器。

5.1.8、服务熔断

熔断机制概述

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。

当检测到该节点微服务调用响应正常后,恢复调用链路。
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第35张图片
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand

服务熔断配置示例:

/**
 * @description:
 * @date: 2022/2/12 17:16
 */
@Slf4j
@RestController
@RequestMapping(path = "/payment")
public class PaymentController {
    @Autowired
    private PaymentService paymentService;
   
    @HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") // 失败率阈值 %60
    })
    @RequestMapping(path = "/circuitBreaker/{id}", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public R paymentCircuitBreaker(@PathVariable(value = "id") Integer id) {
        if (id < 0) {
            throw new RuntimeException("id 不能为负数");
        }
        // https://www.hutool.cn/
        String uuid = IdUtil.simpleUUID(); // String uuid = UUID.randomUUID().toString().replace("-", "");
        return R.ok().put("uuid", uuid);
    }
    
    public R paymentCircuitBreakerFallback(Integer id) {
        return R.error(1000, id + " 不能为负数!");
    }
}

启动测试

id > 0
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第36张图片
id < 0
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第37张图片
那么如何测试服务熔断效果呢?首先让id小于0!快速请求10次以上,然后快速切换id大于0的请求。

经测试发现,当触发服务熔断后,我们快速切换正确请求时,还是给我们响应fallback的内容!
SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第38张图片
总结:

多次错误达到设定的阈值,触发服务熔断!然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行。

服务熔断会经历三个节点!分别是熔断打开、熔断半开、熔断关闭

  • 熔断打开(open):请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设定时钟则进入半熔断状态。
  • 熔断半开(half-open):部分请求根据规则调用当前服务,如果请求成功且符合规则认为当前服务恢复正常,关闭熔断
  • 熔断关闭(closed):熔断关闭不会对服务进行熔断

SpringCoud - 基础入门(服务注册与发现、服务调用、服务降级与熔断篇)_第39张图片
断路器的开启条件或流程梳理:

  1. 当满足一定阈值的时候(默认10秒内超过20个请求次数)
  2. 当失败率达到一定阈值的时候(默认10秒内超过50%的请求失败)
  3. 当达到以上阈值,断路器将会开启,当开启的时候,所有请求都不会进行转发
  4. 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发,如果成功,断路器会关闭,若失败继续开启。重复第4步。

5.2、Sentinel

未完待续!Sentinel属于SpringCloud Alibaba开源组件:

详见:https://blog.csdn.net/m0_46357847/article/details/123142004

END

THANK YOU

END

你可能感兴趣的:(JavaEE,spring,boot,java,spring,cloud)