“在分布式系统中避免不了服务之间的调用,一般是通过RPC跨服务器跨系统调用。假设A系统调用B系统,此时由于网络环境或者B系统的响应速度非常慢,那么A系统调用B系统这部分逻辑就不得不进入等待状态。如果此时用户访问量激增,那么A系统就会积压越来越多的等待线程,最差的情况就是造成A系统也不可用。这种现象也叫“雪崩效应”,因此需要相应的手段来规避服务间调用时出现等待时间过长而造成整个系统不可用;
”
在以前的文章中介绍过SpringCloud的组件——Hystrix,它能控制系统之间调用的时间,如果超时则会做服务降级并响应。本篇主要介绍Hystrix另外两个很强大的特性,线程隔离、流量控制。
为什么需要线程隔离
首先明确一点,Hystrix是使用自己线程池中的线程替代web容器中的线程,然后再进行业务处理。下面两张对比图:
利用tomcat中http-nio-8082-exec工作线程处理的
标记了@HystrixCommand注解的接口,处理业务逻辑的线程是hystrix-InvokerOtherController线程池中的线程
这也就是为什么被Hystrix管理的接口能够有那么多的增强功能(熔断、降级等)。原因在于Hystrix从tomcat的工作线程那里,将程序的执行流程接管了过来,采用了自己的线程。
看到这里你可能就会有疑惑了,Hystrix是不是只维护了一个线程池,供所有标记了@HystrixCommand注解的接口方法使用呢?实际并非如此,Hystrix存在多个线程池,每次用户调用了被Hystrix管理的接口,根据不同组选用不同的线程池,然后从池中挑选一个线程处理业务逻辑。这也是为什么能够线程隔离的原因,准确的讲是因为存在了多个线程池,线程池之间是隔离的。
那为什么需要划分多个线程池呢?或者说为什么需要线程隔离?
假设没有划分,Hystrix只有一个线程池,那么会发生什么情况。同样还是A系统调用B系统,B系统不可用,导致A系统一直调用失败。但此时A系统有了Hystrix的加持可以在指定时间内快速失败并响应,而不是一直等待B系统。如果此时出现下图中的场景:
如果Hystrix只有一个线程池,当调用A系统中接口1的人数增多,那么线程池中的资源都会被接口1所占用,而访问接口2的用户则必须等待池中的资源释放后才能获得业务执行的权利;
由此可知Hystrix如果只有一个线程池是不合理的。那么Hystrix内部线程池是如何划分的呢?
从我们开发人员角度来说主要有两种,一是基于类的线程池,二是自定义HystrixCommand的实现类,基于实例的线程池。
基于类的Hystrix线程池
这种方式使用起来相对简单,接口加上@HystrixCommand注解即可。
@Slf4j
@RestController
public class InvokerOtherController {
/*
* 基于类的线程隔离
*/
@HystrixCommand(fallbackMethod = "findByIdFallBack")
@GetMapping("/invokerOtherByClass")
public ResponseEntity<?> getUserByClass() throws Exception {
// 模拟RPC调用花费了1s
Thread.sleep(1000);
log.info("i'am Ethan");
return ResponseEntity.ok("ok");
}
/**
* 服务降级方法
* 该方法的返回值,参数列表与源方法一样;
*/
public ResponseEntity<?> findByIdFallBack() {
log.info("服务降级了");
return ResponseEntity.ok("对不起网络太拥挤,请稍后再试..####!!!.");
}
}复制
只要在接口上加了@HystrixCommand注解,那么处理该接口的线程池就是以该类的类名,定义线程池。比如上面的代码,处理“/invokerOtherByClass”接口的线程池为[hystrix-InvokerOtherController-]。
不同Contorller类采用的线程池肯定是不一样的(采用@HystrixCommand注解的情况下)。
基于实例的Hystrix线程池
这种类型的线程池的使用不需要我们在Controller接口上标记@HystrixCommand注解。但需要我们自定义Hystrix线程池:
@Slf4j
public class InvokeCommand01 extends HystrixCommand<String> {
private String args;
public InvokeCommand01(String args) {
super(
Setter.withGroupKey(
//服务分组
HystrixCommandGroupKey.Factory.asKey("InvokeCommandGroupOne"))
//线程分组
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("InvokeCommandGroupThreadOne"))
//线程池配置
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10)
.withMaxQueueSize(30)
.withQueueSizeRejectionThreshold(30))
//配置使用线程的策略
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(3000)
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
);
this.args = args;
}
@Override
public String run() {
// 执行业务逻辑,进行系统间的调用
try {
// 模拟RPC
Thread.sleep(3000);
} catch (RestClientException e) {
log.info(e.getMessage());
throw new RuntimeException();
}
return "ok";
}
/**
* 服务降级
*/
@Override
protected String getFallback() {
log.error("InvokeCommand01 调用失败");
return "InvokeCommand01 调用失败";
}
}复制
定义好线程池并设置好了分组后(代码中设置为“InvokeCommandGroupOne”组),我们就可以在Controller接口或是其他地方任意调用了,比较灵活,
@Slf4j
@RestController
public class InvokerOtherController {
@GetMapping("/invokerOtherByInstance")
public ResponseEntity<?> getUserByInstance() throws Exception {
// 用InvokeCommandOne的线程
InvokeCommand01 invokeCommand01 = new InvokeCommand01("参数111~~~");
log.info(invokeCommand01.execute());
// 用InvokeCommandTwo的线程
InvokeCommand02 invokeCommand02 = new InvokeCommand02("参数222~~~");
log.info(invokeCommand02.execute());
return ResponseEntity.ok("OK");
}
}复制
假设InvokeCommand01调用B系统的接口1,InvokeCommand02(代码与InvokeCommand01是一样的)调用B系统的接口2,每次RPC调用只需new 出相应的Command,然后执行.execute()方法即可达到线程隔离并调用B不同接口。
此时处理"/invokerOtherByInstance"接口就有三个线程了:InvokeCommand01中的线程、InvokeCommand02中的线程,还有tomcat的工作线程。InvokeCommand01、InvokeCommand02负责RPC调用,tomcat线程负责主业务。从日志输入的线程信息也能看出来。
流量控制
流量控制是为了防止A服务访问B服务激增,而B服务受限了硬件设备的性能,过多的请求流量将B服务搞垮。
Hystrix采用线程池中的阻塞队列作为流量控制。
上图中,我配置了Hystrix线程池中的阻塞队列最多能存30个任务(请求),队列阈值也是30。当队列达到30个任务时,在调用.execute()方法时直接报错,返回HystrixRuntimeException: CommandOrder fallback execution rejected.此时连服务降级方法都不调用,直接报错响应用户。
总结
Hystrix的线程隔离、流量控制都通过内部自己维护了多个线程池实现的,按照【组】来划分不同的线程池达到线程(池)隔离,通过线程池中阻塞队列大小达到流量控制。对于我们业务开发来说,如果采用Hystrix做接口的健壮性增强,有两种方式,一种是基于类的Hystrix线程池,另一种是基于实例的线程池。