Hystrix是由Netflix开源的一个延迟和容错库,旨在隔离对远程系统,服务或第三方库的调用,防止级联故障,提高系统的弹性和容错能力。
1.服务的雪崩效应
微服务中,服务间调用错综复杂,一个请求,可能需要多个微服务接口才能实现,会形成非常复杂的调用链路。
如果一次业务请求需要调用A,B,C,D四个服务,这4个服务又可能调用其他服务,如果此时某个服务出现异常,请求阻塞,用户得不到效应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞,又因为服务器支持的线程和并发数有限,请求一直阻塞,会会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。
这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使 用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。此时如果有很 多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波 及范围不断扩大。
2.相关概念
2.1服务降级
服务降级是指在系统出现高并发、资源紧张或部分服务出现故障等情况下,为了保证核心服务的可用性和系统的稳定性,主动降低一些非核心服务的性能或暂时停止某些次要服务的策略。
触发情况
程序运行异常
调用超时
资源不足(如CPU,内存,网络宽带等紧张时)
服务熔断触发服务降级
2.2服务熔断
服务熔断是一种在分布式系统中用于保护服务可用性的机制,它类似于电路中的保险丝,当某个服务出现故障或异常时,防止故障进一步扩散,避免整个系统崩溃。
触发情况
高错误率:如果服务在一段时间内返回错误的比例持续高于设定的阈值,如超过50%的请求都返回错误,那么此时会触发熔断机制。
超时率过高:当服务的响应时间超过了预期的阈值,那么此时也会触发熔断机制。
异常流量模式:突然出现的异常流量高峰,是服务器承受不住,即使超时率和错误率没有明显变化,也可能触发,来保护服务和整个系统的稳定性。
2.3服务限流
服务限流是一种用于控制服务请求流量的技术手段,通过限制单位时间内进入系统或特定资源的请求数量。
3.具体实现
下面分别从服务降级和服务熔断两种方法来演示
3.1服务降级
3.1.1服务提供方进行局部服务降级
一般服务降级放在消费端,但是提供者一样能使用。现在我们需要在服务的提供方给每一个控制器方法 都定义一个兜底的方法,意味着如果我们的服务不能正常被访问,就要让用户不要处于一直等待的状态, 而是要执行对应的兜底方法,立马给用户返回一个友好的提示信息。如何定义局部的降级逻辑呢,我们只 需要遵循以下规则就可以了。
1. 降级方法(兜底方法)的返回值必须和被降级的方法保持一致。
2. 降级方法(兜底方法)的形式参数必须和被降级的方法保持一致。
首先在服务提供方引入Hystrix的相关依赖
<!--Hystrix-->
<dependency>
<groupId>org.springframework.cloud </groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<dependency>
在服务提供方的启动类上面使用开启服务熔断的注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker //开启使用Hystrix组件的功能
@SpringCloudApplication //@SpringBootApplication + @EnableDiscoveryClient @EnableCircuitBreaker
public class HystrixProviderApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixProviderApplication.class,args);
}
}
这里的SpringCloudApplication注解相当于SpringBootApplication,EnableDiscoverClient,EnableCircuitBreaker三个注解
给服务提供方的每一个控制器方法定义兜底方法
@RestController
@RequestMapping("provider")
public class PaymentController {
@Autowired
PaymentService paymentService;
@HystrixCommand(fallbackMethod = "callBackByFindById", commandProperties = {
//设置峰值,超过 3 秒,就会调用兜底方法,这个时间也可以由feign控制
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")
})
@RequestMapping("findById")
public Result<Payment> findById(@RequestParam("id") Long id) {
try {
Thread.sleep(3000);
Payment payment = paymentService.findById(id);
return new Result(200, "查询成功,当前服务端口是:", payment);
} catch (Exception e) {
e.printStackTrace();
return new Result(500, "查询失败", null);
}
}
/**
* 所谓的降级策略,就是给控制器方法提供一个备选方案,如果触发了降级,
* 为了快速的给用户响应,需要给用户一个友好的提示信息。局部的降级策略就是给每一个
* 控制器方法都提供一个备选方案,备选方案的返回值和形式参数必须和被降级的方法保持一致。
*/
public Result<Payment> callBackByFindById(Long id) {
return new Result(200, "findById方法触发了局部的降级策略", null);
}
@HystrixCommand(fallbackMethod = "callBackByHello", commandProperties = {
//设置峰值,超过 3 秒,就会调用兜底方法,这个时间也可以由feign控制
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000")
})
@RequestMapping("hello")
Result<Payment> hello() {
int i = 10 / 0; // 模拟出现异常
String message = paymentService.hello();
return new Result(200, message, null);
}
public Result<Payment> callBackByHello() {
return new Result(200, "hello方法触发了局部的降级策略", null);
}
}
3.1.2服务消费方进行局部服务降级
引入相同的依赖
在消费方的启动类上面添加同样的注解。
同样的给每一个控制器方法配置降级兜底方法
具体代码就不写了。
3.1.3全局进行消费降级
通过局部的降级处理,我们确实能够在调用异常或超时的情况下走对应的兜底方法。但是局部降级处理也 有明显的弊端,上面的降级策略,很明显造成了代码的杂乱,提升了耦合度,而且按照这样,每个方法都 需要配置一个兜底方法,很繁琐。现在将降级处理方法(兜底方法)做一个全局的配置,设置一个共有的 兜底方法即可。全局的降级兜底方法如何定义?我们只需要定义一个方法,方法的返回值和被降级的方法 的返回值保持一致即可。
在消费方的控制器里面定义一个全局的服务降级方法
@RestController
@RequestMapping("consumer")
@SuppressWarnings("all")
@DefaultProperties(defaultFallback = "globalFallbackMethod") //defaultFallback 引用的是全局降级策略的方法名称
public class PaymentController {
@Autowired
PaymentClient paymentClient;
@HystrixCommand(commandProperties = {
//设置峰值,超过 3 秒,就会调用兜底方法,这个时间也可以由feign控制
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")
})
@RequestMapping("findById/{id}")
public Result<Payment> findById(@PathVariable("id") Long id) {
Result<Payment> result = paymentClient.findById(id);
return result;
}
@HystrixCommand(commandProperties = {
//设置峰值,超过 3 秒,就会调用兜底方法,这个时间也可以由feign控制
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500")
})
@RequestMapping("hello")
public Result<Payment> hello() {
Result<Payment> result = paymentClient.hello();
return result;
}
/**
* 编写全局的降级策略 降级方法的返回值必须和被降级的方法的返回值保持一致。和被降级方法的参数无关。
*/
public Result<Payment> globalFallbackMethod() {
return new Result(200, "这是一个全局的降级兜底方法", null);
}
}
3.1.4Hystrix整合OpenFeign实现服务降级
不管是局部的降级处理还是全局的降级处理都有一个共性的问题,那就是控制器代码和服务降级代码糅合 在一起,违背了代码的单一性设计原则。现在我们使用Hystrix整合OpenFeign来实现服务降级,来优化上面的代码。
首先,在服务消费方的yml文件中开启Feign对于Hystrix的支持
# 开启Feign对Hystrix的支持
feign:
hystrix:
enabled: true
然后再feign客户端所属的模块里面定义一个类,实现Feign接口
@Component
public class PaymentFallBack implements PaymentClient {
//给对应的控制器方法进行降级处理
@Override
public Result<Payment> findById(Long id) {
return new Result(200,"hystrix整合feign的降级策略,对findById进行降级",null);
}
@Override
public Result<Payment> hello() {
return new Result(200,"hystrix整合feign的降级策略,对hello进行降级",null);
}
}
改造Feign客户端
@FeignClient(value = "service-provider", fallback = PaymentFallBack.class) //标识当前接口是一个Feign客户端 name/value:服务提供方的名称
public interface PaymentClient {
//如果feign客户端 和服务提供方使用问号拼接参数,都加@RequestParam注解。
@RequestMapping("provider/findById")
public Result<Payment> findById(@RequestParam("id") Long id);
@RequestMapping("provider/hello")
Result<Payment> hello();
}
@FeignClient中的fallback属性表明当该Feign客户端调用服务出现问题时,将使用PaymentFallBack类中对应的方法进行服务降级处理。
3.2服务熔断
3.2.1熔断器
熔断器也叫断路器,其英文单词为Circuit Breaker
熔断机制的原理很简单,就像家里的电路熔断器,如果电路发生短路就能够立刻熔断电路,避免发生火灾。在分布式系统中应用这一模式后,服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
但是不同于电路熔断只能断,不能自动重连,Hystrix可以实现弹性容错,也就是当情况好转之后,可以实现自动重连。
熔断器有三个状态
Closed:关闭状态,所有请求都正常访问
Open:打开状态,所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求百 分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不 低于20次。
Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路 器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全关闭断路 器,否则继续保持打开,再次进行休眠计时
实际上熔断机制是应对服务雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或 者响应时间太长的时,就会触发服务降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
在SpringCloud中,如果我们要使用断路器,需要知晓3个非常重要的参数。
1. 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒钟。
2. 请求总数阈值:在快照时间窗内,必须满足请求总阈值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时,或其他原因失败,断路器都不会打开。
3. 错误百分比阈值:当请求总次数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时或者异常,也就是超过了50%的错误百分比,在默认设定50%阈值情况下,断路器就会打开。
3.2.2代码实现
首先在服务消费方的控制器方法中定义一个测试断路器的方法
// 测试断路器
@HystrixCommand(commandProperties = {
// 设置峰值,超过 3 秒,就会调用兜底方法,这个时间也可以由feign控制
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2500"),
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") // 失败率达到多少后跳闸
})
@RequestMapping("testCircuitBreaker")
public Result<Payment> testCircuitBreaker(@RequestParam("id") int id) {
Result<Payment> result = paymentClient.testCircuitBreaker(id);
return result;
}
这些name属性的值都是被设定好了,要用的时候直接写上去就行
然后在Feign客户端中定义服务远程调用的接口
@FeignClient(value = "service-provider", fallback = PaymentFallBack.class) // 标识当前接口是一个Feign客户端 name/value:服务提供方的名称
public interface PaymentClient {
// 如果feign客户端 和服务提供方使用问号拼接参数,都加@RequestParam注解。
@RequestMapping("provider/findById")
public Result<Payment> findById(@RequestParam("id") Long id);
@RequestMapping("provider/hello")
Result<Payment> hello();
@RequestMapping("provider/testCircuitBreaker")
Result<Payment> testCircuitBreaker(@RequestParam("id") int id);
}
最后在服务提供方中定义调用短路器的方法
@RestController
@RequestMapping("provider")
public class PaymentController {
@Autowired
PaymentService paymentService;
@RequestMapping("testCircuitBreaker")
Result<Payment> testCircuitBreaker(@RequestParam("id") int id) {
if (id < 0) {
throw new RuntimeException("id不能为负数");
}
return new Result(200, "testCircuitBreaker调用成功", null);
}
}
只要请求发送到 testCircuitBreaker
这个接口方法,无论 id
是正数还是负数,这个方法都会被调用 。只不过当 id
为负数时,方法执行过程中会抛出 RuntimeException
异常,不会执行到返回成功结果那一步;而 id
为正数时,方法会正常执行并返回成功的结果。当异常出现的频率或数量等达到预先设定的阈值(比如一定时间内失败请求占比过高 ),熔断器就会开启。
开启后,后续请求不再实际调用 testCircuitBreaker
方法去尝试执行,而是直接触发降级逻辑,由降级方法返回一个兜底的结果,避免因持续调用不稳定服务导致更多问题,保障系统整体的可用性和稳定性 。 当熔断器开启一段时间后,会进入半开状态去试探性调用,如果调用成功达到一定比例,熔断器会关闭恢复正常调用;若仍失败,就继续保持开启状态。