发布时间:2022-11-09 16:30
服务雪崩:请求A服务、A调用B、B调用C,此时C宕机了,请求会一直等到超时。在分布式链路中,只要有一个服务宕机,可能导致整个业务线瘫痪。
为了解决这种情况,1.调整等待时间,这样可以缓解压力。缺点:不灵活,有的服务需要多的时间去完成。2.上游服务知道下游服务状态,如果正常就调用,宕机就return,这样就可以缓解服务雪崩。
Hystrix是Netflix的开源项目,他提供了熔断器功能,阻止分布式系统中出现的联动故障,提供故障的解决方案,从而提高整个分布式系统的弹性。
思路:hystrix都是搭配feign或者ribbon使用的,创建一个包用来写Feign接口的实现类,这个实现类就是熔断情况下走的代码
还是沿用之前eureka-server的代码,springboot版本2.3.12.RELEASE springcloud版本Hoxton.SR12,基本依赖有eureka、spring web
创建一个租车服务的springboot项目
server:
port: 8080
spring:
application:
name: rent-car-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
@RestController
public class RentCarController {
@GetMapping("rent")
public String rent(){
return "租车成功";
}
}
@SpringBootApplication
@EnableEurekaClient
public class RentCarServiceApplication {
public static void main(String[] args) {
SpringApplication.run(RentCarServiceApplication.class, args);
}
}
创建调用者springboot项目代码
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
}
@RestController
public class CustomerController {
@Autowired
private CustomerRentFeign customerRentFeign;
@GetMapping("customerRent")
public String CustomerRent(){
System.out.println("请求进入");
String rent = customerRentFeign.rent();
return rent;
}
}
feign接口代码,注解中fallback属性是对应实现类
/**
* 需要指定熔断的类
*/
@FeignClient(value = "rent-car-service",fallback = CustomerRentFeignHystrix.class)
public interface CustomerRentFeign {
@GetMapping("rent")
public String rent();
}
hystrix feign接口的实现类代码
@Component
public class CustomerRentFeignHystrix implements CustomerRentFeign {
/**
* 这个方法就是备选方案
* @return
*/
@Override
public String rent() {
return "熔断的情况";
}
}
如果全部启动,会得到租车成功的结果,如果把租车服务停掉,再次访问localhost:8081/customerRent,返回结果就变成了 熔断的情况
调用者的controller代码会报错,原因是这个类型有两个实现,一个是feign的代理对象,另一个是hystrix对feign接口的实现类创建的对象
@Autowired
private CustomerRentFeign customerRentFeign;
虽然代码报错,但是不影响正常使用,如果想解决报错可以使用@Qualifier注解指定Hystrix对Feign接口的实现类,或者修改高亮提示。
简单实现一个断路器:
启动类代码:
@SpringBootApplication
public class HystrixRealizedApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixRealizedApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
hystrix状态模型
public enum HystrixStatus {
CLOSE, //关闭状态 调用目标方法
OPEN, //开启状态 不调用目标方法
HALF_OPEN //半开状态 少量请求执行方法
}
断路器模型
/**
* 断路器模型
*/
@Data
public class Breaker {
//窗口时间
public static final Integer WINDOW_TIME = 20;
//最大失败次数
public static final Integer MAX_FAIL_COUNT = 3;
//断路器自己的状态
private HystrixStatus status = HystrixStatus.CLOSE;
//当前断路器失败次数 会进行i++操作 AtomicInteger 可以保证线程安全
private AtomicInteger currentFailCount = new AtomicInteger(0);
/**
* 线程池
*/
private ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
4,
8,
30,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
private Object lock = new Object();
{
poolExecutor.execute(()->{
//定期删除 20s一个周期清零 窗口滑动
while(true){
try {
//等待一个窗口时间
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果断路器是开的 请求不会去调用就不会失败 就不会记录次数
if (this.status.equals(HystrixStatus.CLOSE)){
//清零
this.currentFailCount.set(0);
}else{
//半开或者开 不需要记录次数 这个线程可以不工作
synchronized(lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
//记录失败次数
public void addFailCount(){
int i = currentFailCount.incrementAndGet(); //i++
if(i >= MAX_FAIL_COUNT){
//说明失败次数到达阈值
//修改当前状态为open
this.setStatus(HystrixStatus.OPEN);
//断路器打开以后就不能访问了,需要变成半开,等待一个时间窗口,让断路器变成半开
// new Thread(() -> {
// try {
// TimeUnit.SECONDS.sleep(WINDOW_TIME);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// this.setStatus(HystrixStatus.HALF_OPEN);
// //重置失败次数,不然下次请求进来直接变为open状态
// this.currentFailCount.set(0);
// }).start();
//线程池写法
poolExecutor.execute(()->{
try {
TimeUnit.SECONDS.sleep(WINDOW_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setStatus(HystrixStatus.HALF_OPEN);
//重置失败次数,不然下次请求进来直接变为open状态
this.currentFailCount.set(0);
});
}
}
}
controller代码:
@RestController
public class Controller {
@Autowired
private RestTemplate restTemplate;
@GetMapping("doRpc")
@HystrixAnno
public String doRpc(){
String result = restTemplate.getForObject("http:localhost:8989/abc", String.class);
return result;
}
}
aop的注解和相关类:
/**
* 熔断器切面注解,想切哪个方法在哪个方法加@HystrixAnno
*/
@Target(ElementType.METHOD) //切在方法上
@Retention(RetentionPolicy.RUNTIME) //作用域 运行时有效
@Documented
@Inherited
public @interface HystrixAnno {
}
@Component
@Aspect
public class HystrixAspect {
// public static final String POINT_CUT = "execution (com.example.hystrixrealized.controller.Controller.doRpc(..))"; // 这种方式不太灵活,选用注解的形式
//一个消费者可以调用多个提供者,每个提供者都有自己的断路器 创建一个断路器集合
public static Map<String, Breaker> breakerMap = new HashMap<>();
static {
//假设调用order-service的服务
breakerMap.put("order-service",new Breaker());
}
Random random = new Random();
/**
* 判断当前断路器的状态 决定是否发起调用(执行目标方法)
* @param joinPoint
* @return
*/
@Around(value = "@annotation(com.example.hystrixrealized.anno.HystrixAnno)")
public Object HystrixAround(ProceedingJoinPoint joinPoint){
Object result = null;
//获取当前提供者的断路器
Breaker breaker = breakerMap.get("order-service");
HystrixStatus status = breaker.getStatus();
switch(status){
case CLOSE:
//正常情况 执行方法
try {
result = joinPoint.proceed(); //执行目标方法
return result;
} catch (Throwable e) {
//调用失败 记录次数
breaker.addFailCount();
return "备用方案";
}
case OPEN:
//不能调用
return "备用方案";
case HALF_OPEN:
//少许流量调用
int i = random.nextInt(5);//取0-4的随机数
System.out.println(i);
if (i == 1){
//调用目标方法
try {
result = joinPoint.proceed();
//成功 断路器关闭
breaker.setStatus(HystrixStatus.CLOSE);
synchronized (breaker.getLock()){
breaker.getLock().notifyAll(); //断路器关闭状态 唤醒定时删除任务
}
} catch (Throwable e) {
return "备用方案";
}
}
default:
return "备用方案";
}
}
}
这个controller调用随便写得,实际没有这个提供者代码,测试访问后,断路器的变化,一开始应该是关闭状态,请求直接进到方法里,请求在一个时间窗口内(Breaker内的时间窗口属性 为20s)达到四次失败后,断路器变为开启状态 请求不会进入方法,再等待一个时间窗口,断路器变为半开状态,根据随机数结果部分请求会进入方法,如果调用成功状态变为关闭。
定期删除任务可以进行优化,断路器开的情况 请求不会进入方法调用 没有必要清零,可以不执行定期删除任务,相关代码就是断路器多线程 同步那块 还有aop里断路器变为关闭状态 唤醒定时删除任务。
Hystrix常用配置
hystrix:
command:
default: # 全局控制 ,可以换成方法名 单个方法控制
circuitBreaker:
enable: true
requestVolumeThreshold: 30 # 失败次数(阈值)
sleepWindowInMillIseconds: 20000 # 窗口时间
errorThresholdPercentage: 60 # 失败率
execution:
isolation:
Strategy: thread # 隔离方式 thread线程隔离 semaphore信号量隔离
thread:
timeoutInMilliseconds: 3000 # 调用超时时长
fallback:
isolation:
semaphore:
maxConcurrentRequests; 1000 #信号量隔离最大并发数