文章目录
1. 为什么需要Hystrix?
在大中型分布式系统中,通常系统很多依赖服务,比如商城系统中支付服务、商品服务、购物车服务,用户服务等等。比如APP依赖于商品服务、用户服务、商家服务和订单服务,而订单服务依商品服务、商家服务、用户服务。依赖关系如下:
而当商家服务不可用时,在APP服务对商家服务的请求就会阻塞、重试,这是大量的请求进入APP服务。那么APP服务中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,这种连锁的反应叫做雪崩。
在这种时候,就需要我们的熔断机制来挽救整个系统。
Hystrix是一个有关延迟和失败容错的开源库包,用来设计隔离访问远程系统端点或微服务等,防止级联爆炸式的失败,也就是由一个小问题引起接二连三扩大的疯狂的错误爆炸直至整个系统瘫痪,能够让复杂的分布式系统更加灵活具有弹性,同时也是可以帮我们做依赖服务的治理和监控.
Hystrix如何解决依赖隔离?
- hystrix把每个依赖都进行隔离,对依赖的调用全部包装成HystrixCommand或者HystrixObservableCommand,每个Command在单独线程中/信号授权下 执行。
- 对依赖的调用耗时设置阀值,如果超过阀值直接判定超时
- 对每个依赖维护一个连接池,如果连接池满直接拒绝访问
- 依赖调用结果分:成功,失败(抛出异常,超时,线程拒绝,短路)。 请求失败(异常,拒绝,超时,短路)时执行fallback(降级)逻辑。
- 提供熔断器组件,可以自动运行或手动调用,停止当前依赖一段时间(10秒),熔断器默认错误率阈值为50%,超过将自动运行。
- 提供近实时依赖的统计和监控
2. Hellowrd
Hystrix提供了Command模式的接入方式,如果一个服务CommandHelloWorld需要熔断降级的能力,那么只需要继承Hystrix的HystrixCommand类,并重写run方法和getFallback方法。
public class CommandHelloWorld extends HystrixCommand<String> {
private final String name;
public CommandHelloWorld(String name) {
//最少配置:指定命令组名(CommandGroup)
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
// 省略业务逻辑
// 该方法可能会抛出异常
return "Hello " + name + " thread:" + Thread.currentThread().getName();
}
@Override
protected String getFallback() {
return "Hello Failure " + name + "!";
}
//调用实例
public static void main(String[] args) throws Exception {
//重复调用对应异常信息:This instance can only be executed once. Please instantiate a new instance.
CommandHelloWorld helloWorldCommand = new CommandHelloWorld("Hello word !");
String result = helloWorldCommand.execute();
}
}
代码很简单,声明一个类CommandHelloWorld,集成HystrixCommand, HystrixCommand携带泛型,泛型的类型就是我们的执行方法run()返回的结果的类型。逻辑执行体就是run方法的实现。构造方法至少要传递一个分组相关的配置给父类才能实现实例化。
3. HystrixCommand基本配置
Group & Key
Hystrix把执行都包装成一个HystrixCommand,并启用线程池实现多个依赖执行的隔离。Hystrix每个command都有对应的commandKey可以认为是command的名字,默认是当前类的名字getClass().getSimpleName(),每个command也都一个归属的分组,这两个东西主要方便Hystrix进行监控、报警等。HystrixCommand使用的线程池也有线程池key。
//command group
HystrixCommandGroupKey.Factory.asKey("ExampleGroup")
//command key
HystrixCommandKey.Factory.asKey("HelloWorld")
//线程池 key
HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")
举个粒子:
public CommandHelloWorld(){
super(Setter
//分组key
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("helloWorldGroup"))
//commandKey
.andCommandKey(HystrixCommandKey.Factory.asKey("commandHelloWorld"))
//command属性配置
.andCommandPropertiesDefaults(HystrixPropertiesCommandDefault.Setter().withCircuitBreakerEnabled(true).withCircuitBreakerForceOpen(true))
//线程池key
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("helloWorld_Poll"))
//线程池属性配置
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(20).withMaxQueueSize(25))
);
}
4. 同步和异步
CommandHelloWorld helloWorldCommand = new CommandHelloWorld("Synchronous-hystrix");
//使用execute()同步调用代码,效果等同于:helloWorldCommand.queue().get();
String result = helloWorldCommand.execute();
System.out.println("result=" + result);
helloWorldCommand = new CommandHelloWorld("Asynchronous-hystrix");
//异步调用,可自由控制获取结果时机,
Future<String> future = helloWorldCommand.queue();
//get操作不能超过command定义的超时时间,默认:1秒
result = future.get(100, TimeUnit.MILLISECONDS);
System.out.println("result=" + result);
System.out.println("mainThread=" + Thread.currentThread().getName());
异步调用使用 command.queue()get(timeout, TimeUnit.MILLISECONDS),同步调用使用command.execute() 等同于 command.queue().get()。
5. request cache
Hystrix中的cache,在上下文中多次请求同一个Command,如果启用cache,那么run方法执行一次,返回给所有的请求。其实hystrix内部有一个类似于Hasnmap的数据结构保存Command的执行结果。
public class CacheCommand extends HystrixCommand<String> {
//查找执行结果的key
private String key;
private static final HystrixCommandKey COMMANDKEY = HystrixCommandKey.Factory
.asKey("CachedCommand_cmd");
public CacheCommand(String key) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("CachedCommand"))
.andCommandKey(COMMANDKEY));
this.key = key;
}
@Override
protected String getCacheKey() {
return this.key;
}
//清除缓存
public static void flushCache(String key) {
HystrixRequestCache.getInstance(COMMANDKEY,
HystrixConcurrencyStrategyDefault.getInstance()).clear(key);
}
@Override
protected String run() throws Exception {
return "I'm cache";
}
public static void main (String []args) {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
CacheCommand cacheCommand1 = new CacheCommand("cachekey");
CacheCommand cacheCommand2 = new CacheCommand("cachekey");
cacheCommand1.execute();
cacheCommand2.execute();
System.out.println(cacheCommand2.isResponseFromCache());
context.shutdown();
}
}
6. 请求合并
请求合并就是将多个请求合并到一个线程/信号下批量执行。使用请求合并来减少执行并发HystrixCommand执行所需的线程数和网络连接数。
下图显示了两种情况下的线程数和网络连接数:没有使用请求合并和请求合并的对比:
public class CommandCollapserTest extends
HystrixCollapser<List<String>, String, Integer> {
private final Integer key;
public CommandCollapserTest(Integer key) {
this.key = key;
}
@Override
public Integer getRequestArgument() {
return key;
}
@Override
protected HystrixCommand<List<String>> createCommand(
final Collection<CollapsedRequest<String, Integer>> requests) {
//创建返回command对象
return new BatchCommand(requests);
}
@Override
protected void mapResponseToRequests(List<String> batchResponse,
Collection<CollapsedRequest<String, Integer>> requests) {
int count = 0;
for (CollapsedRequest<String, Integer> request : requests) {
//手动匹配请求和响应
request.setResponse(batchResponse.get(count++));
}
}
private static final class BatchCommand extends HystrixCommand<List<String>> {
private final Collection<CollapsedRequest<String, Integer>> requests;
private BatchCommand(Collection<CollapsedRequest<String, Integer>> requests) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetValueForKey")));
this.requests = requests;
}
@Override
protected List<String> run() {
ArrayList<String> response = new ArrayList<String>();
for (CollapsedRequest<String, Integer> request : requests) {
response.add("ValueForKey: " + request.getArgument());
System.out.println(request.getArgument()+Thread.currentThread().toString());
}
return response;
}
}
public static void main(String[] args) {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
Future<String> f1 = new CommandCollapserTest(1).queue();
Future<String> f2 = new CommandCollapserTest(2).queue();
Future<String> f3 = new CommandCollapserTest(3).queue();
Future<String> f4 = new CommandCollapserTest(4).queue();
f1.get();
f2.get();
f3.get();
f4.get();
System.out.println(HystrixRequestLog.getCurrentRequest().getAllExecutedCommands().size());
HystrixCommand<?> command = HystrixRequestLog.getCurrentRequest().getAllExecutedCommands()
.toArray(new HystrixCommand<?>[1])[0];
assertEquals("GetValueForKey", command.getCommandKey().name());
assertTrue(command.getExecutionEvents().contains(HystrixEventType.COLLAPSED));
assertTrue(command.getExecutionEvents().contains(HystrixEventType.SUCCESS));
context.shutdown();
}
}
7. Fallback
以下几种情况Hystrix会尝试恢复执行getFallback方法。
- 当construct()或者run()抛出异常时
- 当断路器开启
- 当线程池的队列或信号量处于饱满转台
- 当命令超过其超时
只要实现HystrixCommand 的getFallback方法即可调用逻辑如下:
public class CommandWithFallBack extends HystrixCommand<String> {
public CommandWithFallBack(boolean throwException) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
}
@Override
protected String run() {
throw new RuntimeException("failure from CommandThatFailsFast");
}
@Override
protected String getFallback() {
return "I'm fallback";
}
}
多级fallback
当我们执行业务的时候,有时候会有备用方案1、备用方案2,当备用方案1失败的时候启用备用方案2,所以可以使用多级fallback。
其实就是HystrixCommand1执行fallback1, fallback1的执行嵌入HystrixCommand2,当HystrixCommand2执行失败的时候,触发HystrixCommand2的fallback2,以此循环下去实现多级fallback。关系图如下:
public class CommandWithMultiFallBack1 extends HystrixCommand<String> {
public CommandWithMultiFallBack1(boolean throwException) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
}
@Override
protected String run() {
throw new RuntimeException("failure from CommandThatFailsFast");
}
@Override
protected String getFallback() {
//执行上面的command
return new CommandWithFallBack.execute();
}
}
Hystrix的流程解析
-
每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中.
-
执行execute()/queue做同步或异步调用.
-
Request Cache
-
判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤5
-
判断线程池/队列/信号量是否跑满,上报调用指标信息至7,如果跑满进入降级步骤8,否则继续后续步骤.
-
调用HystrixCommand的run方法.运行依赖逻辑,上报调用指标信息至7,运行依赖逻辑可能会有以下情况:
- 依赖逻辑调用超时,进入步骤8.
- 调用出错,进入步骤8.
- 没有出现任何故障,返回成功调用结果,进入步骤9
-
计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,同时,Hystrix会根据记录的数据来计算失败比率,一旦失败比率达到某一阈值将自动开启Circuit Breaker。
-
getFallback()降级逻辑,以下四种情况将触发getFallback调用:
- 熔断器开启拦截调用(4)
- 线程池/队列/信号量是否饱满(5)
- run()方法抛出非HystrixBadRequestException异常(6)
- run()方法调用超时(6)
调用降级结果有以下几种情况:
- 没有实现getFallback的Command将直接抛出异常
- fallback降级逻辑调用成功直接返回
- 降级逻辑调用失败抛出异常
-
返回执行成功结果
两种隔离(Isolation)
Hystrix隔离方式采用线程/信号的方式,通过隔离限制依赖的并发量和阻塞扩散.
-
线程隔离
把执行依赖代码的线程与请求线程,请求线程可以自由控制离开的时间(异步过程)。通过线程池大小可以控制并发量,当线程池饱和时可以提前拒绝服务,防止依赖问题扩散。
优:减少所依赖服务发生故障时的影响面,比如ServiceA服务发生异常,导致请求大量超时,对应的线程池被打满,这时并不影响ServiceB、ServiceC的调用。
如果接口性能有变动,可以方便的动态调整线程池的参数或者是超时时间,前提是Hystrix参数实现了动态调整。缺:请求在线程池中执行,肯定会带来任务调度、排队和上下文切换带来的开销。因为涉及到跨线程,那么就存在ThreadLocal数据的传递问题,比如在主线程初始化的ThreadLocal变量,在线程池线程中无法获取
-
信号隔离
信号隔离也可以用于限制并发访问,防止阻塞扩散, 与线程隔离最大不同在于执行依赖代码的线程依然是请求线程。实现也很简单,一个简单的计数器,当请求进入熔断器时,执行tryAcquire(),计数器加1,结果大于阈值的话,就返回false,发生信号量拒绝事件,执行降级逻辑。当请求离开熔断器时,执行release(),计数器减1。
本文初步的介绍了Hystrix的各个组件,以及基本原理,后面会出一些列原理的文章,尽情期待!
参考:https://github.com/Netflix/Hystrix/wiki/How-it-Works#RequestCollapsing