随着互联网发展,传统的大型机单块应用到后来的分布式计算及发展到现在的微服务,Docker的发展更是让微服务如虎添翼。微服务的优点不必细说,网上一搜一大把,这里就聊一下微服务带来的问题和如何去解决这些问题。通过这些问题分析下Hystrix是如何成为设计微服务利器的。
微服务带来的问题:
一、增加服务依赖复杂度:单块应用拆分成微服务之后,原来运行在同一个JVM进程内的功能,可能要分到多个JVM进程中,拆分后的微服务之间通过RPC调用来共同完成之前的功能,这就必然会增加服务依赖的复杂度;
二、不能FailFast : 原来系统是在同一个JVM进程内的,不存在网络调用,异常就会直接失败,不会导致处理hang住。微服务之后,逻辑功能则是由网络上的另一个服务提供的,如果提供者处理速度很慢,甚至最后会失败(超时),服务的请求端在返回失败之前就会一直hang在这里;另外在一段时间内90%的请求都返回失败,第91个请求就不需要再发送给服务端以增加服务端的压力了,直接fast fail,走后续流程即可。一段时间之后,再次尝试请求。
三、不能平均分配资源:微服务之后,服务的依赖会增多,每个依赖的服务端性能和吞吐量都不尽相同,这时就要隔离每个依赖所占有的资源。比如某个机器P1有三个服务SA、SB、SC,分别依赖于A、B、C三个依赖,一段时间之后,A依赖的服务端响应速度开始变慢,这时S1所在机器的CPU、线程、IO等资源都会耗尽到A依赖上,而没有空闲CPU、线程、IO等资源去处理B、C的依赖。以至于S1不仅不能对外正常提供SA服务,连SB、SC服务都会受到影响,最后甚至会导致机器P1宕机。
以上提到的这三点主要问题也是Hystrix提供的核心功能:监控、熔断器、隔离。
Hystrix简介
Hystrix天生就是为微服务提供的,是由Netflix公司开发,目前为Netflix公司每天处理着数百亿的线程隔离和更多的信号隔离请求,代码托管在Github(https://github.com/Netflix/Hystrix)上。Hystrix的监控可以随时查看一个微服务运行的健康情况,内部线程、信号占用率;熔断器则可以实现快速失败,同时还能保护后端服务;隔离机制更是为微服务的高效运行保驾护航,当某一个依赖出现异常时能够及时进行降级保护。
隔离
上面提到计算及的CPU、线程、IO等资源都是非常宝贵而且是有限的,当被使用达到上限时,那么距离该机器宕机也就不远了。另外一个容器对外提供的服务会有多个,可能每个服务都有一个依赖,当某一个依赖运行出现异常时,只能使该依赖对应的那个服务对外不可用,而其他的服务都应该是正常运行的。
而现实中恰恰是当某一个服务不可用时,紧接着第二个第三个服务都会出现问题。这就是没有对依赖进行隔离的原因。因为有一个依赖运行异常,该机器上的所有CPU、线程等资源都过来处理这个依赖资源的请求了,没有空闲的资源去处理其他服务的请求。这时就需要对每个依赖进行资源隔离。当某个依赖出现问题,只会有分配给该依赖的线程、IO等处于忙碌状态,其他线程、IO等资源应该正常处理其他的依赖。
Hystrix针对依赖资源的隔离提供了两种策略,分别是:线程池隔离和信号隔离。
为什么用Hystrix
在一个复杂的分布式架构系统中,依赖服务是避免不了的,特别是在一个微服务化的时代。如果一个系统不能对相关的依赖进行隔离,那该系统就会有被拖垮被宕机的风险。在一个高并发高访问量的系统中,当一个被依赖的服务出现网络连接缓慢、处理延迟等情况,在几分钟甚至几秒内,所有的线程都会block在被依赖的资源上,进而导致所有资源被耗尽,系统被拖垮。
所有依赖服务都正常时:
当其中一个依赖出现延迟时,服务器的大部分线程都会block在依赖I上,在一个高流量的系统中,几秒内所有的线程资源都被blocking在了依赖上,从而导致系统的其他服务也不可用,甚至整个系统被拖垮,不可用。
Hystrix对依赖进行隔离和控制,通常这些依赖都是网络调用。Hystrix通过线程池和信号进行隔离,给每一个依赖分配指定的资源,当该依赖的资源不足(线程池爆满、信号tryAcquire) 就会直接返回失败,不会占用其他依赖的资源。
同时,Hystrix还提供了CircuitBreaker机制,当失败率达到某一个阀值时,Hystrix会Fast Fail 并迅速恢复,或者优雅的降级。
环境搭建
使用Maven搭建的项目,在pom.xml文件中引入hystrix-core坐标:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.4.18</version>
<artifactId>hystrix-core</artifactId>
<version>1.4.18</version>
</dependency>
环境搭建好之后就可以开发了,接下来会写如何开发Hystrix应用。
Hystrix开发
首先还从HelloWorld 开始,首先定义一个Command类,继承
HystrixCommand
类,在构造函数中设置相关参数。重写run() 和 getFallback() ,业务逻辑实现放在run方法里。所有非
HystrixBadRequestException
异常都会调用
getFallback方法,降级方案一般在getFallback方法中实现。
class HelloWorldCommand extends HystrixCommand<String> {
protected HelloWorldCommand(String name) {
super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(name)));
}
@Override
protected String run() throws Exception {
System.out.println("run success " + Thread.currentThread().getName());
return "run success " + Thread.currentThread().getName();
}
protected HelloWorldCommand(String name) {
super(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(name)));
}
@Override
protected String run() throws Exception {
System.out.println("run success " + Thread.currentThread().getName());
return "run success " + Thread.currentThread().getName();
}
}
很简单,在run方法里面返回了当前的线程名字。执行下 TestCase ,观察打印数据
@Test
public void helloWorldCommand() {
HelloWorldCommand helloWorldCommand = new HelloWorldCommand("klov");
String result = helloWorldCommand.execute();
System.out.println("【HelloWorldCommand】 result = "+result);
public void helloWorldCommand() {
HelloWorldCommand helloWorldCommand = new HelloWorldCommand("klov");
String result = helloWorldCommand.execute();
System.out.println("【HelloWorldCommand】 result = "+result);
}
打印数据如下:
run success hystrix-klov-1
【HelloWorldCommand】 result = run success hystrix-klov-1
由输出数据可知线程池的名字默认是 hystrix+commandGroupKey+inde。
到此,Hystrix的开发环境以及基本的开发已经完成,后面会继续介绍Hystrix两个隔离方式的开发及原理。
线程池隔离
使用线程会在以下三个场景带来性能消耗:
1、线程的创建和销毁;
2、线程上下文空间的切换,线程池调度需要操作系统介入,系统需要从用户态空间切换到内核态空间,调度完成后又需要切回到用户态空间。
Hystrix通过使用固定线程池大小的方式解决了第一个问题,在创建线程池时,Hystrix只允许你设置CoreSize,而不允许你设置MaxSize。在系统初始化线程池时,MaxSize等于CoreSize。这样就避免了线程的频繁创建和销毁带来的性能消耗。Hystrix的线程池创建源码是在
HystrixConcurrencyStrategy类getThreadPool方法
中实现的:
public ThreadPoolExecutor getThreadPool(final HystrixThreadPoolKey threadPoolKey, HystrixProperty<Integer> corePoolSize, HystrixProperty<Integer> maximumPoolSize, HystrixProperty<Integer> keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
return new ThreadPoolExecutor(corePoolSize.get(), maximumPoolSize.get(), keepAliveTime.get(), unit, workQueue, new ThreadFactory() {
protected final AtomicInteger threadNumber = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "hystrix-" + threadPoolKey.name() + "-" + threadNumber.incrementAndGet());
thread.setDaemon(true);
return thread;
}
});
}
跟普通的创建线程池没什么区别,根据传进来的线程池参数创建一个线程池。
但是玄机出现在调用的地方,传递的corePoolSize和maximumPoolSize是同一个值
。调用方是HystrixThreadPool类的173行
public HystrixThreadPoolDefault(HystrixThreadPoolKey threadPoolKey, HystrixThreadPoolProperties.Setter propertiesDefaults) {
this.properties = HystrixPropertiesFactory.getThreadPoolProperties(threadPoolKey, propertiesDefaults);
HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
this.queueSize = properties.maxQueueSize().get();
this.queue = concurrencyStrategy.getBlockingQueue(queueSize);
this.metrics = HystrixThreadPoolMetrics.getInstance(
threadPoolKey,
//这里是调用线程池创建的地方,corePoolSize和maximumPoolSize传递的都是coreSize()这个值
concurrencyStrategy.getThreadPool(threadPoolKey,properties.coreSize(), properties.coreSize(), properties.keepAliveTimeMinutes(), TimeUnit.MINUTES, queue),
properties);
this.threadPool = metrics.getThreadPool();
/* strategy: HystrixMetricsPublisherThreadPool */
HystrixMetricsPublisherFactory.createOrRetrievePublisherForThreadPool(threadPoolKey, this.metrics, this.properties);
}
信号量隔离
为解决线程池隔离带来的第二性能开销的场景,Hystrix使用了信号量隔离。信号量隔离通过原子操作类AtoInteger实现Permits的管理,AtoInteger类使用的是CAS操作,相对于锁,CAS是通过硬件实现的原子操作,减小了性能开销。下面是获取Permit的源码分析:
public boolean tryAcquire() {
//count是AtoInteger类型的成员变量
int currentCount = count.incrementAndGet();
if (currentCount > numberOfPermits.get()) {
count.decrementAndGet();
return false;
} else {
return true;
}
}
tryAcquire获取Permit成功,调用方要在finally中释放Permit,调用release方法。
public void release() {
//对Permit进行减一操作
count.decrementAndGet();
}
建议不同的业务之间通过线程池隔离,同一个业务不同的依赖资源则可以通过信号量隔离,以提高吞吐量和性能。
熔断器
熔断器的功能等同于家庭电路中的自动跳闸器,当电路中流经的电流负荷过高或者有漏电等非安全用电情况时,跳闸器就会自动跳闸,起到对家用电器保护的作用。软件系统中的熔断器和跳闸器是类似的,当请求回路中发生异常时,熔断器打开。只是Hystrix会在一段时间后试着关闭回路,让部分请求发送成功,以检测异常是否恢复。
熔断器机制是指,在后端服务可用率降低到阀值以下时,新来的请求不再发给后端服务,直接返回请求失败即可,以实现 Fail-Fast机制,快速给用户请求,执行后续的失败流程。这样既可以拦截不必要的请求,减少对后端本来就异常的服务的压力,还可以实现Fail-Fast机制。
Hystrix默认熔断器机制是开启的
,可以在
HystrixCommand的run方法中设置为开启
,设置代码如下:
HystrixCommandProperties.Setter().withCircuitBreakerEnabled(true)
开启熔断器机制后, Hystrix默认的阀值是50% 。如果在 10秒内请求失败率 达到50%及以上,Hystrix会自动断开回路,后面的请求不会再被发往后端的服务器中,会直接返回给客户端。 5秒之后 ,Hystrix会试着关闭回路,放一部分请求过去,以检测异常是否恢复。
HystrixCircuitBreakerImpl类的allowSingleTest方法中实现该功能,源码如下:
public boolean allowSingleTest() {
long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();
// 1) if the circuit is open
// 2) and it's been longer than 'sleepWindow' since we opened the circuit
if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested +properties.circuitBreakerSleepWindowInMilliseconds().get()) {
// We push the 'circuitOpenedTime' ahead by 'sleepWindow' since we have allowed one request to try.
// If it succeeds the circuit will be closed, otherwise another singleTest will be allowed at the end of the 'sleepWindow'.
if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {
// if this returns true that means we set the time so we'll return true to allow the singleTest
// if it returned false it means another thread raced us and allowed the singleTest before we did
return true;
}
}
return false;
}
properties.circuitBreakerSleepWindowInMilliseconds().get()取的值就是设置的间隔时间,默认是5秒
。在每次打开熔断器时都要保存当前时间,在下次决策是否需要关闭回路时,判断距离上次开启时间是否达到设置的值。在Hystrix中大量使用了CAS在保证成员变量的原子操作前提下,又提高了性能。