Hystrix 服务熔断
熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制,熔断一般配置在服务提供者,即作为一个服务给别人调用的时候,出现了某种异常情况对自己进行保护。
熔断状态
在熔断机制中涉及了三种熔断状态:
- 熔断关闭状态(Closed):当服务访问正常时,熔断器处于关闭状态,服务调用方可以正常地对服务进行调用。
- 熔断开启状态(Open):默认情况下,在固定时间内接口调用出错比率达到一个阈值(例如 50%),熔断器会进入熔断开启状态。进入熔断状态后,后续对该服务的调用都会被切断,熔断器会执行本地的降级(FallBack)方法。
- 半熔断状态(Half-Open): 在熔断开启一段时间之后,熔断器会进入半熔断状态。在半熔断状态下,熔断器会尝试恢复服务调用方对服务的调用,允许部分请求调用该服务,并监控其调用成功率。如果成功率达到预期,则说明服务已恢复正常,熔断器进入关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。
Hystrix 实现服务熔断的步骤如下:
- 当服务的调用出错率达到或超过 Hystix 规定的比率(默认为 50%)后,熔断器进入熔断开启状态。
- 熔断器进入熔断开启状态后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,该服务的降级逻辑会临时充当业务主逻辑,而原来的业务主逻辑不可用。
- 当有请求再次调用该服务时,会直接调用降级逻辑快速地返回失败响应,以避免系统雪崩。
- 当休眠时间窗到期后,Hystrix 会进入半熔断转态,允许部分请求对服务原来的主业务逻辑进行调用,并监控其调用成功率。
- 如果调用成功率达到预期,则说明服务已恢复正常,Hystrix 进入熔断关闭状态,服务原来的主业务逻辑恢复;否则 Hystrix 重新进入熔断开启状态,休眠时间窗口重新计时,继续重复第 2 到第 5 步。
下图是详细的流程图:
配置案例:
1.主配置类上添加@EnableEurekaClient (开启eureka客户端),@EnableCircuitBreaker(开启断路器)。
2.service方法上配置断路器熔断参数(开启断路器,在10秒的窗口期,调用次数超过10次,且失败率达到60%,即进行服务熔断)。
import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;
@Service
public class PaymentService {
//=====服务熔断 10s之内 10次请求有6次失败 就会开启断路器
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreakerFallback(Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " + id;
}
}
3.配置调用service的controller
import com.hyh.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: " + result);
return result;
}
}
我们浏览器先成功访问controller http://localhost:8001/payment/circuit/1 。控制台打印如下:
2024-05-19 11:29:37.872 INFO 5680 --- [io-8001-exec-10] c.h.s.controller.PaymentController : ****result: hystrix-PaymentService-10 调用成功,流水号: afe8f78e6eb740dc8b6a2df2810b31e0
2024-05-19 11:29:46.966 INFO 5680 --- [nio-8001-exec-1] c.h.s.controller.PaymentController : ****result: hystrix-PaymentService-10 调用成功,流水号: b2a5086c1aa24a77aff05142dcfea4d9
2024-05-19 11:29:47.335 INFO 5680 --- [nio-8001-exec-2] c.h.s.controller.PaymentController : ****result: hystrix-PaymentService-10 调用成功,流水号: 3c9f32e0a7e74693abe2685b684ec685
我们再把参数设置为-1,快速狂刷10次,控制台显示如下:
2024-05-19 11:29:51.933 INFO 5680 --- [nio-8001-exec-3] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:53.860 INFO 5680 --- [nio-8001-exec-8] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:54.268 INFO 5680 --- [nio-8001-exec-9] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:54.636 INFO 5680 --- [nio-8001-exec-7] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:54.955 INFO 5680 --- [nio-8001-exec-6] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:55.292 INFO 5680 --- [nio-8001-exec-4] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:55.618 INFO 5680 --- [nio-8001-exec-5] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:55.947 INFO 5680 --- [io-8001-exec-10] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:56.308 INFO 5680 --- [nio-8001-exec-1] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:29:56.683 INFO 5680 --- [nio-8001-exec-2] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
我们再赶快把参数设置为原来正常的参数1,发现服务确实被熔断保护起来了,仍然不能调用,一段时间后(默认是5秒),这个时候断路器是半开状态,会让其中一个进行转发。如果成功,断路器会关闭,若失败,继续开启。
2024-05-19 11:29:56.683 INFO 5680 --- [nio-8001-exec-2] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: -1
2024-05-19 11:30:03.358 INFO 5680 --- [nio-8001-exec-3] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: 1
2024-05-19 11:30:05.115 INFO 5680 --- [nio-8001-exec-8] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: 1
2024-05-19 11:30:05.501 INFO 5680 --- [nio-8001-exec-9] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: 1
2024-05-19 11:30:05.875 INFO 5680 --- [nio-8001-exec-7] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: 1
2024-05-19 11:30:06.277 INFO 5680 --- [nio-8001-exec-6] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: 1
2024-05-19 11:30:06.646 INFO 5680 --- [nio-8001-exec-4] c.h.s.controller.PaymentController : ****result: id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: 1
2024-05-19 11:30:06.986 INFO 5680 --- [nio-8001-exec-5] c.h.s.controller.PaymentController : ****result: hystrix-PaymentService-10 调用成功,流水号: f9307e7c12e14131acfda81a35e7bbe7
Hystrix 服务降级
Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。既然是这样,则说明降级配置在服务提供方,也能配在消费方。
服务降级的使用场景有以下 2 种:
- 在服务器压力剧增时,根据实际业务情况及流量,对一些不重要、不紧急的服务进行有策略地不处理或简单处理,从而释放服务器资源以保证核心服务正常运作。
- 当某些服务不可用时,为了避免长时间等待造成服务卡顿或雪崩效应,而主动执行备用的降级逻辑立刻返回一个友好的提示,以保障主体业务不受影响。
详细来说就是:
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
详细例子
1.服务端的服务降级。
假设我们有个服务PaymentService,自身有个paymentInfoTimeOut方法,若4900毫秒未执行完毕,就进行降级操作,这里我们给这个方法执行时间睡眠个5秒钟,模拟5秒此服务才执行完毕。
import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.concurrent.TimeUnit;
@Service
public class PaymentService {
/**
* 正常访问,肯定OK
*/
public String paymentInfoOk(Integer id) {
return "线程池: " + Thread.currentThread().getName() + " paymentInfo_OK,id: " + id + "\t" + "O(∩_∩)O哈哈~";
}
@HystrixCommand(fallbackMethod = "paymentInfoTimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "4900")
})
public String paymentInfoTimeOut(Integer id) {
//int age = 10/0;
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + " id: " + id + "\t" + "O(∩_∩)O哈哈~" + " 耗时(秒): ";
}
public String paymentInfoTimeOutHandler(Integer id) {
return "线程池: " + Thread.currentThread().getName() + " 8001系统繁忙或者运行报错,请稍后再试,id: " + id + "\t" + "o(╥﹏╥)o";
}
}
写个controller来暴漏这个service的访问接口。
import com.hyh.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfoOk(id);
log.info("*****result: " + result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfoTimeOut(id);
log.info("*****result: " + result);
return result;
}
}
然后我们再来编写一个客户端,来请求这个服务端。
先配置远程调用服务端的接口。
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(value = "cloud-provider-hystrix-payment",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfOk(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfTimeOut(@PathVariable("id") Integer id);
}
顺便指定客户端降级的方法PaymentFallbackService。
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfOk(Integer id) {
return "PaymentFallbackService 失败降级了 "+id;
}
@Override
public String paymentInfTimeOut(Integer id) {
return "PaymentFallbackService 超时降级了 "+id;
}
}
编写客户端的请求接口。
import com.hyh.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfOk(id);
}
/*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5500")
})*/
@HystrixCommand
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
//int age = 10 / 0;
String s = paymentHystrixService.paymentInfTimeOut(id);
log.info(s);
return s;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
/**
* 下面是全局fallback方法
*/
public String paymentGlobalFallbackMethod() {
return "(客户端)Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
访问客户端接口http://localhost:80/consumer/payment/hystrix/timeout/1
服务端的控制台发现服务端直接超时降级了:
客户端页面返回:
客户端的控制台:
2.客户端降级操作。
我们在客户端的paymentInfoTimeOut方法中,报一个客户端上的异常(int age = 10 / 0;)。
import com.hyh.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author summerday
*/
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentInfOk(id);
}
/*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5500")
})*/
@HystrixCommand
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
int age = 10 / 0;
String s = paymentHystrixService.paymentInfTimeOut(id);
log.info(s);
return s;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
/**
* 下面是全局fallback方法
*/
public String paymentGlobalFallbackMethod() {
return "(客户端)Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
然后再次访问客户端接口http://localhost:808/consumer/payment/hystrix/timeout/1
发现客户端抛出了它的全局异常。
当然我们也可以针对这个异常方法,定义属于自己的异常处理方法:
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5500")
})
//@HystrixCommand
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfoTimeOut(@PathVariable("id") Integer id) {
int age = 10 / 0;
String s = paymentHystrixService.paymentInfTimeOut(id);
log.info(s);
return s;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
再次访问:
这里要着重注意的是:如果异常属于服务端的,那么它的异常处理会走 PaymentFallbackService对应的降级方法;如果异常属于客户端自己的,它的异常处理会走客户端自己定义的paymentTimeOutFallbackMethod方法。
这里还需要注意的是:客户端访问服务端,如果服务端产生了异常,我们如何获取服务端的异常呢?
在使用Feign作为声明式HTTP客户端时,如果后端服务调用失败(例如,超时、网络错误、服务端返回非2xx状态码等),你可以使用@FeignClient
注解的fallback
或fallbackFactory
属性来定义回退逻辑。然而,默认情况下,Feign的Fallback实现并不直接提供后端服务产生的具体异常或错误消息。
为了获取服务端的异常提示,你可以采取以下几种方法:
-
使用ErrorDecoder:
你可以自定义一个ErrorDecoder
,并在其中捕获和处理HTTP响应。如果响应指示了一个错误(例如,非2xx状态码),你可以从这个响应中提取错误消息,并将其封装到一个自定义的异常中,然后抛出这个异常。你的Fallback实现可以捕获这个异常并据此提供回退逻辑。 -
使用FallbackFactory:
相比于简单的Fallback类,FallbackFactory
允许你访问触发回退的请求和异常(如果有的话)。你可以使用这个信息来定制你的回退逻辑。例如,你可以检查异常的类型或消息,并据此提供不同的回退行为。 -
使用Hystrix(如果你在使用它):
如果你的Feign客户端与Hystrix集成,那么当命令失败时,Hystrix会触发回退逻辑。你可以在Hystrix的Command执行逻辑中捕获异常,并尝试从异常中提取服务端的错误信息。然而,请注意,Hystrix已经被宣布进入维护模式,并且不再接受新功能。 -
在服务端统一错误处理:
另一个策略是在你的服务端应用中统一处理错误,并确保所有错误都返回一个标准的错误格式(例如,JSON格式的错误对象)。这样,无论服务端发生什么错误,你的Feign客户端都可以解析这个标准的错误格式,并据此提供适当的回退逻辑。 -
日志记录:
如果你只是想在开发或调试过程中查看服务端的错误提示,那么你可以简单地配置Feign或你的日志框架来记录详细的请求和响应。这样,当后端服务调用失败时,你可以查看日志以获取更多信息。
(备注)spring cloud 一些相关组件的默认超时时间如下:
-
Zuul网关:
zuul.host.socket-timeout-millis
:默认值为1000毫秒(1秒)。zuul.host.connect-timeout-millis
:默认值为(可能是)2000毫秒(2秒)。但请注意,这些默认值可能因Spring Cloud的版本而异。
-
Ribbon:
ReadTimeout
:负载均衡超时时间,默认值为5000毫秒(5秒)。ConnectTimeout
:Ribbon请求连接的超时时间,默认值可能是2000毫秒(2秒),但这也可能因版本而异。MaxAutoRetries
:对当前实例的重试次数,默认值为0。MaxAutoRetriesNextServer
:对切换实例的重试次数,默认值为1。
-
Feign:
- Feign调用服务的默认时长是1秒钟。但Feign的底层使用Ribbon,因此也可以通过配置Ribbon来调整Feign的超时时间。
- 使用
ribbon.ReadTimeout
和ribbon.ConnectTimeout
可以设置Feign的连接和读取超时时间。
-
Hystrix:
- Hystrix默认的超时时间(阻塞)是2000毫秒(2秒)。超过这个时间,将发生熔断请求。但请注意,只有在你的项目中引入了Hystrix并使用它作为熔断器时,这个默认超时时间才适用。
整体调用流程: