服务容错保护
前言
- 微服务机构中,我们会将系统拆分成很多的服务单元,各单元的应用通过服务注册于订阅的方式互相依赖。
- 每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,就可能因为网络的原因或者依赖的服务自身出现问题出现调用故障或延迟,这些问题会直接导致调用方的对外服务出现延迟,当此调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。
- 比如订单服务需要调用库存服务。如果库存服务因自身逻辑导致响应缓慢,会直接导致订单服务的线程被挂起,等待库存申请服务的相应,在漫长的等待后用户会因请求库存失败而导致创建订单失败的结果。如果在高并发的情况下,挂起的线程未被释放,后续的订单请求别阻塞,会导致订单服务也不可用。
- 为了解决这样的问题,产生了断路器等一系列的服务保护机制
- 断路器本身是一种开关装置,用于在电路上保护线路过载,当线路中发生短路时,能够及时切断故障电路,防止发生过载、发热甚至是火灾等严重后果
- 分布式架构中,断路器模式的作用也类似,当某个服务发生故障之后,通过熔断器的故障监控,向调用者返回一个错误响应,而不是长时间等待,避免了故障在分布式系统的蔓延。
- Hystrix 中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。Hystrix是Netflix开源的一款容错框架。
隔离、熔断、降级
- 隔离:将请求封装在HystrixCommand中,然后这些请求在一个独立的线程中执行,每个依赖服务维护一个小的线程池(或信号量),在调用失败或超时的情况下可以断开依赖调用或者返回指定逻辑
- 熔断:当HystrixCommand请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务,断路器保持在开路状态一段时间后(默认5秒),自动切换到半开路状态(HALF-OPEN),这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED),否则重新切换到开路状态(OPEN)
- 降级:服务降级是指当请求后端服务出现异常的时候, 可以使用fallback方法返回的值
容错模式
- 主动超时:Http请求主动设置一个超时时间,超时就直接返回,不会造成服务堆积
- 限流:限制最大并发数
- 熔断:当错误数超过阈值时快速失败,不调用后端服务,同时隔一定时间放几个请求去重试后端服务是否能正常调用,如果成功则关闭熔断状态,失败则继续快速失败,直接返回。(此处有个重试,重试就是弹性恢复的能力)
- 隔离:把每个依赖或调用的服务都隔离开来,防止级联失败引起整体服务不可用
- 降级:服务失败或异常后,返回指定的默认信息
快速入门
向构建一个如下图架构的服务调用关系
- eureka-server工程: 服务注册中心,端口8761
- hello-service工程: 启动二个实例,端口分别为 8083,8082
- ribbon-consume工程:使用Ribbon实现的服务消费者,端口 9000
项目构建
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HelloController {
public Map<String, Object> hello( { String name)
Map<String, Object> map = new HashMap<>();
if (StringUtils.isEmpty(name)) {
map.put("message", "hello, I am hello server.");
} else {
map.put("message", name);
}
return map;
}
}消费端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class RestTemplateConfig {
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
public class ConsumTest {
private RestTemplate restTemplate;
private DiscoveryClient discoveryClient;
public Object hello( { String msg)
// 直接通过暴露的服务名调用,自动负载均衡获取实际地址
String url = "http://SERVICE-HELLO/hello/" + msg;
System.out.println("===============>>>从EurekaServer集群获取服务实例拼接的url:" + url);
// 4、消费者之间调用服务提供者
// 调用远程服务—> 简历微服务接口 RestTemplate -> JdbcTempate
// httpclient封装好多内容进行远程调用
Map forObject = restTemplate.getForObject(url, Map.class);
return forObject;
}
}关闭8082的服务后,再次访问后会得到500的错误
引入Hystrix
在consumer下引入Spring Cloud Hystrix 的依赖
1
2
3
4<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>在主函数上添加注解@EnableCircuitBreaker,启动Hystrix
1
2
3
4
5
6
7
8
9
10
11
// @EnableCircuitBreaker过时
// 开启断路器
public class RibbonConsumeApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonConsumeApplication.class, args);
}
}改造消费的方式,在函数上增加@HystrixCommand注解指定回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//通过HystrixCommand注解,手动指定一个降级方法,出现异常后会调用该降级方法
public Object hello( { String msg)
// 直接通过暴露的服务名调用,会自动获取实际地址
String url = "http://SERVICE-HELLO/hello/" + msg;
System.out.println("===============>>>从EurekaServer集群获取服务实例拼接的url:" + url);
// 4、消费者之间调用服务提供者
// 调用远程服务—> 简历微服务接口 RestTemplate -> JdbcTempate
// httpclient封装好多内容进行远程调用
Map forObject = restTemplate.getForObject(url, Map.class);
return forObject;
}
public Object helloFallback(String message){
Map<String,Object> msg = new HashMap<>();
msg.put("status", 995);
msg.put("message", "hello服务出现故障,"+message+"调用失败");
return msg;
}关闭8082的server服务,在轮询调用的过程中,发现8082被调用的时候不是返回上面的错误内容,返回的是回调的内容。
模拟服务阻塞(长时间未响应),Hystrix 默认超时时间为2000毫秒,对方法做些修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Map<String, Object> hello( String name)
throws InterruptedException {
// 等待几秒
int sleepTime = new Random().nextInt(3000);
log.info("sleepTime:" + sleepTime);
Thread.sleep(sleepTime);
Map<String, Object> map = new HashMap<>();
if (StringUtils.isEmpty(name)) {
map.put("message", "hello, I am hello server.");
} else {
map.put("message", name);
}
return map;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object hello( String msg)throws InterruptedException {
//消费端统计消耗时间
long start = System.currentTimeMillis();
// ....
long end = System.currentTimeMillis();
log.info("spend time: " + (end - start));
return forObject;
}测试:如果当spend time大于2000的时候,页面会返回降级后的结果
原理分析
- 通过上面的示例,我们对 Hystrix的使用场景和使用方法已经有了一个基础的认识,接下来解读下Netflix Hystrix 官方的流程图,了解单一请求调用相关依赖后Hystrix是如何工作的
Hystrix 容错的流程图
- 每个请求都会封装到 HystrixCommand 中
- 请求会以同步或异步的方式进行调用
- 判断熔断器是否打开,如果打开,它会直接跳转到 8 ,进行降级
- 判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8
- 如果前面没有错误,就调用 run 方法,运行依赖逻辑
- 运行方法可能会超时,超时后从 5a 到 8,进行降级
- 运行过程中如果发生异常,会从 6b 到 8,进行降级
- 运行正常会进入 6a,正常返回回去,同时把错误或正常调用结果告诉 7 (Calculate Circuit Health)
- Calculate Circuit Health它是 Hystrix 的大脑,是否进行熔断是它通过错误和成功调用次数计算出来的
- 降级方法(8a没有实现降级、8b实现降级且成功运行、8c实现降级方法,但是出现异常)
- 没有实现降级方法,直接返回异常信息回去
- 实现降级方法,且降级方法运行成功,则返回降级后的默认信息回去
- 实现降级方法,但是降级也可能出现异常,则返回异常信息回去
创建HystrixCommand,HystrixObservableCommand对象
首先构建一个HystrixCommand对象,用来表示对依赖服务的操作请求,同时传递所有需要的参数
从命名可得知它采用了“命令模式”来实现对服务调用操作的封装
- HystrixCommand:用在依赖服务返回单个操作结果的时候
- HystrixObservableCommand: 用在依赖服务返回多个操作结果的时候
命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。可以被用于实现“行为请求者”与行为实现者的解耦,以便 使二者可以适应变化
命令模式的简单实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51// 接收者
class Receiver{
public void action(){
//真正的业务逻辑
}
}
// 抽象命令
interface Command{
void execute();
}
// 具体的实现类
class ConcreteCommand implements Command{
private Receiver receiver;
public ConcreteCommand(Receiver receiver){
this.receiver = receiver;
}
public void execute() {
this.receiver.action();
}
}
//客户端调用者
class Invoker{
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void action(){
this.command.execute();
}
}
class Clien{
public static void main(String[] args) {
Receiver receiver = new Receiver();
ConcreteCommand concreteCommand = new ConcreteCommand(receiver);
Invoker invoker = new Invoker();
invoker.setCommand(concreteCommand);
invoker.action();// 客户端通调用者执行命令
}
}- Receiver: 接收者,知道如何处理具体的业务逻辑
- Command: 抽象命令:定义了一个命令对象具备的命令操作。如execute(), do(), redo(),命令操作被调用后会触发接收者去执行命令对应的具体业务逻辑
- ConcreteCommand: 具体的命令实现类;绑定了命令操作与接收者之间的关系,execute命令的实现委托给了Receiver的action()m函数
- Invoker: 调用者,持有命令对象,通过命令独显完成具体的业务逻辑
- Invoker 和 Receiver 通过Command命令接口实现了解耦,对于调用者来说,可以注入多个命令操作,在需要的时候直接调用即可。不需要关系具体的实现。
HystrixCommand 是对Command的进一步抽象定义。 Invoker和Receiver的关系非常类似 请求- 响应 模式,所以它比较适用于实现记录日志,撤销操作,队列请求等
————————————————
@HystrixCommand
定义Hystrix的熔断器,查看HystrixCommand源码,这个注解有10个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package com.netflix.hystrix.contrib.javanica.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public HystrixCommand {
String groupKey() default "";
String commandKey() default "";
String threadPoolKey() default "";
String fallbackMethod() default "";
HystrixProperty[] commandProperties() default {};
HystrixProperty[] threadPoolProperties() default {};
Class<? extends Throwable>[] ignoreExceptions() default {};
ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;
HystrixException[] raiseHystrixExceptions() default {};
String defaultFallback() default "";
}
版权声明:本文为CSDN博主「闪耀的瞬间」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhuyu19911016520/article/details/85346065
Hystrix的熔断、降级、隔离说的也很好:https://www.cnblogs.com/Leo_wl/p/9054906.html