Hystrix入门
假设有一个应用程序,有着如下的调用关系:
假设在某个时刻,数据库因为某些原因不可用了,基础服务就会得到“数据库无法访问”的信息,炳辉将次信息告知服务A。在出现问题时,用户不断地请求服务A模块,而服务A模块则继续请求基础服务模块,基础服务模块仍然不停地连接有问题的数据库直到超时,大量用户的请求(包括重试的请求)持续发送过来,整个应用不堪重负。
更有甚者,因为数据库的长时间响应或者无法响应,可能导致整个机房的网络阻塞,影响到同机房的所有其他服务。
这个时候,如果服务A在调用基础模块的时候将基础模块隔离开来,短时间内不再调用基础模块,并且快速响应用户的请求,就可以保证服务A自身乃至整个集群的稳定性。
Hystrix是Netflix下的一个Java库,Spring Cloud将Hystrix整合到Netflix项目中,Hystrix通过添加延迟阈值以及容错的逻辑,来帮助我们控制分布式系统间组件的交互。Hystrix通过隔离服务间的访问点,停止它们之间的级联故障。提供可回退操作来实现容错。
Hystrix主要实现以下功能:
- 当所依赖的网络服务发生延迟或者失败时,对访问的客户端程序进行保护
- 在分布式系统中,停止级联故障
- 网络服务恢复正常后,可以快速恢复客户端的访问能力
- 调用失败时执行服务回退
- 可支持实时监控、报警和其他操作
第一个Hystrix程序
Hystrix Server端
新建一个Server端项目,提供两个REST服务。一个正常返回,一个“伪装”成故障调用,休眠10S再返回。
pom的依赖如下:
1 | <dependencies> |
提供REST服务的Controller如下:
1 |
|
启动类如下:
1 |
|
Hystrix Client端
Client的依赖如下:
1 | <dependencies> |
新建一个命令类
1 | public class HelloCommand extends HystrixCommand<String> { |
实现它的run方法,发出一个HTTP请求。重写getFallback方法,作为调用失败,回退逻辑。
然后在写两个调用类,分别调用Server提供的normalHello和errorHello接口
1 | public class HelloMain { |
1 | public class HelloErrorMain { |
Hystrix默认的超时时间是1S,所以,调用errorHello接口超时后会走getFallback方法
Hystrix的运作流程
简单整理下Hystrix的运作流程
- 在命令开始执行时,做一些准备操作
- 判断是否打开了缓存,如果打开缓存,直接查找缓存并返回结果
- 判断断路器是否打开,如果打开了表示链路不可用,直接执行回退方法
- 判断线程池、信号量(计数器)等条件,例如像线程池超负荷,则执行回退方法,否则,就去执行命令的内容
- 执行命令,计算是否要对断路器进行处理,执行完成后如满足一定条件,则需要开启断路器。如果执行成功,则返回结果,反之则执行回退。
Hystrix的使用
命令执行
一个命令对象可以使用以下方法来执行命令:
- toObservable:返回一个最原始的可观察的实例(Observable),Observable是R小Java的类,使用该对象可以观察命令的执行过程,并且将执行信息传递给订阅者
- observe:调用toObservable方法,获得一个原始的Observable实例后,使用ReplaySubject作为原始Observable的订阅者
- queue:通过toObservable方法获取原始的Observable实例,在调用Observable的toBlocking方法得到一个BlockingObservable实例,最后调用BlockingObservable的toFuture方法返回Future实例,调用Future的get方法得到执行结果。
- execute:调用queue的get方法返回命令的执行结果,该方法同步执行。
上面4个方法,除了execute外,都为异步执行。observe和toObservable方法的区别在于,toObservable方法被调用后,命令不会立即执行,只有当返回的Observable实例被订阅后,才会真正执行命令。而在observe方法的实现中,会调用toObservable得到Observable实例,再对其进行订阅,因此调用observe方法后会立即执行命令(异步)。
1 | public class RunTest { |
属性配置
使用Hystrix时,可以为命令设置属性。将超时时间由默认的1S改为500ms。但只对当前命令有效。
1 | public HelloCommand(boolean isTimeout) { |
如果向全局生效,可以使用以下代码片段:
1 | ConfigurationManager.getConfigInstance(). |
除了超时配置外,还需要了解下命令的相关名称,可以为命令设置以下名称:
- 命令组名称(GroupKey):必须提供命令组名称,默认情况下,全局维护的线程池Map以该值作为Key,该Map的value为执行命令的线程池
- 命令名称(CommandKey):可选参数
- 线程池名称(ThreadPoolKey):指定了线程的key后,全局维护的线程池Map将以该值作为key
以下的代码片段分别设置上面的三个key
1 | super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("group-key")). |
断路器开启
断路器一旦开启,就会直接调用回退方法,不再执行命令,而且也不会更新链路的健康状况。断路器的开启需要满足两个条件:
- 整个链路达到一定阈值,默认情况下,10秒内产生超过20次请求,则符合第一个条件
- 满足第一个条件的情况下,如果请求的错误百分比大于阈值,则会打开断路器默认为50%
1 | import com.netflix.config.ConfigurationManager; |
断路器关闭
断路器打开后,在一段时间内,命令不会再执行(一直触发回退),这段时间我们称作“休眠期”。休眠期的默认值为5秒,休眠期结束后,Hystrix会尝试性地执行一次命令,此时断路器的状态不是开启,也不是关闭,而是半开的状态,如果这一次命令执行成功,则会关闭断路器并清空链路的健康信息;如果执行失败,断路器会继续保持打开的状态。
1 | public class CloseTest { |
隔离机制
命令的真正执行,除了断路器要关闭以外,还要再过一关:执行命令的线程池或者信号量是否满载。如果满载,命令就不会执行,而是直接触发回退,这样的机制,在控制命令的执行上,实现了错误的隔离。Hystrix提供了两种隔离策略:
- THREAD:默认值,由线程池来决定命令的执行,如线程池满载,则不会执行命令。Hystrix使用了ThreadPoolExecutor来控制线程池的行为,线程池的默认大小为10.
- SEMAPHORE:由信号量来决定命令的执行,当请求的并发数高于阈值时,就不再执行命令。
相对于线程策略,信号量策略开销更小,但是该策略不支持超时以及异步,除非对调用的服务有足够的信任,否则不建议使用该策略进行隔离。
下面举个例子,来看看两个隔离方式有什么区别、
1 | public class MyCommand extends HystrixCommand<String> { |
线程池隔离
1 | public class ThreadIso { |
信号量隔离
1 | public class SemaphoreIso { |
执行ThreadIso中的main方法,可以看到有3次命令会触发回退。
执行SemaphoreIso中的方法,可以看到有4次命令会触发回退。
合并请求
默认情况下,Hystrix会为命令分配线程池来执行命令实例,线程池会消耗一定的性能。对于一些同类型的请求(URL相同,参数不同),Hystrix提供了合并请求的功能,在一次请求的过程中,可以将一个时间段内的相同请求(参数不同),收集到同一个命令中执行,这样就节省了线程的开销,减少了网络连接,从而提升了执行的性能。
实现合并请求的功能,至少包含以下3个条件:
- 需要有一个执行请求的命令,将全部参数进行整理,然后调用外部服务
- 需要有一个合并处理器,用于收集请求,以及处理结果
- 外部接口支持。
1 | public class CollapseTest { |
请求缓存
Hystrix支持缓存功能,如果在一次请求的过程中,多个地方调用同一个接口,可以考虑使用缓存。缓存打开后,下一次的命令不会去执行,直接到缓存中获取响应并返回。
开启缓存较为简单,在命令中重写父类的getCacheKey即可。
1 | public class CacheMain { |