天天看点

SpringCloud进阶-详解如何Hystrix实战服务容错(一)

作者:架构师面试宝典
SpringCloud进阶-详解如何Hystrix实战服务容错(一)

首先,通过前面的介绍,我们知道了在微服务的架构中,存在多个可以调用的服务的时候,如果这些服务在调用过程中其中一个服务出现故障之后就会引起各种的连锁反应,最终导致整个系统的不可用,这种情况有一个专业术语叫做服务雪崩。那么我们如何解决服务雪崩的问题呢?在Spring Cloud中提供了Hystrix组件用来解决这个问题。

SpringCloud进阶-详解如何Hystrix实战服务容错(一)

当然,现在市面上有很多的解决服务雪崩的解决方案,有兴趣的读者也可以了解相关的解决方案。这里我们重点介绍一下Hystrix,通过Hystrix来看一下,在解决服务雪崩问题的时候都有那些需要注意的点。

简单介绍

Hystrix是Netflix用来解决微服务分布式系统服务熔断保护机制的中间件,相当于电路中的空气开关,如果发生电路异常的时候会保护电路。

在整个的微服务体系中,有着各种错综复杂的服务调用,如果不对服务进行熔断保护就会出现上面我们提到的服务雪崩,这就会导致整个的服务不可用。而Hystrix就提供了一个服务熔断保护机制,当服务出现问题的时候,就会对出问题的服务进行隔离,保证其他服务不会出现问题,导致各种连锁反应,从而实现服务降级的操作。

Hystrix的案例

首先我们来创建一个Maven项目并且在项目中引入如下的依赖。这个依赖就是为项目中增加Hystrix的依赖。

<dependency>
	<groupId>com.netflix.hystrix</groupId>
	<artifactId>hystrix-core</artifactId>
	<version>1.5.18</version>
</dependency>           

接下来我们需要编写一个HystrixCommand类。

public class MyHystrixCommand extends HystrixCommand<String> {

	private final String name;

	public MyHystrixCommand(String name) {
		super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
		this.name = name;
	}
	@Override
	protected String run() {
		System.err.println("get data");   
		return this.name + ":" + Thread.currentThread().getName();
	}
}
           

需要注意的是在HystrixCommand的构造函数中我们需要先对其设置一个GroupKey的值。而具体的方法实现逻辑是在继承了run()方法中来实现,这里我们向控制台输出了一个当前线程的名字。然后可以通过测试方法来调用这个方法。

public class HystrixMainApplication {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		String result = new MyHystrixCommand("架构师").execute(); 
		System.out.println(result);	
	}
}
           

从输出结果上来看,我们设置在构造函数中的GroupKey成了线程的名字。

而上面这种方式是通过同步调用的方式来实现的。当然我们还可以通过异步的方式来实现这种操作。如下

public class HystrixMainApplication {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		Future<String> future = new MyHystrixCommand("架构师").queue();
		System.out.println(future.get()); 
	}
}           

通过上面这种方式可以实现对于方法的异步调用,也就是说不需要等到线程结束获取调用结果。就可以执行后续的操作。

回退支持

在一些场景中,由于网络的原因,或者是数据库查询的原因会导致调用超时的情况发生,这个时候,我们就需要对HystrixCommand进行一个简单的改造,使其支持如果在调用超时之后可以调用其他稳定的接口来保证系统的稳定性等问题。

public class MyHystrixCommand extends HystrixCommand<String> {

	private final String name;

	public MyHystrixCommand(String name) {
		super(HystrixCommandGroupKey.Factory.asKey("MyGroup"));
		this.name = name;
	}

	@Override
	protected String getFallback() {
		return "调用失败";
	}

	@Override
	protected String run() {
		System.err.println("get data");   
		return this.name + ":" + Thread.currentThread().getName();
	}
}           

在HystrixCommand中有一个getFallback()方法就是来完成这个操作的。然后我们需要在run方法中设置一个调用超时时间。

@Override
protected String run() {
	try {
		Thread.sleep(1000 * 10);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
	return this.name + ":" + Thread.currentThread().getName();
}           

继续执行上面的代码,就会发现返回值是调用失败,也就是说执行了回退方法。在很多场景下,这种方式可以很好地保证系统调用的稳定性。

资源隔离

比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。

三个服务均是通过RPC调用。当依赖的订单服务变慢了,而这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩。

所以,有必要将多个依赖服务的调用分别隔离到各自自己的资源池内,不对其他服务造成影响。Hystrix为我们提供了两种资源隔离策略。一种是信号量策略,一种是线程隔离策略。下面我们分别来看一下这两种策略。

信号量隔离策略

关于信号量隔离,用于隔离本地代码或可快速返回的远程调用(如memcached,redis)可以直接使用信号量隔离,降低线程隔离的上下文切换开销。

线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此hystrix提供了信号量隔离。

主要适用场景: 并发需求不大的依赖调用(因为如果并发需求较大,相应的信号量的数量就要设置得够大,因为Tomcat线程与处理线程为同一个线程,那么这个依赖调用就会占用过多的Tomcat线程资源,有可能会影响到其他服务的接收)

和线程池隔离类似,同一个HystrixCommandGroupKey共用一个信号量(默认为类名)

public MyHystrixCommand(String name) {

    super(HystrixCommand.Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("MyGroup"))
            .andCommandPropertiesDefaults(
                    HystrixCommandProperties.Setter()
                            .withExecutionIsolationStrategy(
                                    HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE

                            )
            )
    );
    this.name = name;
}           

线程隔离策略

适用场景:适合绝大多数的场景,对依赖服务的网络调用timeout,TPS要求高的这种问题

执行依赖代码的线程与请求线程(比如Tomcat线程)分离,请求线程可以自由控制离开的时间,这也是我们通常说的异步编程,Hystrix是结合RxJava来实现的异步编程。通过为每个包裹了HystrixCommand的API接口设置独立的、固定大小的线程池(hystrix.threadpool.default.coreSize)来控制并发访问量,当线程饱和的时候可以拒绝服务(走fallback方法),防止依赖问题扩散。

线上建议线程池不要设置过大,否则大量堵塞线程有可能会拖慢服务器。

public MyHystrixCommand(String name) {
	 super(HystrixCommand.Setter.withGroupKey(
	           HystrixCommandGroupKey.Factory.asKey("MyGroup"))                 
	         .andCommandPropertiesDefaults(     
	             HystrixCommandProperties.Setter()     
	             .withExecutionIsolationStrategy(      
	               HystrixCommandProperties.ExecutionIsolationStrategy.THREAD 
	             )                 
	         ).andThreadPoolPropertiesDefaults(    
	             HystrixThreadPoolProperties.Setter()      
	               .withCoreSize(10)                
	 	       .withMaxQueueSize(100)          
	       	       .withMaximumSize(100)               
	         )         
	);       
	this.name = name;
}           

线程池隔离:

  • 1、调用线程和hystrixCommand线程不是同一个线程,并发请求数受到线程池(不是容器tomcat的线程池,而是hystrixCommand所属于线程组的线程池)中的线程数限制,默认是10。
  • 2、这个是默认的隔离机制
  • 3、hystrixCommand线程无法获取到调用线程中的ThreadLocal中的值

信号量隔离:

  • 1、调用线程和hystrixCommand线程是同一个线程,默认最大并发请求数是10
  • 2、调用速度快,开销小,由于和调用线程是处于同一个线程,所以必须确保调用的微服务可用性足够高并且返回快才用

注意:如果发生找不到上下文的运行时异常,可考虑将隔离策略设置为SEMAPHONE。