限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。
而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
关于线程隔离和熔断降级是什么,看这里雪崩问题
不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。
而我们的微服务远程调用都是基于Feign来完成的,因此我们需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。
FeignClient整合Sentinel
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
修改配置,开启sentinel功能
修改order-service的application.yml文件,开启Feign的Sentinel功能:
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
编写失败降级逻辑
业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。
给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
步骤一:在feing-api项目中定义类,实现FallbackFactory:
feign-api是抽出去的模块,里面包含了所有的feign发送请求的功能。抽取Feign远程调用
package cn.itcast.feign.client.fallback;
import cn.itcast.feign.client.UserClient;
import cn.itcast.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
//如果远程调用findById接口发生了异常,就返回一个包含错误信息的user
//这个方法重写是因为实现FallbackFactory接口时用了UserClient的泛型,重写的这里面的方法
@Override
public User findById(Long id) {
log.error("查询用户异常", throwable);
return new User(-1L,"错误","异常");
}
};
}
}
步骤二:在feing-api项目中的配置类中将UserClientFallbackFactory注册为一个Bean:
package cn.itcast.feign.config;
import cn.itcast.feign.client.fallback.UserClientFallbackFactory;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL; // 日志级别为BASIC
}
@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
return new UserClientFallbackFactory();
}
}
步骤三:在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:
package cn.itcast.feign.client;
import cn.itcast.feign.client.fallback.UserClientFallbackFactory;
import cn.itcast.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class, fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
第三步的
configuration = DefaultFeignConfiguration.class
可以设置在启动类的注解@EnableFeignClients(clients = {UserClient.class},configuration = DefaultFeignConfiguration.class)
里面实现全局配置 这个配置注解@EnableFeignClients在order-service模块里面的。Feign自定义配置配置类的方式
重启order-service后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路:
线程隔离(舱壁模式)
线程隔离有两种方式实现:
- 线程池隔离
- 信号量隔离(Sentinel默认采用)
如图:
线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
- 优点:
- 支持主动超时
- 支持异步调用
- 缺点
- 线程的额外开销比较大
- 场景
- 低扇出,就是少调用其他服务,比如A服务调用多个个其他服务,概念上就像一个扇形,一个顶点,圆弧等,这个扇形越大,调用的服务越多,开销越大。
信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
- 优点:
- 轻量级,无额外开销
- 缺点
- 不支持主动超时
- 不支持异步调用
场景
QPS:就是每秒的请求数,流量控制>控流模式
线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)
测试
给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2
用JMeter测试,我们设置0秒发20个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
可以看到,虽然请求结果都成功了,但是用户这里却走了失败降级的逻辑熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
1.慢调用
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
如上图的意思是:
请求超过50ms的调用是慢调用,统计最近1000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.3(慢调用数量/请求总数),则触发熔断,熔断时长为10秒。然后进入half-open状态,放行一次请求做测试。
测试
按照上图的配置,对远程调用user的请求做降级配置。
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
//id为1时,触发慢调用
if(id==1L){
Thread.sleep(60);
}
return userService.queryById(id);
}
此时,orderId=101的订单,关联的是id为1的用户,调用时间大于60ms。orderId=102的订单,关联的是id为2的用户,调用时间小于60ms。
我们快速的访问orderId=101的订单,1秒内访问超过5次,就会触发熔断,熔断后任何订单调用user都会被阻碍直接失败。
orderId=101访问被熔断了,102同样访问不了,10秒后就能访问了。
2.异常比例,异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
测试
像上图这样设置降级规则,统计1秒内,超过5次请求,且报异常的请求数量超过2就熔断10秒
首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常数的熔断:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
//id为1时,触发慢调用
if(id==1L){
Thread.sleep(60);
}else if(id==2L){
throw new RuntimeException("故意抛出异常,出发异常数熔断");
}
return userService.queryById(id);
}
我们去浏览器快速访问orderId为102的接口,直接异常,触发了熔断规则后,再去访问103也是异常