前文
SpringCloud —— 服务注册进 Eureka 集群
SpringCloud —— SpringCloud Consul 实现服务注册中心
高并发测试
在进行高并发测试之前,先来看下单个请求的,可以发现,请求 http://localhost:8001/payment/hystrix/ok/1
的时候响应很快
使用 JMeter 工具模仿高并发,设置 20000 个并发去访问 8001,20000 个请求都是访问 http://localhost:8001/payment/hystrix/timeout/1
然后再去访问 http://localhost:8001/payment/hystrix/ok/1
先清空控制台
启动 JMeter 工具,设置好 200 个线程,循环 100 次,也就是 20000 次请求
启动高并发访问
在使用高并发测再去访问的时候可以发现访问 http://localhost:8001/payment/hystrix/ok/1
的时候也需要等待了
控制台输出:可右边的滚动条可以发现 timeout 确实接收到了很多的请求
为什么会卡死
也就是说,当一大波请求去访问 http://localhost:8001/payment/hystrix/timeout/1 的时候,http://localhost:8001/payment/hystrix/ok/1 也被拖慢了
tomcat 的默认的工作线程数被打满了( Windows 每个进程中的线程数不允许超过 2000),没有多余的线程来分解压力和处理
上面仅仅是服务提供者 8001 自己测试,假如此时外部的消费者 80 也来访问,那么消费者只能干等,最终导致消费者 80 不满意,服务端 8001 直接崩了
新建 Module 80(消费者)
添加依赖
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix-->
<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>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web-->
<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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
</dependencies>
编写 application.yml 文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
编写启动类
package com.java.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author Woo_home
* @create 2020/3/28 13:59
*/
@SpringBootApplication
@EnableFeignClients
public class HystrixOrderMain80 {
public static void main(String[] args) {
SpringApplication.run(HystrixOrderMain80.class,args);
}
}
编写业务接口
package com.java.springcloud.service;
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;
/**
* @author Woo_home
* @create 2020/3/28 14:00
*/
@Component
@FeignClient(name = "DEMO-PROVIDER-HYSTRIX-PAYMENT") // 上述 8001 的服务实例名称
public interface PaymentHystrixService {
@GetMapping(value = "/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping(value = "/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}
编写 Controller
package com.java.springcloud.controller;
import com.java.springcloud.service.PaymentHystrixService;
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;
/**
* @author Woo_home
* @create 2020/3/28 14:03
*/
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Timeout(id);
return result;
}
}
测试
未开启高并发请求的测试(跟上述一样使用 JMeter),正常访问
开启高并发之后访问,卡顿
总结:
- 消费者要么转圈圈等待
- 消费端要么超时错误
服务降级
服务降级容错解决的维度要求
- 如果出现超时导致服务器变慢(转圈),那么不再等待
- 出错(宕机或程序运行出错)
- 解决
- 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须要有服务降级
- 对方(8001)宕机了,调用者(80)不能一直卡死等待,必须要有服务降级
- 对方服务(8001)OK 了, 调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者)
服务降级配置
使用 @HystrixCommand 注解(一旦调用服务方法失败并抛出了错误信息后,会自动调用 @HystrixCommand 标注好的 fallbackMethod 调用类中的指定方法)
服务提供者超时设置
*设置服务提供者 8001 自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,做服务降级 fallback
修改服务提供者 8001 的 PaymentService
package com.java.springcloud.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author Woo_home
* @create 2020/3/28 12:33
*/
@Service
public class PaymentService {
/**
* 正常访问
* @param id
* @return
*/
public String paymentInfo_OK(Integer id) {
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id :" + id;
}
// ########## 新增注解 ##########
@HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = {
// 如果是 3 s 内就走正常逻辑,也就是服务自身调用的峰值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String paymentInfo_Timeout(Integer id) {
// 设置比上面大的时间
int timeNumber = 5;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_timeout,id :" + id + "\t" + "耗时 " + timeNumber + " s";
}
// 服务降级会执行的方法
public String paymentInfo_TimeoutHandler(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeoutHandler,id :" + id + "\t" + "进入 paymentInfo_TimeoutHandler 方法";
}
}
修该服务提供者 8001 的启动类
增加 @EnableCircuitBreaker 注解
测试访问
可以发现,使用 @HystrixCommand 注解设置峰值为 3 s,超过这个峰值就会走 fallbackMethod 设置的的方法
结论:标注 @HystrixCommand 的方法如果超时异常或者计算异常都会 fallback
消费端超时设置
修改 application.yml 文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
feign:
hystrix:
enabled: true
修改提供者的超时设置为 5 s
修改消费端控制器代码
package com.java.springcloud.controller;
import com.java.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;
/**
* @author Woo_home
* @create 2020/3/28 14:03
*/
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
// 如果是 1.5 s 内就走正常逻辑,也就是服务自身调用的峰值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Timeout(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者 80,对方支付系统繁忙请 10 s 后再试或者自己运行出错检查自己";
}
}
修改消费端启动类
加上 @EnableHystrix 注解
package com.java.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author Woo_home
* @create 2020/3/28 13:59
*/
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class HystrixOrderMain80 {
public static void main(String[] args) {
SpringApplication.run(HystrixOrderMain80.class,args);
}
}
测试访问
@DefaultProperties(defaultFallback = “”)
- 1:1 每个方法配置一个服务降级方法,技术上可以,实际上不太可行
- 1:N 除了个别重要核心业务有专属,其它普通的可以通过 @DefaultProperties(defaultFallback = “”) 统一跳转到统一处理结果页面
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量
修改消费端 OrderHystrixController
package com.java.springcloud.controller;
import com.java.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
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;
/**
* @author Woo_home
* @create 2020/3/28 14:03
*/
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping(value = "/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
/*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
// 如果是 1.5 s 内就走正常逻辑,也就是服务自身调用的峰值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})*/
@HystrixCommand
public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Timeout(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者 80,对方支付系统繁忙请 10 s 后再试或者自己运行出错检查自己";
}
// 下面是全局 fallback
public String paymentGlobalFallbackMethod() {
return "Global 异常处理信息,请稍后再试";
}
}
测试访问
FeignFallback
由于上面的代码耦合度较高,所以尽量使用接口的形式
新建 PaymentHystrixService 接口的实现类
package com.java.springcloud.service;
import org.springframework.stereotype.Component;
/**
* @author Woo_home
* @create 2020/3/28 16:32
*/
@Component // 记得加上注解
public class PaymentHystrixServiceImpl implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "------ PaymentHystrixService fallback paymentInfo OK";
}
@Override
public String paymentInfo_Timeout(Integer id) {
return "------ PaymentHystrixService fallback paymentInfo timeout";
}
}
修改 PaymentHystrixService 代码
package com.java.springcloud.service;
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;
/**
* @author Woo_home
* @create 2020/3/28 14:00
*/
@Component
// 使用实现类的返回信息,降低耦合度
@FeignClient(name = "DEMO-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentHystrixServiceImpl.class)
public interface PaymentHystrixService {
@GetMapping(value = "/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping(value = "/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}
测试访问
启动服务端 7001,提供者 8001,消费者 80,正常访问
故意关掉提供者 8001,模仿宕机,可以发现,页面返回了 PaymentHystrixService 实现类定义的信息
服务熔断
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息
当检测到该节点微服务调用响应正常后,恢复调用链路
在 SpringCloud 框架里,熔断机制通过 Hystrix 实现,Hystrix 会监控微服务间调用的状况,当失败的调用到一定的阀值,缺省是 5 s 内 10 次调用失败,就会启动熔断机制,熔断机制的注解是 @HystrixCommand
修改服务提供者 8001 的 PaymentService 代码
package com.java.springcloud.service;
import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;
/**
* @author Woo_home
* @create 2020/3/28 12:33
*/
@Service
public class PaymentService {
/**
* 正常访问
* @param id
* @return
*/
public String paymentInfo_OK(Integer id) {
return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK,id :" + id;
}
// ########## 新增注解 ##########
@HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = {
// 如果是 3 s 内就走正常逻辑,也就是服务自身调用的峰值
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "5000")
})
public String paymentInfo_Timeout(Integer id) {
// 设置比上面小的时间
int timeNumber = 3;
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " paymentInfo_timeout,id :" + id + "\t" + "耗时 " + timeNumber + " s";
}
// 服务降级会执行的方法
public String paymentInfo_TimeoutHandler(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " 系统繁忙,请稍后再试 " + id + "\t" + "进入 paymentInfo_TimeoutHandler 方法";
}
// ====== 服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",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"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
if(id < 0)
{
throw new RuntimeException("****** id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试 id: " +id;
}
}
其中上面的配置是从 HystrixCommandProperties 中拿的, Hystrix 全部配置都在这个类里面
修改服务提供者 8001 的控制器代码
增加以下代码
测试访问
当多次错误请求后会出现下面的情况(说明启动了服务熔断),得过段时间才能正确访问
结论
熔断类型:
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为 MTTR (平均故障处理时间,当打开时长达到所设时钟则进入半熔断状态)
- 熔断关闭:熔断关闭不会对服务进行熔断
- 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
断路器在什么情况下开始起作用?
涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的 10 s
- 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为 20,意味着在 10s 内,如果该 Hystrix 命令的调用次数不足 20 次,即使所有的请求都超时或其他原因失败,断路器都不会打开
- 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了 30 次调用,如果在这 30 次调用中,有 15 次发生了超时异常,也就是超过了 50% 的错误百分比,在默认的设定 50% 阀值情况下,这时候就会将断路器打开
断路器开启或者关闭的条件
- 当满足一定的阀值的时候(默认 10 s 内超过 20 个请求次数)
- 当失败率达到一定的时候(默认 10 s 内超过 50% 的请求失败)
- 到达以上阀值,断路器将会开启
- 当开启的时候,所有请求都不会进行转发
- 一段时间后(默认为 5 s),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,则继续开启,重复 4 和 5 步骤
断路器打开之后
1、再有请求调用的时候,将不会调用主逻辑,而是直接调用降级 fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果
2、原来的主逻辑要如何恢复?
对于这一问题,Hystrix 也为我们实现了自动恢复功能,当断路器打开,对主逻辑进行熔断之后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时
完整代码已上传至码云,感兴趣的朋友可以下载测试下 代码地址