在以前的文章中,我们介绍过使用Gauva实现限流的功能,现在我们来了解一下如何在服务框架中实现熔断和降级的方法。
简介Hystrix
大型系统架构的演进基本上都是这样一个方向:从单体应用到分布式架构。这个演进过程离不开一个字“拆”,我们会把一个系统拆成很多子系统,子系统之间一定会存在依赖,有的是强依赖(比如Http调用),有的是弱依赖(比如,使用消息中间件)。“拆”的主要意义之一就是一个子系统挂了不影响其他的服务正常运行,而对于那么我们拆出来强依赖的服务如果挂了,还是会影响整个系统,所以我们光“拆”是没有达到效果的。
Hystrix源于Netflix,该库可以对那些在访问远程系统、服务、第三方库或者其他方法调用时所产生的延迟或者故障提供更强大的容错能力。说白了就是提供了一种简洁、灵活和强大的框架去处理依赖异常,一个服务挂了不影响其他的服务正常运行,也是微服务概念中熔断,降级的具体框架实现。
它主要的功能如下:
1.对第三方组件进行访问(特别是网络访问)时出现的延迟或者的异常进行保护和控制。
2.在复杂的分布式系统中,停止级联故障。
3.快速失败,快速恢复。
4.可以优雅的降级。
5.可以提供近实时的监控、告警和操作控制。
典型场景:雪崩效应
Hystrix典型的场景就是处理复杂的分布式调用过程中依赖服务延迟或者异常的场景。下图是一张服务依赖图,用户的请求依赖于跨网络的A、H、I、P四个其他的服务。
如果各个服务运行的都很好,那么不会有问题。在大型系统中,随着服务越来越多,网络或者性能问题引起的超时,逻辑的异常带来的不可访问,都是必然会出现的情况。
如上图,一个用户请求的时候,其中的一个依赖服务I出现了延迟,那么整个的这个用户请求将被阻塞。
随着大量流量的增加,一个单一的后端依赖会成为潜在风险,可能导致整个环境的软硬件资源出现问题。不单单这个服务会挂掉,这些问题还有可能,影响环境中的消息队列、线程等其他资源,从而导致系统上的级联故障。最终出现下图的情况。
网络连接失败或降级,服务和服务器失败或变慢,新的库或服务部署改变行为或性能特征,客户端库有bug等等,所有这些都有可能会造成上述的情况,这就需要我们要隔离和管理这些故障和延迟,以便一个失败的依赖项不能影响整个应用程序或系统。而Hystrix正是解决这类问题的。
Hystrix怎样解决这些问题
1.Hystrix使用命令模式HystrixCommand(Command)包装依赖调用逻辑,每个命令在单独线程中/信号授权下执行。
2.可配置依赖调用超时时间,超时时间一般设为比99.5%平均时间略高即可.当调用超时时,直接返回或执行fallback逻辑。
3.为每个依赖提供一个小的线程池(或信号),如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
4.依赖调用结果分:成功,失败(抛出异常),超时,线程拒绝,短路。 请求失败(异常,拒绝,超时,短路)时执行fallback(降级)逻辑。
5.提供熔断器组件,可以自动运行或手动调用,停止当前依赖一段时间(10秒),熔断器默认错误率阈值为50%,超过将自动运行。
6.提供近实时依赖的统计和监控
Hystrix把每个潜在的依赖性进行包装,把每一个依赖项都与另一个依赖项隔离开来,当潜在的问题发生时能限制它的资源,并在调用发生异常时能够决定如何应答给调用方,从而快速的恢复处理,不影响下面的调用。如下图,Hystrix对依赖I配置了5个线程。当调用依赖I的时候,Hystrix配置了两种策略,异常和超时,那么只要有一样触发,或者线程被占满,就可以马上进入我们预设的异常处理逻辑。
示例程序HelloWorld
下面我们用一个简单的例子来展示怎样使用Hystrix来处理上面的场景。一个接口调用超过5秒钟或者出现异常的时候,走另外的降级逻辑。
1.pom文件,引用jar包
<dependency> <groupId>com.netflix.hystrix</groupId> <artifactId>hystrix-core</artifactId> <version>1.5.12</version> </dependency>
2.使用Hystrix的命令基类HystrixCommand封装调用逻辑,在执行方法体中,我模拟了3种调用结果:分别是正常调用、调用超时(5s)、和接口中出现异常。
import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandProperties; import java.util.concurrent.TimeUnit; /** * 使用HystrixCommand封装执行方法。继承Hystrix命令基类,泛型参数即调用方法的返回参数 */ public class HelloWorldCommand extends HystrixCommand<String> { private final String name; /** * 定义构造函数,参数即被包装方法的输入参数 * @param name */ public HelloWorldCommand(String name) { //定义命令组 和 方法调用超时时间 super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup")) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(5000))); this.name = name; } /** * 封装业务逻辑的方法体,在这里执行真正的业务逻辑。 * @return * @throws Exception */ @Override protected String run() throws Exception { //例子中,模拟执行超时 if (name.equals("TimeoutCommand")) { TimeUnit.SECONDS.sleep(6); } //例子中,模拟出现异常 if (name.equals("ExceptionCommand")) { throw new Exception("ExceptionCommand"); } //例子中,模拟正常返回 return "Hello " + name + "!"; } /** * 降级方法定义,即,run方法中出现异常后应该执行的方法。包括例子中的超时。 * @return */ @Override protected String getFallback() { return "Command failed!"; } }
3.调用方法
public class HelloWorld
{
public static void main(String[] args)
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s = new HelloWorldCommand("Nick").execute();
System.out.println(sdf.format(new Date()) + "->" + s);
s = new HelloWorldCommand("TimeoutCommand").execute();
System.out.println(sdf.format(new Date()) + "->" + s);
s = new HelloWorldCommand("ExceptionCommand").execute();
System.out.println(sdf.format(new Date()) + "->" + s);
}
}
调用方法模拟了3中不同的结果:正常调用、调用超时(5s)、和接口中出现异常。结果如下:
2017-12-24 17:58:18->Hello Nick!
2017-12-24 17:58:23->Command failed!
2017-12-24 17:58:23->Command failed!