在分布式系统中,一个子节点的超时或者故障会引起关联节点的故障,从而蔓延到整个系统中,比如库存服务超时,商品服务获取不到库存,订单服务无法获取到商品...因此分布式系统中需要一个容错管理来管理这些容错,SpringCloud提供Hystrix来实现服务降级和熔断,本文将通过一个简单的例子来介绍Hystrix。
系列文章
SpringCloud(一)-手把手教你创建springcloud微服务父子项目
SpringCloud(二)-手把手教你搭建Eureka Server和Eureka Client
SpringCloud(三)-手把手教你通过Rinbbon实现客户端负载均衡
SpringCloud(四)-手把手教你使用OpenFeign
SpringCloud(五)-手把手教你使用Hystrix配置服务熔断和降级以及Hystrix Dashboard
SpringCloud(六)-手把手教你搭建SpringCloud Config配置中心
SpringCloud(七)-手把手教你使用消息总线Bus实现动态刷新
SpringCloud(八)-手把手教你使用Stream消息驱动
1. Hystrix简介
Hystrix提供了服务降级,服务熔断,服务限流等服务。
2. 降级
降级是指,当请求超时、资源不足等情况发生时进行服务降级处理,不调用真实服务逻辑,而是使用快速失败(fallback)方式直接返回一个托底数据,保证服务链条的完整,避免服务雪崩。
2.1 pom.xml引入依赖
找到消费者8200项目,修改pom.xml配置
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
2.2 主启动类加上@EnableFeignClients注解
package com.elio.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class ProductConsumer8200 {
public static void main(String[] args){
SpringApplication.run(ProductConsumer8200.class, args);
}
}
2.3 controller接口中加上fallback函数
ProductConsumerController中以selectById方法为例,假设服务提供者根据传入的id为正则正常,为负数抛出一个异常来模拟。那么在selectById方法上加上@HystrixCommand(fallbackMethod = "getErroInfo")注解,其中getErroInfo就是降级函数
package com.elio.springcloud.controller;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
public class ProductConsumerController {
@Resource
RestTemplate restTemplate;
@Resource
ProductService productService;
//public static String url = "http://localhost:8100/";
public static String url = "http://springcloud-product-provider/";
/**
* 查询
* @return
*/
@GetMapping("product/consumer/get/info")
public Result getServiceInfo(){
return productService.getServiceInfo();
/*return new Result(200, "查询成功",
restTemplate.getForObject(url+"product/provider/get/info", Result.class));*/
}
/**
* 查询
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "getErroInfo")
@GetMapping("product/consumer/get/{id}")
public Result selectById(@PathVariable("id") Long id){
return productService.selectById(id);
/* return new Result(200, "查询成功",
restTemplate.getForObject(url+"product/provider/get/"+id, Result.class));*/
}
public Result getErroInfo(){
return new Result(500, "服务器内部出现错误", null);
}
}
2.4 测试
启动相应服务后,访问消费者API,结果出现异常页面,这不是我们期望的。
查看控制台日志,发现降级函数要与被处理的函数参数一致,这个也是个小坑
因此修改上面的getErroInfo加上参数
package com.elio.springcloud.controller;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
public class ProductConsumerController {
@Resource
RestTemplate restTemplate;
@Resource
ProductService productService;
//public static String url = "http://localhost:8100/";
public static String url = "http://springcloud-product-provider/";
/**
* 查询
* @return
*/
@GetMapping("product/consumer/get/info")
public Result getServiceInfo(){
return productService.getServiceInfo();
/*return new Result(200, "查询成功",
restTemplate.getForObject(url+"product/provider/get/info", Result.class));*/
}
/**
* 查询
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "getErroInfo")
@GetMapping("product/consumer/get/{id}")
public Result selectById(@PathVariable("id") Long id){
return productService.selectById(id);
/* return new Result(200, "查询成功",
restTemplate.getForObject(url+"product/provider/get/"+id, Result.class));*/
}
public Result getErroInfo(Long id){
return new Result(500, "服务器内部出现错误", null);
}
}
最终id传入-1,和1的结果如下。我们可以看到,当传入为-1时,参数不正常,服务提供者会抛出异常,因为我们在服务消费者方定义了降级函数,所以会调用降级函数getErroInfo 的逻辑 ,这比上面我们出错抛出错误异常也面要友好的多。当传入为1时,参数正常,返回结果也正常执行正常逻辑。
3. 使用OpenFegin 处理降级
通过我们上面的例子我们虽然实现了服务的降级,但是问题就是业务代码和公共代码耦合在一起了,这对后续的阔这和维护增加了困难,因此我们需要另外的将降级逻辑提取出来。我们上篇文章介绍的OpenFeign其实就已经集成了Hystrix,我们可以直接使用OpenFeign来实现服务的降级。
3.1 修改application.yml配置文件
server:
port: 8200
spring:
application:
name: springcloud-product-consumer
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8300/eureka/,http://localhost:8301/eureka/
feign:
hystrix:
enabled: true # fegin默认关闭hystrix服务
3.2 主启动类加上@EnableCircuitBreaker注解
package com.elio.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class ProductConsumer8200 {
public static void main(String[] args){
SpringApplication.run(ProductConsumer8200.class, args);
}
}
3.3 新增降级服务类
因为我们要对OpenFeign接口进行服务降级,所以我们需要新增一个回调类实现ProductService接口,对每个方法进行回调处理。新增ProductFallbackServieImpl.java
package com.elio.springcloud.service.impl;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import org.springframework.stereotype.Component;
@Component
public class ProductFallbackServieImpl implements ProductService {
public Result getServiceInfo() {
return new Result(500,"服务器内部出现错误,导致getServiceInfo接口异常",null);
}
public Result selectById(Long id) {
return new Result(500,"服务器内部出现错误,导致selectById接口异常",null);
}
}
3.4 商品接口类添加降级类
我们在ProductService接口上的@FeignClient注解,指定对应的降级处理类为ProductFallbackServieImpl
package com.elio.springcloud.service;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.impl.ProductFallbackServieImpl;
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(name="springcloud-product-provider", fallback = ProductFallbackServieImpl.class)
public interface ProductService {
/**
* 查询
* @return
*/
@GetMapping("product/provider/get/info")
public Result getServiceInfo();
/**
* 查询
* @param id
* @return
*/
@GetMapping("product/provider/get/{id}")
public Result selectById(@PathVariable("id") Long id);
}
3.5 测试
接下来就是测试了,首先我们注释掉之前写的@HystrixCommand 注解
package com.elio.springcloud.controller;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.service.ProductService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
public class ProductConsumerController {
@Resource
RestTemplate restTemplate;
@Resource
ProductService productService;
//public static String url = "http://localhost:8100/";
public static String url = "http://springcloud-product-provider/";
/**
* 查询
* @return
*/
@GetMapping("product/consumer/get/info")
public Result getServiceInfo(){
return productService.getServiceInfo();
/*return new Result(200, "查询成功",
restTemplate.getForObject(url+"product/provider/get/info", Result.class));*/
}
/**
* 查询
* @param id
* @return
*/
//@HystrixCommand(fallbackMethod = "getErroInfo")
@GetMapping("product/consumer/get/{id}")
public Result selectById(@PathVariable("id") Long id){
return productService.selectById(id);
/* return new Result(200, "查询成功",
restTemplate.getForObject(url+"product/provider/get/"+id, Result.class));*/
}
public Result getErroInfo(Long id){
return new Result(500, "服务器内部出现错误", null);
}
}
依次启动8300,8100,8101,8200,然后测试接口 http://localhost:8200/product/consumer/get/1 ,发现成功进入降级函数,这比将降级逻辑和业务逻辑混杂在一起好很多。
4. 熔断
熔断很好理解, 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN). 这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力。熔断器一般是在服务提供者配置的,而服务降级是在服务消费端使用的,接下来通过一个简单的例子来了解熔断机制。
4.1 修改pom.xml引入依赖
找到服务提供者项目8100,然后找到对应的pom.xml引入Hystrix依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.boot
spring-boot-starter-actuator
4.2 主启动类加上注解
我们找到服务提供者项目8100,然后添加注解 @EnableCircuitBreaker,由于我们需要由Hystrix Dashboard实时监控,所以还需要配置一个Servlet。具体仪表盘如何配置,请求第五步。
package com.elio.springcloud;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
@MapperScan("com.elio.springcloud.dao")
@EnableDiscoveryClient
@EnableCircuitBreaker
@Configuration
public class ProductProvider8100 {
public static void main(String[] args){
SpringApplication.run(ProductProvider8100.class, args);
}
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/actuator/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
4.3 servie配置熔断
以service实现类ProductServiceImpl中的selectById方法为例子,来配置熔断信息。其中fallbackMethod配置的降级函数,
- "circuitBreaker.enabled" 配置是否开启服务熔断
- "circuitBreaker.requestVolumeThreshold"为时间窗口内的请求阈值,只有达到这个阈值,才会判断是否打开断路器。比如配置为10次,那么在时间窗口内请求9次,9次都失败了也不会打开断路器
- "circuitBreaker.sleepWindowInMilliseconds"为时间窗口,当断路器打开后,会根据这个时间继续尝试接受请求,如果请求成功则关闭断路器。
- "circuitBreaker.errorThresholdPercentage"为配置的失败比率,在时间窗口内请求次数达到请求阈值,并且失败比率达到配置的50%,才会打开断路器。
package com.elio.springcloud.service.impl;
import com.elio.springcloud.dao.ProductMapper;
import com.elio.springcloud.dto.Result;
import com.elio.springcloud.entity.Product;
import com.elio.springcloud.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class ProductServiceImpl implements ProductService{
@Resource
private ProductMapper productMapper;
@HystrixCommand(fallbackMethod = "selectByIdFallbackHandler", commandProperties = {
// 是否启用服务熔断
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
// 请求阈值
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
// 时间窗口
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
// 错误比率
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
})
public Product selectById(Long id) throws Exception {
if(id < 0){
throw new Exception("id为负数");
}
return productMapper.selectById(id);
}
public Product selectByIdFallbackHandler(Long id){
return null;
}
public int deleteById(Long id) {
return productMapper.deleteById(id);
}
public int updateById(Long id, String name) {
return productMapper.updateById(id, name);
}
public int insertOne(Product product) {
return productMapper.insertOne(product);
}
}
4.4 测试
现在配置好了,启动服务提供者8100项目,然后不断地请求http://localhost:8100/product/provider/get/-1接口,这是仪表盘的信息如下:
请求8次,失败率为100%,没有达到请求阈值,断路器还是关闭状态
请求13次,失败率为100%,达到请求阈值,断路器处于打开状态
5. Hystrix Dashboard
Hystrix 熔断工作流程就是收集接口的访问情况,从而判断是否打开熔断来保护服务。在开发或者运维的过程中,我们需要一个图形界面实时观察这些信息,接下来就通过搭建Hystrix Dashboard来实时观察Hystrix熔断信息。
5.1 新增springcloud-hystrix-dashboard-8400子项目
新增一个名为springcloud-hystrix-dashboard-8400的dashboard子项目来进行信息监控。
5.1 修改pom.xml引入依赖
springcloudtest
com.elio.springcloud
1.0-SNAPSHOT
4.0.0
springcloud-hystrix-dashboard
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.cloud
spring-cloud-starter-ribbon
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-feign
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
5.2 新增主启动类
新增主启动类 HystrixDashboard8400,主要是加上@EnableHystrixDashboard这个注解
package com.elio.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboard8400 {
public static void main(String[] args){
SpringApplication.run(HystrixDashboard8400.class, args);
}
}
5.3 测试
接下俩就是启动项目来判断dashboard是否搭建成功,启动后浏览器中输入http://localhost:8400/hystrix
- 第一行输入栏就是我们要监控的微服务的地址
- delay 参数用来控制服务器上轮询监控信息的延迟时间,默认是2000毫秒,可以通过配置该属性来降低客户端的网络和cpu消耗。
- title 就是监控信息的标题,这个我们一般填写微服务名
5.4 消费者8200修改配置
因为8200开启了熔断配置,因此我们需要监控8200接口的访问信息,此时我们需要修改8200项目的pom.xml新增以下依赖
org.springframework.boot
spring-boot-starter-actuator
5.5 测试监控消费者8200的信息
现在消费者8200配置好了依赖,然后在dashboard主界面输入我们需要监控的信息,如下图
点击 monitor stream按钮,结果出现Unable to connect to Command Metric Stream.报错信息,但是我们该配置的都配置好了,怎么还会出现这个错误呢,原来因为我们使用的SpringCloud版本是H版本,版本太高了,我们需要配置一个servlet。
5.6 配置HystrixMetricsStreamServlet
找到消费者端8200的RestConfig配置类,配置HystrixMetricsStreamServlet这个Servlet
package com.elio.springcloud.config;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/actuator/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
5.7 测试
现在该有的配置都配置好了,接下来就是重启服务提供方测试了,再进进入dashboard界面,效果如下
现在我们尝试着不断访问接口,效果如下
仪表盘的数据也会跟着变动,出现的错误信息如下,出现5个错误,100%失败率。
6 总结
这篇文章简单的介绍了如何在消费端配置服务降级和熔断,但是熔断原理和一些概念性的支持本文没有涉及到,因为优点复杂,需要另开一篇文章来讲解了。