“ 在微服务的架构中,服务隔离应该是一个比较常见词汇,什么是服务隔离呢,它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务 ”
说明:这篇文章仅仅举一个实际的测试例子同解决方案来解释一下服务隔离的意义,详细的说明我们暂且略过,在后续的文章中我们再看。
01
—
测试
假设现在有一个应用提供了两个接口,A和B,某时刻,A访问量激增,此时整个应用并没有去考虑到这些突发事件,那么对于整个服务或者B接口有什么影响呢?
这里我们使用Spring Boot写两个接口
@RequestMapping(value = "/A", method = RequestMethod.POST)
public void A() throws InterruptedException {
int x = i.incrementAndGet();
Thread.sleep(5000);
logger.info("===================== 成功执行" + x);
}
@RequestMapping(value = "/B", method = RequestMethod.GET)
public void B() throws InterruptedException {
logger.info("=====================其他服务执行");
}
这两个接口并无实际意义,A在访问时候使线程睡眠,模拟某些耗时的计算或者IO操作
然后我们使用jmeter工具在测试线程。对于A使用500的并发请求,
对于B使用21个并发请求
最开始的时候我执行这个请求发现也能执行,就是把A的并发量改为1000同样执行没问题,于是想到了Spring Boot内嵌的Tomcat最大连接数和线程默认是10000,于是修改这两个参数:
有兴趣的小伙伴可以使用jconsole来监控一下请求线程的变化。
server.tomcat.max-connections=20
server.tomcat.max-threads=20
此时我们再次执行jmeter就出现了问题,在处理大概是第300个请求的时候,后面的请求出现了超时。
对于B我们会发现除了最开始的请求成功了,后续的其他全部失败。
这个测试想表达的意思是,A服务请求激增,对于B也产生了很大的影响,但是局部的从B的访问量21个来看,并没有所谓的高并发,请求失败并不合理(整体看很合理,因为A占用了所有的资源)。
这里举网上看到的例子,造船行业有一个专业术语叫做「舱壁隔离」。利用舱壁将不同的船舱隔离起来,如果某一个船舱进了水,那么就可以立即封闭舱门,形成舱壁隔离,只损失那一个船舱,其他船舱不受影响,整个船只还是可以正常航行。
上述的问题就是因为A服务没有隔离,让A船舱进了水(耗尽了所有的资源),导致整艘船不可用了。
02
—
解决方案
最开始想解决方案的时候,想到Dubbo的服务降级,但是由于dubbo太久没用了,弄了半天没成功,所以这里我使用hystrix集成到Spring Boot中来解决这一问题,Hystrix是由Netflix开源的一个服务隔离组件,通过服务隔离来避免由于依赖延迟、异常,引起资源耗尽导致系统不可用的解决方案。
Hystrix内部提供了两种模式执行逻辑:信号量、线程池。默认的是线程池。
信号量暂且不说,在线程池的模式下,用户请求会被提交到各自的线程池中执行,把执行每个下游服务的线程分离,从而达到资源隔离的作用。当线程池来不及处理并且请求队列塞满时,新进来的请求将快速失败,可以避免依赖问题扩散
首先导入Maven依赖
<!--hystrix-->
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
<version>1.5.12</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
<version>1.5.12</version>
</dependency>
然后在A服务上加上注解:
@RequestMapping(value = "/A", method = RequestMethod.POST)
@HystrixCommand(
fallbackMethod = "getOrderPageListFallback",
groupKey = "A",
threadPoolProperties = {
//10个核心线程池,超过20个的队列外的请求被拒绝;
//当一切都是正常的时候,线程池一般仅会有1到2个线程激活来提供服务
@HystrixProperty(name = "coreSize",
value = "10"),
@HystrixProperty(name = "maxQueueSize",
value = "10"),
@HystrixProperty(name = "queueSizeRejecti"+
"onThreshold", value = "20")},
commandProperties = {
@HystrixProperty(name = "execution.isolation.
thread.timeoutInMilliseconds", value = "10000"),
//命令执行超时时间
@HystrixProperty(name = "circuitBreaker.
requestVolumeThreshold", value = "2"),
//若干10s一个窗口内失败三次, 则达到触发熔断的最少请求量
@HystrixProperty(name = "circuitBreaker.
sleepWindowInMilliseconds", value = "30000")
//断路30s后尝试执行, 默认为5s
})
public void getOrderPageList() throws InterruptedException {
int x = i.incrementAndGet();
Thread.sleep(5000);
logger.info("===================== 成功执行" + x);
}
其中getOrderPageListFallback为降级后执行的方法
public void getOrderPageListFallback() {
int x = i.incrementAndGet();
logger.error("================= 资源耗尽,服务不可用" + x);
}
此时我们重启服务,再用jmeter测试一下。
我们会发现B接口全部可用
此时控制台日志,我数一下成功执行的请求是20个,其它的全部降级处理,这里我们就使用hystrix成功的实现服务隔离,当然有很多问题还没有去说明,只是稍加注解,比如说接口熔断和恢复,我们在后面的文章继续研究。
03
—
小结
为什么要做服务隔离设计呢?
我们在做系统设计的时候,必须有一个清楚的认知是:任何软件系统,故障是不可避免的,并且大多数还是不可预测的,因此,我们只能在系统的设计之初就充分的考虑好应对措施,如何在故障发生时,去尽最大可能的止损和减少故障范围。
没有人敢说他的系统是百分百可用,我们能做的就是,使用一切方法去减少故障的影响面,尽可能的去提高系统的整体可用率。
而把系统分离成子服务,将子服务进行一定程度隔离的做法,能保证在有不可预测的故障发生时,缩小故障范围的最佳手段。
在该模式下,接收请求和执行下游依赖在同一个线程内完成,不存在线程上下文切换所带来的性能开销,所以大部分场景应该选择信号量模式,但是在下面这种情况下,信号量模式并非是一个好的选择。
比如一个接口中依赖了3个下游:serviceA、serviceB、serviceC,且这3个服务返回的数据互相不依赖,这种情况下如果针对A、B、C的熔断降级使用信号量模式,那么接口耗时就等于请求A、B、C服务耗时的总和,无疑这不是好的方案。
线程池模式
在该模式下,用户请求会被提交到各自的线程池中执行,把执行每个下游服务的线程分离,从而达到资源隔离的作用。当线程池来不及处理并且请求队列塞满时,新进来的请求将快速失败,可以避免依赖问题扩散。
在信号量模式提到的问题,对所依赖的多个下游服务,通过线程池的异步执行,可以有效的提高接口性能。
优势
减少所依赖服务发生故障时的影响面,比如ServiceA服务发生异常,导致请求大量超时,对应的线程池被打满,这时并不影响ServiceB、ServiceC的调用。
如果接口性能有变动,可以方便的动态调整线程池的参数或者是超时时间,前提是Hystrix参数实现了动态调整。
缺点
请求在线程池中执行,肯定会带来任务调度、排队和上下文切换带来的开销。
因为涉及到跨线程,那么就存在ThreadLocal数据的传递问题,比如在主线程初始化的ThreadLocal变量,在线程池线程中无法获取
注意
因为Hystrix默认使用了线程池模式,所以对于每个Command,在初始化的时候,会创建一个对应的线程池,如果项目中需要进行降级的接口非常多,比如有上百个的话,不太了解Hystrix内部机制的同学,按照默认配置直接使用,可能就会造成线程资源的大量浪费。