1.背景
分布式系统环境下,服务间类似依赖非常常见,一个业务调用通常依赖多个基础服务。如下图,对于同步调用,当库存服务不可用时,商品服务请求线程被阻塞,当有大批量请求调用库存服务时,最终可能导致整个商品服务资源耗尽,无法继续对外提供服务。并且这种不可用可能沿请求调用链向上传递,这种现象被称为级联故障效应,最终结果导致整个系统服务不可用。
2.断路器
所谓 熔断 就是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过 断路器 直接将此请求链路断开。也就是我们上面商品服务调用库存服务在指定时间窗内,调用的失败率到达了一定的值,那么 Hystrix
则会自动将商品服务与库存服务之间的请求都断了,以免导致服务雪崩现象。降级的目的是为了解决整体项目的压力,而牺牲掉某一服务模块而采取的措施。比如说商品详情页面可以暂时不显示推荐商品信息、商品评论等等功能来保证用户能正常查看商品顺利下单。
其实断路器思想借鉴了生活中电路保险丝的原理,在电路出现问题时(电压过高、短路),保险丝自动跳闸,以保护电路中电器。实现熔断降级的比较成熟的框架有:Netflix的Hystrix,阿里巴巴的sentinel。
项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
微信公众号:Shepherd进阶笔记
交流探讨群:Shepherd_126
3.Hystrix
Netflix开源了Hystrix组件,实现了断路器模式,SpringCloud对这一组件进行了整合。 在微服务架构使用我们只需要引入如下依赖即可:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
然后再项目启动类加上加@EnableHystrix注解开启Hystrix。
restTemplate调用下游服务接口
使用注解@HystrixCommand注解,配置超时时间,失败的降级方法等等
// @HystrixCommand: fallbackMethod 熔断降级调的方法必须在同一个类中
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "50")}, fallbackMethod = "anotherBackMethod")
public String testHystrix(Long skuId) {
try {
String result = restTemplate.getForObject("http://mall-product-service/api/mall/product/sku/" + skuId, String.class);
return result;
} catch (Exception e) {
log.error("调用商品详情接口失败", e);
throw new BusinessException("调用商品详情接口失败");
}
}
feign调用下游服务接口
springCloud的feign注解整合了Hystrix,我们只需要在配置文件中开启即可:
feign:
client:
config:
default:
connectTimeout: 60000
readTimeout: 60000
hystrix:
enabled: true
注意:我们都知道feign的底层使用ribbon实现的,设置调用接口超时时间既可以设置ribbon,也可以设置feign的,两者都设置了去feign的,但是设置feign时必须connectTimeout和readTimeout同时设置,否则不生效。同时如果开启hystrix,Feign的请求方式是先Hystrix然后Ribbon,Hystrix在最外层则熔断时间必须大于Ribbon的(ConnectTimeout + ReadTimeout)。而且如果Ribbon开启了重试机制,还需要计算对应的重试次数,保证在Ribbon的超时时间*重试次数这个时间范围内,Hystrix的熔断时间不会超时,hystrix的默认超时时间是1000ms,超过时间就会降级处理。
/**
* 在网络请求时,可能会出现异常请求,如果还想再异常情况下使系统可用,那么就需要容错处理,比如:网络请求超时时给用户提示“稍后重试”或使用本地快照数据等等。
*
* Spring Cloud Feign就是通过Fallback实现的,有两种方式:
*
* 1、@FeignClient.fallback = ProductFeignFallback.class指定一个实现Feign接口的实现类。
*
* 2、@FeignClient.fallbackFactory = UserFeignFactory.class指定一个实现FallbackFactory<T>工厂接口类
*
* 因为Fallback是通过Hystrix实现的, 所以需要开启Hystrix,spring boot application.properties文件配置feign.hystrix.enabled=true,这样就开启了Fallback
*/
@FeignClient(name = "${micro-server.mall-product}", path = "/api/mall/product", fallback = ProductFeignFallback.class)
//@FeignClient(name = "${micro-server.mall-product}", path = "/api/mall/product", fallbackFactory = ProductFeignFactory.class)
//@FeignClient(name = "${micro-server.mall-product}", path = "/api/mall/product")
public interface ProductService {
@GetMapping("/sku/{skuId}")
ResponseVO<SkuInfo> getSku(@PathVariable("skuId") Long skuId);
}
@Component
@Slf4j
public class ProductFeignFallback implements ProductService {
@Override
public ResponseVO<SkuInfo> getSku(Long skuId) {
log.info("执行feign接口对应的fallback方法了");
ResponseVO responseVO = new ResponseVO();
responseVO.setCode(200);
responseVO.setMsg("success");
SkuInfo skuInfo = new SkuInfo();
skuInfo.setBrandName("自定义");
skuInfo.setName("name");
responseVO.setData(skuInfo);
return responseVO;
}
}
@Component
@Slf4j
public class ProductFeignFactory implements FallbackFactory<ProductService> {
@Resource
private ProductFeignFallback productFeignFallback;
@Override
public ProductService create(Throwable throwable) {
log.info("执行ProductFeignFactory了");
throwable.printStackTrace();
log.error("异常信息:{}", throwable.getMessage());
return productFeignFallback;
}
}
hystrix实现原理
架构图如下:
Hystrix整个工作流如下:
1)构造一个 HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数;
2)执行命令,Hystrix提供了4种执行命令的方法;
3)判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;
4)判断熔断器是否打开,如果打开,跳到第8步;
5)判断线程池/队列/信号量是否已满,已满则跳到第8步;
6)执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;
7)统计熔断器监控指标;
8)走Fallback备用逻辑
9)返回请求响应
Hystrix提供了4种执行命令的方法:
execute()和queue() 适用于HystrixCommand对象,而observe()和toObservable()适用于HystrixObservableCommand对象。
execute()
以同步堵塞方式执行run(),只支持接收一个值对象。hystrix会从线程池中取一个线程来执行run(),并等待返回值。
queue()
以异步非阻塞方式执行run(),只支持接收一个值对象。调用queue()就直接返回一个Future对象。可通过 Future.get()拿到run()的返回结果,但Future.get()是阻塞执行的。若执行成功,Future.get()返回单个返回值。当执行失败时,如果没有重写fallback,Future.get()抛出异常。
observe()
事件注册前执行run()/construct(),支持接收多个值对象,取决于发射源。调用observe()会返回一个hot Observable,也就是说,调用observe()自动触发执行run()/construct(),无论是否存在订阅者。
如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run();如果继承的HystrixObservableCommand,将以调用线程阻塞执行construct()。
observe()使用方法:
1)调用observe()会返回一个Observable对象
2)调用这个Observable对象的subscribe()方法完成事件注册,从而获取结果
toObservable()
事件注册后执行run()/construct(),支持接收多个值对象,取决于发射源。调用toObservable()会返回一个cold Observable,也就是说,调用toObservable()不会立即触发执行run()/construct(),必须有订阅者订阅Observable时才会执行。
如果继承的是HystrixCommand,hystrix会从线程池中取一个线程以非阻塞方式执行run(),调用线程不必等待run();如果继承的是HystrixObservableCommand,将以调用线程堵塞执行construct(),调用线程需等待construct()执行完才能继续往下走。
toObservable()使用方法:
1)调用observe()会返回一个Observable对象
2)调用这个Observable对象的subscribe()方法完成事件注册,从而获取结果
需注意的是,HystrixCommand也支持toObservable()和observe(),但是即使将HystrixCommand转换成Observable,它也只能发射一个值对象。只有HystrixObservableCommand才支持发射多个值对象。
4.sentinel
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
sentinel基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
sentinel提供了控制台管理界面,可以实时监控资源以及管理规则
使用案例
流控
通过在控制台对服务某个服务的某个接口添加流控规则,例如对下面接口做流控,我们只需要找到对应服务在簇点链路下找到要限流的接口添加规则即可,当然我们也可以在流控规则新增,资源名为路径即可
/**
* @author fjzheng
* @version 1.0
* @date 2021/9/2 00:19
*/
@RestController
@RequestMapping("/sentinel")
@ResponseResultBody
@Slf4j
public class TestSentinelController {
@Resource
private TestSentinelService testSentinelService;
@GetMapping("/flow/control/1")
@ApiOperation("测试限流")
public String limitFlow() {
log.info("执行sentinel限流的接口的代码逻辑了");
String str = " hello, sentinel";
return str;
}
}
这时候我们一秒内只能调用一次接口,超出次数就会被限流
熔断降级
例如我们在秒杀服务中调商品服务的根据商品id查询商品接口,这时候我们在秒杀服务调用链路的查询商品接口添加降级规则如下:
配置的RT(响应时间)为1ms,当多次调用超时达到系统熔断比率,这时候接下来的10s就会熔断,不会在调用该接口。
/**
* @author fjzheng
* @version 1.0
* @date 2021/9/2 00:19
*/
@RestController
@RequestMapping("/sentinel")
@ResponseResultBody
@Slf4j
public class TestSentinelController {
@Resource
private TestSentinelService testSentinelService;
@GetMapping("/degrade")
@ApiOperation("熔断降级")
public String degrade() {
String s = testSentinelService.degrade(1l);
return s;
}
}
@Service
@Slf4j
public class TestSentinelServiceImp implements TestSentinelService {
@Resource
private ProductService productService;
@Override
public String degrade(Long skuId) {
try {
ResponseVO<SkuInfo> responseVO = productService.getSku(skuId);
SkuInfo skuInfo = responseVO.getData();
log.info("skuInfo: {}", skuInfo);
return JSONObject.toJSONString(skuInfo);
} catch (Exception e) {
log.error("调用商品详情接口失败", e);
throw new BusinessException("调用商品详情接口失败");
}
}
@Override
public String customResource() {
log.info("执行测试自定义的保护资源咯");
StringBuilder s = new StringBuilder();
try(Entry entry = SphU.entry("customSource")) {
for (int i =1; i<=5; i++) {
s.append(i);
TimeUnit.MILLISECONDS.sleep(100);
}
s.append("上山打老虎");
return s.toString();
} catch (BlockedException | BlockException | InterruptedException e) {
log.error("自定义资源异常信息:{}", e);
throw new BusinessException("自定义资源被限流了");
}
}
@Override
@SentinelResource(value = "annotationCustomResource", blockHandler = "blockHandler")
public String annotationCustomResource() throws InterruptedException {
StringBuilder s = new StringBuilder();
for (int i =1; i<=5; i++) {
s.append(i);
TimeUnit.MILLISECONDS.sleep(10);
}
s.append("上山打老虎1111111");
return s.toString();
}
public String blockHandler(BlockException e) {
log.error("异常信息:{}", e);
log.info("注解的降级方法执行了");
return "block handler";
}
}
同时我们可以使用try抛出异常的方式、注解方式定义资源,然后在控制台根据自定义的资源名配置对应的规则即可,代码如上所示。