浅谈微服务的服务隔离
服务隔离的必要性
微服务理念是指将单一的应用或系统再划分成更多的小服务,各个服务之间互相协调、互相配合,并最终实现系统的完整功能。在系的统工作过程中,一个客请求通常需要多个服务共同参与,各个服务间使用 rpc通信。因此,做好各个服务、甚至是各个服务的不同实例间的隔离显得尤为重要。
没有服务隔离的场景
现在,我们假设一下,如果我们的系统没做服务隔离,那么将会出现什么情况?
-
在各个子服务都正常的情况下,一派岁月静好,欣欣向荣的景象:
-
某一天,其中一个子服务发生了故障:
由于子服务发生了故障,未能及时响应上层请求,从而导致请求线程被阻塞。
- 暴风雨总是来得比想象中要猛烈 在原定的方案中,我们觉得某个微服务故障,最多只会影响和此微服务相关的功能,然而事实却是故障的微服务阻塞了大量的请求线程,占用了大量的系统资源,从而导致整个业务系统最终都不可用
服务隔离的方式
为了避免上述的问题,我们需要对服务做隔离,那么,我们要怎样做服务隔离呢?
阶段1:用线程池做隔离
由于导致业务系统不可用的根本原因是故障微服务的请求占用了大量的系统线程,所以我们的第一反应是可以将不同的微服务用不同的线程池隔离起来,形如:
private ThreadPoolExecutor serviceAExecutor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(50), THREAD_FACTORY, new AbortPolicy());
private ThreadPoolExecutor serviceBExecutor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(50), THREAD_FACTORY, new AbortPolicy());
public <T> T remoteCall(String service, Callable<T> task) throws InterruptedException, ExecutionException{
if("a".equals(service)){
return serviceAExecutor.submit(task).get();
}else if("b".equals(service)){
return serviceBExecutor.submit(task).get();
}else{
throw new RuntimeException("not support service!");
}
}
通过上述代码,我们把服务a和服务b用了不同的线程池去做请求。然而,我们仍然还有一个关键问题没用解决。
阶段2:给每个请求增加超时时间
阶段1的代码看似用了不同的线程池去做请求,实际上,Future.get()是一个同步方法,虽然在实际运行中,阶段1的代码有可能不会导致整个业务系统不可用(当业务系统总的可用线程数大于故障服务的线程数及配置的最大等待队列时,由于线程池的丢弃规则设置成AbortPolicy,所以不会再占用更多的资源)。但是,它仍然是不可靠的,我们需要为每一个服务设置合适的超时时间:
private int serviceATimeout = 2;
private int serviceBTimeout = 3;
public <T> T remoteCall(String service, Callable<T> task) throws InterruptedException, ExecutionException, TimeoutException{
if("a".equals(service)){
return serviceAExecutor.submit(task).get(serviceATimeout, TimeUnit.SECOND);
}else if("b".equals(service)){
return serviceBExecutor.submit(task).get(serviceBTimeout, TimeUnit.SECOND);
}else{
throw new RuntimeException("not support service!");
}
}
阶段3:采用信号量隔离
经过阶段2的改造后,服务大部分情况下是可用的,但是在实际情况中,不可避免地有以下情况发生:
- 各个子服务的请求,在不同时候并不总是均匀的,可能有些时候,某些微服务的请求多点,有些微服务的请求少点,这些我们在配置线程池参数时,不一定能准确估算出来
- 组成业务系统的子服务可能很多,需要依赖的外部服务或服务实例也可能很多,而操作系统能创建的最大线程数是有限的
- 虽然我们可以配置合适的线程回收机制来回收线程池中的闲置线程,但是创建大量的线程池将会使程序占用的系统资源不可控,不利于我们合理地评估及分配系统资源。
这个时候,我们就需要换一种思路,我们能不能创建少量的线程池就能实现不同服务间的隔离呢?
答案是可以的,对于上述例子,假设我们评估正常情况下,服务A的最大并发数不超过10个,服务B的最大并发数不超过20个,我们可以分别标记服务A和服务B当前占用的线程数,最终就能复用一个线程池来做隔离了,简单的示例代码如下:
private ThreadPoolExecutor serviceExecutor = new ThreadPoolExecutor(30, 50, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(50), THREAD_FACTORY, new AbortPolicy());
private final int serviceAMax = 10;
private final int serviceBMax = 20;
private final AtomicInteger serviceAOccupy = new AtomicInteger(0);
private final AtomicInteger serviceBOccupy = new AtomicInteger(0);
public <T> T remoteCall(String service, Callable<T> task) throws InterruptedException, ExecutionException, TimeoutException{
if("a".equals(service)){
try{
if(serviceAOccupy.getAndIncrement() >= serviceAMax){
throw new RuntimeException("reject request!");
}
return serviceAExecutor.submit(task).get(serviceATimeout, TimeUnit.SECOND);
}finally{
serviceAOccupy.decrementAndGet();
}
}else if("b".equals(service)){
try{
if(serviceBOccupy.getAndIncrement() >= serviceBMax){
throw new RuntimeException("reject request!");
}
return serviceBExecutor.submit(task).get(serviceBTimeout, TimeUnit.SECOND);
}finally{
serviceBOccupy.decrementAndGet();
}
}else{
throw new RuntimeException("not support service!");
}
}
这样我们就能够简单实现利用一个线程池隔离两个服务的功能了。
线程池 OR 信号量
那么在实际的项目中,我们应该选用线程池的方式隔离还是信号量的方式隔离呢?
一般情况下,我们都会优先使用线程池的方式做隔离,一来它的实现比较简单直接,易于维护,其次能够较大限度地提高应用的性能,仅仅在线程池方式不能满足要求时,我们才会去考虑采用信号量来做隔离,通常来说,以下几种场景是适合使用信号量隔离的:
- 系统资源比较紧张,无法支持较多的线程切换开销
- 需要隔离的对象极多,单纯使用线程池所带来的系统资源开销巨大
- 请求量非常密集,并发量极高,导致线程隔离的开销比较高的时候
- 业务响应的时间非常快,即使有限的线程数仍然能满足应用的需求,
使用hystrix来做隔离
如上文所述,自己真正去实现服务的隔离,绝不仅仅是上述那么简单的几行示例代码,需要考虑各种因素,例如各种并发情况,配置情况等等。 这个时候,利用一个优秀的开源项目来做服务隔离就显得尤为便捷,毕竟,并不是任何时候都有重复去做轮子的必要性。
hystrix 是 netflix开源的一个优秀的分布式系统的延迟和容错库,它能提供延迟、容错、隔离等的功能。
上手hystrix,我们先写一个建单的例子来利用线程池帮我们隔离两个请求:
public class HystrixTest extends HystrixCommand<String> {
private String id;
public HystrixTest(String id) {
super(Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("ExampleCommandKey" + id))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds((int)TimeUnit.SECONDS.toMillis(2))
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)));
this.id = id;
}
@Override
protected String run() throws Exception {
String result = null;
//TODO
result = doSomething();
return result;
}
通过上述的方法,我们就构建了一个基础自HystrixCommand的类,当我们需要使用这个类上的配置来做隔离时,只需要用 new HystrixTest("serviceA") 或者 new HystrixTest("serviceB") 这样即可,之后,就可以使用HystrixCommand提供的方法来进行调用了。
如果我们需要使用信号量来做隔离,那只需要把HystrixCommandProperties.ExecutionIsolationStrategy.THREAD替换成HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE 即可。
本文主要是讨论服务隔离的方式,此处就不展开讨论hystrix了。关于Hystrix的更多用法,有兴趣的读者可以自行到hystrix的官网去了解。
参考链接:
https://github.com/Netflix/Hystrix/wiki