终于到了必须要了解Hystrix的时候了,Hystrix直译过来是刺猬的意思,这里指的是Netflix开源的一个包含限流、熔断等功能的库类,它能给系统提供快速失败和快速恢复的能力,让其更具“弹性”。
流控、熔断和快速恢复是现在大型分布式系统中各个服务节点应该具备的基本抗灾和容错能力,如何在流量突增、依赖服务宕机等外界紧急情况发生时不需要人工干预来自动做到快速止损(比如服务降级)、防止整个分布式系统雪崩?紧急情况消失后又能在短时间内做到整个系统服务的快速恢复?这将是本系列文章的主题之一,也许Hystrix能帮你做到这一点。
Hystrix由Netfilix API团队研发于2011年,2012年开始在Hystrix公司内部推广和使用,Hystrix在Netflix中久经沙场,现在已经是一个非常成熟的系统,而最近流行的微服务架构和Spring Cloud,让Hystrix成为了配套的基础设施,在国内也逐渐流行起来。然而仍有部分用户抱怨Hystrix不太好用,那又是为什么呢?我们来逐渐解开Hystrix的面纱。
如果做一个简单的限流功能,那是很容易的,《常用限流方案的设计和实现》一文已经比较清楚的介绍了其实现,但如果想做更精准的控制、处理后的细分和快速恢复,还有大量的工作需要做。很多RPC框架也自带流控和熔断功能,比如Dubbo,但功能不够强大,大多需要人工手动操作,离自动还有段距离,这也是为啥需要将其作为一套单独的解决方案的原因。
这里先对一些关键术语进行简单的解释:
限流:即限制流量的最大值,是流控的一种方式,
熔断/断路:熔断其实是一种熔断器(又叫断路器)模式中的一种状态,这里先说熔断器(Circuit Breaker),断路器本质上是一个物理元件,但在这里指的是软件系统对紧急情况进行快速止损的一种设计方案(详见http://martinfowler.com/bliki/CircuitBreaker.html),熔断器设计中有三种状态,closed(关闭状态,流量可以正常进入)、open(即熔断状态,一旦错误达到阈值,熔断器将打开,拒绝所有流量)和half-open(半开状态,open状态持续一段时间后将自动进入该状态,重新接收流量,一旦请求失败,重新进入open状态,但如果成功数量达到阈值,将进入closed状态),即下图:
降级:即我们常说的服务降级,其实来自于服务等级(或服务分级),根据服务的质量、功能或其他指标,人为的将服务分成多个等级,便于我们分析和定位服务级别,而服务降级指的是当达到某个条件或特殊场景时,需要下调服务等级。
为了不陷入术语怪圈(比如并发和并行的区别),我们这边将系统遇到“危险”时采取的整套应急方案和措施统一称为降级或服务降级。想要帮助服务做到自动降级,需要先做到如下几个步骤:
- 可配置的降级策略:降级策略=达到降级的条件+降级后的处理方案,策略一定得可配置,因为不同的服务对服务的质量定义不一样,降级的方案也将不一样。
- 可识别的降级边界:一定要精确的知道需要对谁进行降级,可以是一个对外服务、对下游的一个依赖或者是内部一段处理逻辑。降级边界主要用来植入降级逻辑。
- 数据采集:是否达到降级条件依赖于采集的数据,这些数据可以是当前某段时间的数据,也可以是很长一段时间的历史数据。
- 行为干预:进入降级状态后将会对正常的业务流程产生干预,可能是限流、熔断,也可能是同步流程变为异步流程等(比如发送MQ的变成oneway的形式)等。
- 结果干预:是返回null,还是默认值,还是流程上的同步改异步等。
- 快速恢复:即如何从降级状态变回正常状态,这也需要达到某些条件。
我们来逐步看下Hystrix是如何做到以上几点的,
### 可配置的降级策略 ###
Hystrix提供了三种降级策略:并发、耗时和错误率,而Hystrix的设计本身就能很好的支持动态的调整这些策略(简单的说就是调整并发、耗时和错误率的阈值),当然,如何去动态调整需要用户自己来实现,Hystrix只提供了入口,就是说,Hystrix并没有提供一个服务端界面来动态调整这些策略,这多少有点让人遗憾。如果要了解Hystrix具体的策略配置,可以看看HystrixCommandProperties和HystrixThreadPoolProperties两个类。
### 可识别的降级边界 ###
降级工具面临的第一个难题就是如何在业务代码中植入降级逻辑,业务研发人员得提前明确和定义哪些地方是风险点,然后将这些地方的逻辑抽取出来,Hystrix包装需降级的业务逻辑采用的是Command设计模式,我们知道,命令模式主要是将请求封装到对象内部,让我们使用对象一样来使用请求。这样对Hystrix大有好处,因为你需要降级的业务逻辑和数据已经封装成一个Command对象交给Hystrix了,Hystrix直接来接管业务逻辑的执行权,该何时调用,或者甚至不调用都可以,我们来看看Hystrix定义的命令接口(实际是抽象类,这里已简化):
public abstract class HystrixCommand{ protected abstract R run() throws Exception; protected R getFallback() { throw new UnsupportedOperationException("No fallback available."); } public R execute() { try { return queue().get(); } catch (Exception e) { throw Exceptions.sneakyThrow(decomposeException(e)); } } public Future queue() { ... 太长了,略 ... } }
只需要简单继承HystrixCommand,就相当于接入了Hystrix,泛型R代表返回值类型,在run()方法中直接实现正常的业务逻辑,并返回R类型的结果,如果降级后需要返回特殊的值,你只需要覆盖getFallback()方法即可。举个例子,我们这里有个抽奖活动,只要是我们的注册用户,就有一次抽奖机会,前提是不在我们的黑名单内。
public class ChouJiangService { /** * 尝试抽奖 * * @param userId * @return 中奖结果,false-没中奖,true-已中奖 */ public boolean tryChouJiang(Long userId) { if(!checkUserStatus(userId)) { return false; } // 返回是否中奖 return ThreadLocalRandom.current().nextInt(2) == 1; } /** * 检查用户是否是黑名单用户 * * @param userId * @return true-不在黑名单,false-在黑名单 */ private boolean checkUserStatus(Long userId) { // 请不要在意内部逻辑有多奇怪,这里只是演示 return System.currentTimeMillis() % 2 == 1; } }
对于抽奖服务来说,检查用户黑名单并不是必须的行为,如果checkUserStatus内部发生问题(有可能里面依赖了外部服务),不应该影响正常的抽奖逻辑,因为毕竟在黑名单里的用户是少数,如果我们要对checkUserStatus逻辑使用Hystrix,我们就会先创建一个CheckUserStatusCommand类,来封装检查用户黑名单的逻辑:
public class CheckUserStatusCommand extends HystrixCommand{ private Long userId; public CheckUserStatusCommand(Long userId) { super(HystrixCommandGroupKey.Factory.asKey("ChouJiangCommandGroup")); this.userId = userId; } @Override protected Boolean run() throws Exception { // 请不要在意内部逻辑有多奇怪,这里只是演示 return System.currentTimeMillis() % 2 == 1; } }
这样做了以后,ChouJiangService#tryChouJiang方法就该写成如下样子:
public boolean tryChouJiang(Long userId) { if(!new CheckUserStatusCommand(userId).execute()) { return false; } // 返回是否中奖 return ThreadLocalRandom.current().nextInt(2) == 1; }
可以看出,我们创建了一个CheckUserStatusCommand的实例,然后调用了execute方法来获取结果,这样就基本完成了,Hystrix库类已经给检查用户黑白名单的逻辑附上了自动降级逻辑了,当然里面使用了大量Hystrix默认的降级策略配置(本文不是Hystrix使用的详细教程,所以这里主要突出的是用法而不强调具体的策略配置)。这里同样也说明了为什么动态调整配置是很容易的,因为每个请求都会新建Command对象(注意,Command对象是有状态的,不能重用),你只需要在创建时调整策略参数就行了,当然,这得用户自己来实现。
虽然看起来很简单,但老司机马上会发现问题:
- 系统中每一处需要降级的逻辑都需要将其封装成一个Command类,哪怕需要降级的方法只有一行代码。如果一个系统有一百个需要降级的点,那么我们需要在系统中新增一百个Command类,有时候这让人难以接受。
- 对老的业务系统来说,接入Hystrix将意味着巨大的工作量,因为你要把很多逻辑都封装成Command,你能接受但测试同学未必愿意。
- 每次请求都将创建一个Command对象,因为Command对象包含了降级逻辑的大部分操作,是个重状态的对象,不能复用,如果QPS过高,将产生大量的朝生夕死的对象,对内存分配和GC将产生一定的压力。
很多用户确实也提出过抱怨,为何Hystrix的侵入性那么强?但Hystrix设计者们这么做自然有他们的道理(详见:https://github.com/Netflix/Hystrix/wiki/FAQ%20:%20General 的Why is it so intrusive?部分),他们认为,我们需要给应用的依赖提供一个清晰的屏障,使用Command模式不仅仅是出于功能上的原因,也是作为一种标准机制,通过Command对象来向用户传递它是受保护的资源。可见,Hystrix的设计者们并不建议我们使用基于注解或AOP来作为接入Hystrix的方式,但他们仍然说:If you still feel strongly that you shouldn't have to modify libraries and add command objects then perhaps you can contribute an AOP module.(直译过来就是如果你嫌麻烦不想创建这么多Command对象,有本事你自己去实现AOP啊!开个玩笑(*^__^*) )。
### 数据采集 ###
收集数据是必不可少的一步,每个降级点(需要采取降级保护的点)的数据是独立的,所以我们可以给每个降级点配置单独的策略。这些策略一般是建立在我们对这些降级点的了解之上的,初期甚至可以先观察一下采集的数据来指定降级策略。采集哪些数据?数据如何存储?数据如何上报?这都是Hystrix需要考虑的问题,Hystrix采用的是滑动窗口+分桶的形式来采集数据(具体细节见另一篇),这样既解决了数据在统计周期间切换而带来的跳变问题(通过时间窗口),也控制了切换了力度(通过桶大小)。另一个有意思的地方是,与常规的同步统计数据的方式不同,Hystrix采用的是RxJava来进行事件流的异步统计数据,类似于观察者模式(具体细节见另一篇),这样做的好处是降低统计时阻塞业务逻辑的风险,在某些情况下还能享受多核CPU所带来的性能上的收益。
### 行为干预 ###
一旦发现采集的数据命中了降级策略,那么降级工具就将对请求进行行为干预,行为干预是评价一个降级工具好坏的重要指标,它的设计直接关系到系统的“弹性”到底有多大。但有时候行为干预和上面提到的数据采集这两个动作是同时完成的,比如使用信号量、线程池或者令牌桶算法来进行降级的时候。行为干预的设计是很有技巧的,一般来说有如下两种方案:
- 实时采集(当前某段时间周期的)数据,对每笔请求都进行策略判断(每笔请求都会加入数据并进行分析),一旦命中策略,当即对这笔请求进行行为干预,如果没有命中,则执行正常的业务逻辑。
- 实时采集(当前某段时间周期的)数据,对每笔请求都进行策略判断(每笔请求都会加入数据并进行分析),一旦有一笔请求命中了策略,接下来的一段时间(可配)内的所有请求都会被行为干预,哪怕接下来再也没有请求命中策略,一直到该段时间过去。
方案a似乎是比较合理的,它总是将系统的行为尽可能的控制在我们预期之内(即各项指标都在配置的策略之下),但多数情况下,我们配置策略会比较宽泛,不那么严格,那这时候采用方案a对系统来说还是有一定的风险。这时候就出现了相对更激进的方案b!一但某些请求导致统计数据触犯了降级策略,那么系统会对后续一段时间的所有请求进行降级处理,即我们熟知的降级延长。而Hystrix将两者结合起来了,让行为干预更加灵活。
### 结果干预 ###
被降级后的请求是应该返回null?还是默认值?还是抛异常?这些都要根据业务而定。Hystrix也在HystrixCommand提供了getFallback方法来方便用户返回降级后的结果。
### 快速恢复 ###
快速恢复功能在那些经常由于外部因素而导致进入降级状态的系统来说尤为重要,降级系统或工具的一个重大目标就是自动性,摆脱需要人为控制开关来保证功能熔断的“原始时代”,所以当外部条件已经恢复,系统也应该在最短的时间内恢复到正常服务状态,这就要求降级系统能够在让业务系统进入降级状态的同时,让业务系统有探测外界环境的机会。大多数降级系统都会在一段时间后“放”一笔请求进来,让它去“试一试”,如果结果是成功的,那么将让业务系统恢复到正常状态,Hystrix同样也是采用这种做法。
如果看到这里,其实大家已经对Hystrix的功能有一定的了解,这里再给一张官方的图:
这张图已经充分说明了官方推荐的是通过Command+线程池的模式来进行业务功能的剥离和管理,这些大大小小的线程池,使用不当,将产生隐患,所以千万不要让Hystrix的这种用法变成反模式。
在最后,我们来简单总结下Hystrix的特色:
- Hystrix内部大量使用了响应式编程模型,通过RxJava库,把能异步做的都做成异步了。这似乎能降低代码复杂度(我是指对RxJava了解的人),并且在多核CPU的服务器上能带来意外性能收获。
- Hystrix能做到通过并发、耗时和异常来进行降级,并能在(并发、限流或内部产生的异常导致的)错误率达到一定阈值时进行服务熔断,并且还能做到从降级状态快速恢复。
- Hystrix通过Command模式来包装降级业务,这有时候提高了接入成本。
- Hystrix只提供了策略变更的入口,但具体的策略可视化和动态配置还是得用户来实现,这确实非常尴尬。
- Hystrix默认的仪表盘只提供了简单的实时数据显示,如果要持久化历史数据,也得用户来实现。
Hystrix并不完美,但也许简单也是一种美,后续文章将深入介绍Hystrix的内部设计。
欢迎关注我们的技术公众号