Spring cloud入门-11:服务降级-Hystrix
1、Hystrix入门介绍
1.1 分布式系统面临的问题
前面已经构建了erureka集群(7001,7002)、服务提供模块集群(8001,8002)、和消费服务模块(80)的简单demo。
但是在实际的分布式系统中,一般不是和demo这样简单的调用依赖关系,而是多级的服务调用关系,但是这些服务之间的相互调用在某些时候不可避免地会出现失败。
比如微服务A调用微服务B和微服务C,微服务B又调用微服务D,微服务C又调用微服务E。如果微服务D的调用时间过长或者已经不可用了。此时,A调用C,C调用D,在D这里调用时间长或不可用的话,会导致该服务其实一致没有返回。但是在高流量的情况下,微服务A还会继续接收到流量,此时前面的服务都还没有处理完响应。因此对微服务A的持续调用就会占用越来越多的系统资源,导致延迟增加,备份队列,线程和其他系统资源紧张,从而导致整个系统发生更多的级联故障,进而引起系统崩溃,发生所谓的“雪崩效应”。
1.2 Introduction to Hystrix
针对上面分布式系统中遇到的问题,本节介绍Hystrix。首先访问官网,看下介绍:https://github.com/Netflix/Hystrix。
Hystrix 是一个用于处理分布式系统的延迟(latency)和容错(tolerance)的开源库,旨在隔离对远程系统、服务和第 3 方库的访问点,停止级联故障(cascading failure),使故障不可避免的复杂分布式系统具有弹性(enable resilience)。
也就是当某个服务发生故障后,Hystrix可以向调用方返回一个预期的、可处理的备选响应(FallBack),而不是长时间等待或者抛出调用方法无法处理的异常,从而保证服务调用方的线程不会长时间地占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。
2、构建服务提供模块8001
还是和之前一样,构建注册进eureka集群的服务提供模块8001。
服务降级主要在以下4种情况下会发生:
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满导致服务降级
2.1 业务类新增异常返回
在服务提供模块(8001)中的查看订单的业务逻辑中,如果id为小于等于0,那么就抛出异常。
package com.example.springcloud.controller;
import com.example.springcloud.entities.CommonResult;
import com.example.springcloud.entities.Payment;
import com.example.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment) {
int result = paymentService.create(payment);
log.info("插入结果" + result);
if (result > 0) {
return new CommonResult(200, "插入数据库成功, serverPort: " + serverPort, result);
} else {
return new CommonResult(500, "插入数据失败, serverPort: " + serverPort);
}
}
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) throws Exception{
if (id<=0) {
throw new Exception("订单编号必须大于0");
}
Payment payment = paymentService.getPaymentById(id);
log.info("读取订单: " + payment);
if (payment==null) {
return new CommonResult(500, "未查询到该订单,查询id: " + id);
} else {
return new CommonResult(200, "查询成功, serverPort: " + serverPort, payment);
}
}
@GetMapping(value = "/payment/feign/timeout")
public String PaymentFeignTimeout() {
// 模拟较长时间的业务处理,时间为3秒
int timeOut = 3;
try {
TimeUnit.SECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "长时间业务处理:" + serverPort;
}
}
3、构建服务消费模块80
3.1 建module
构建一个包含服务降级hystrix的服务消费模块:cloud-consumer-order-feign-hystrix-80。改模块的环境构建可参考cloud-consumer-order-feign-80工程。
3.2 改pom
依赖包新增hystrix:spring-cloud-starter-netflix-hystrix。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2021</artifactId>
<groupId>org.example.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-order-feign-hystrix-80</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- 导入 eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!-- 导入公共包-->
<dependency>
<groupId>org.example.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- spring boot web 依赖模块:web, actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.3 写yml
在配置文件中开启feign-hystrix属性。
server.port=8080
spring.application.name=cloud-order-service
# 添加Eureka Client配置
# 表示是否将自己注册进Eureka Server,默认为true
eureka.client.register-with-eureka=true
# 表示是否从Eureka Server抓取已有的注册信息,默认为true。
# 该配置在单节点时无所谓,集群配置时必须设置为true,才能配合ribbon使用负载均衡
eureka.client.fetchRegistry=true
#eureka.client.service-url.defaultZone=http://localhost:7001/eureka # 单机版
eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
# 建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ribbon.CoonecTimeout=5000
# 建立连接后从服务器读取到可用资源所用的时间
ribbon.ReadTimeout=5000
# 指定feign日志以什么级别监控哪个接口
logging.level.com.example.springcloud.service.PaymentFeignService=debug
# 开启feign-hystrix属性
feign.hystrix.enabled=true
3.4 主启动
启动类上加上启动Hystrix的注解:@EnableHystrix
package com.example.springcloud;
import cn.hutool.db.sql.Order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
3.5 业务类
构建openfeign支付服务接口,直接复制服务提供模块的controller即可。
两个接口:
1)getPaymentById:根据id查询订单
2)PaymentFeignTimeout:长时间业务处理接口,业务处理时间为3秒
package com.example.springcloud.service;
import com.example.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Component
@FeignClient(value = "cloud-payment-service")
public interface PaymentHystrixService {
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id);
@GetMapping(value = "/payment/feign/timeout")
public String PaymentFeignTimeout();
}
构建订单服务controller,复制PaymentHystrixService即可,接口路径上添加/consumer表示是服务消费接口。
针对服务端可能出现的长时间调用或者服务异常的情况,给每个接口添加对应的服务降级方法。
当服务2秒后未返回,或者服务发生异常,80服务接口直接调用备选方法(fallback method)返回。
package com.example.springcloud.controller;
import com.example.springcloud.entities.CommonResult;
import com.example.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping(value = "/consumer/payment/get/{id}")
@HystrixCommand(
fallbackMethod = "getPaymentByIdFallbackMethod",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
}
)
public CommonResult getPaymentById(@PathVariable("id") Long id) {
return paymentHystrixService.getPaymentById(id);
}
@GetMapping(value = "/consumer/payment/feign/timeout")
@HystrixCommand(
fallbackMethod = "PaymentFeignTimeoutFallbackMethod",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
}
)
public String PaymentFeignTimeout() {
return paymentHystrixService.PaymentFeignTimeout();
}
// getPaymentById接口的服务降级方法
public CommonResult getPaymentByIdFallbackMethod(@PathVariable("id") Long id) {
String msg = "getPaymentById接口繁忙,请稍后再试,当前处理线程:" + Thread.currentThread().getName();
return new CommonResult(500, msg);
}
// PaymentFeignTimeout接口的服务降级方法
public String PaymentFeignTimeoutFallbackMethod() {
return "PaymentFeignTimeout接口繁忙,请稍后再试,当前处理线程:" + Thread.currentThread().getName();
}
}
3.6 测试
3.6.1 测试查看订单接口:getPaymentById
首先测试没有异常的情况,也就是id大于0的情况:http://localhost:8080/consumer/payment/get/3
然后测试有异常的情况,也就是id小于等于0的情况:http://localhost:8080/consumer/payment/get/-3。
可以看到当服务提供接口发生异常的时候,80接口直接返回备选方法的返回结果,而不会返回服务提供接口的异常返回信息。
3.6.2 测试长时间业务处理接口:PaymentFeignTimeout
由于在8001接口中,我们设置的长时间业务处理有3秒。而在服务消费模块80的controller这里设置的超时时间为2s。因此调用消费服务模块的接口一定会返回备选方法的返回值。
服务消费接口:http://localhost:8080/consumer/payment/feign/timeout
根据上面的测试,我们可以得到,当服务异常或者超时的情况下,可以发生服务降级,也就是返回该服务的备选方法的结果,避免程序发生级故障,导致系统崩溃。