作为 SpringCloud 中的执法部门-六扇门,Hystrix 监管着服务的一举一动,不管是超时还是异常抛出,但凡有违法乱纪的现象发生,就会被强制放到 fallback 里进行改造。可是,每个应用都有一长串的服务,那全部都交给 Hystrix 这能管得过来吗?
Hystrix 可不是所有服务都监督,毕竟六扇门人力有限,他们只盯梢一些关键人物,给每个关键人物指派一个锦衣卫,但凡有异常发生,立即动手。接下来,我们就来看看六扇门的锦衣卫是如何秉公执法的。
服务降级原理
盯梢名单 - @HystrixCommand
现在流行苍蝇老虎一起打,可我们六扇门不在乎小苍蝇,只盯大老虎。对那些一旦发生异常,影响特别恶劣,但是还有补救措施的,我们就安排一个锦衣卫贴身监视。
@HystrixCommand(fallbackMethod = "putInPrison")
public String bigTiger() {
throw RuntimeException("Eat People");
}
上面就是我们的贴身锦衣卫@HystrixCommand
注解,直接管在大老虎(具体 Method 方法)的头上,没有指派锦衣卫的方法就可以放飞自我了,Hystrix
不会管你的。@HystrixCommand
注解中指定了一个fallbackMethod
,这里就是大老虎进行改造的地方,也就是降级逻辑所在的方法名。注意,降级方法的方法签名(参数列表)要和原方法保持一致,也就是说,如果原方法声明了一个参数 String,那么降级方法也要声明同样的参数,Hystrix
会原封不动的把当前参数传递给降级方法。
当 Hystrix
和 Feign
共同使用的时候,还有一种配置方式,那就是在 FeignClient
注解中指定一个 class,在这个 class 里可以处理 Feign 接口中声明的所有方法的降级需求。
@FeignClient(name = "feign-service-provider", fallback = Fallback.class)
public interface MyHelloService extends HelloService {
}
锦衣卫工作流程 - 异常捕捉
如果小伙伴直接去阅读 Hystrix
服务降级的源码,相信有很有效的直接劝退的作用。Hystrix
的源码大量基于 RxJava
,在实现上比较接近函数式语言的风格,运用了大量的异步回调函数和事件驱动,层层嵌套十分崩溃,这对有 JavaScript 或其他函数语言经验的同学会容易理解一些。
为了避免直接劝退,这里的流程图以业务流为主,不涉及具体类名和方法名来过一下:
@HystrixCommand
: 安插在方法上的锦衣卫,标识此方法由Hystrix
监管AspectJ
:运用 Spring 的切面能力,给带有@HystrixCommand
注解的方法配置了切面点,在方法调用的时候,将首先执行切面逻辑。- ·Request Cache·:如果处于开启状态,则尝试用
CacheKey
从本地缓存中获取数据,也就不用发起方法调用了。如果处于关闭状态,就继续往下走到最烧脑的部分 - 注册
Observer
:Observer
是观察者模式(在 RxJava 中又叫
Observable),但这里只是一个幌子,这个 Observer 背后运用 RxJava 注册了一堆异步回调函数,当方法正常执行、异常抛出、结束或其他状态的时候,将会触发对应的回调函数进行处理,而且回调函数里面还会嵌套回调函数。 - 发起调用:在发起调用之前,将会检查熔断状态,如果断路器当前处于开启的状态,那么将直接走向 fallback 流程。如果断路器处于关闭,则发起真正的调用
- 异常,又见异常:前面你来我往这么久,就是等方法调用抛异常。异常触发了步骤 4 中注册的回调函数,然后直接转给了降级方法
服务降级常用方案
话说进了 fallback 的多多少少都是犯了点错的人,犯了错就要老实交代,我们六扇门的锦衣卫也不是吃素的,对付进了 fallback 的人,那手段可多了去了,就看你招还是不招?
沉默是金 - 静默处理
所谓的静默处理,就是什么也不干,在 fallback 逻辑中直接返回一个空值 Null。小伙伴们可能会问,那我用 try-catch 捕捉异常不也是能达到一样的效果吗?其实不然,首先try-catch 只能处理异常抛出的情况,并不能做超时判定。其次,使用 try-catch 就要在代码里包含异常处理块,我们在程序设计时讲究单一职责和开闭原则。正所谓凯撒的归凯撒,上帝的归上帝,既然有了专门的 fallback 处理类,那这个工作还是交给fallback 来吧,这样你的业务代码也落个清爽。
瞒天过海:默认值
瞒天过海实际上就是说个谎话,在并不确定真实结果的情况下返回一个默认值。
打个比方,假如在商品详情页调用营销优惠接口时发生了故障,无法返回正确的计算结果,这里我们就可以在 fallback 逻辑中返回商品原价,作为打折后的价格,这样就相当于返回了一个没有打折优惠的计算结果。
这种方式下接口的返回值并不是真实的,因此不能应用在某些核心主链路中。举个例子,比如下单页面就是核心主链路,是最终确定订单价格的关键步骤。假如我们对订单优惠计算采用了这种瞒天过海的默认值,那么就会实际造成用户损失。因此,这里面的优惠计算决不能返回默认值,一定要得出真实结果,如果无法获取那么宁可返回异常中断下单操作。
小伙伴们可能会问,那为什么商品详情页可以用默认值降级,而下单页面不能呢?这就要讲到主链路的规划,简单来说,电商平台的用户购物行为是一个漏斗模型,上宽下窄,用户流量在漏斗口最多,在尾部最少,越接近尾部的流量被转化为购物行为的比例就越高,因此越到后面对降级的容忍度就越低。商品搜索和商品详情页处于漏斗的上部,主要是导流端,在没有发生金钱往来的情况下我们可以容忍一定程度的降级误差。但对于下单页,这是整个漏斗模型的尾部,直接发生交易的环节,绝不能容忍任何金钱上的误差。
好好改造:想办法恢复服务
这个才能称得上是正儿八经的积极措施,fallback 会尝试用各种方法获取正确的返回值,有这么几个常用场景。
- 缓存异常:假如因为缓存故障无法获取数据,在 fallback 逻辑中可以转而访问底层数据库(这个方法不能用在热点数据上,否则可能把数据库打挂,或者引发更大范围的服务降级和熔断,要谨慎使用)。反过来如果数据库发生故障,也可以在 fallback 里访问缓存,但要注意数据一致性
- 切换备库:一般大型应用都会采用主从+备库的方式做灾备,假如我们的主从库都发生了故障,往往需要人工将数据源切换到备份数据库
- 重试:Ribbon 可以处理超时重试,但对于异常情况来说(比如当前资源被暂时锁定),我们可以在 fallback 中自己尝试重新发起接口调用
- 人工干预:有些极其重要的接口,对异常不能容忍,这里可以借助 fallback启动人工干预流程,比如做日志打点,通过监控组件触发报警,通知人工介入真实项目里服务降级的补救措施是八仙过海各显神通,但是目标都是相同的,就是对系统的影响降到最低。
一错再错 —— 多次降级
总有那么几个顽固分子,在 fallback 里不好好改造,又捣鼓出一个异常来。这时候我们可以做二次降级,也就是在 fallback 中再引入一个 fallback。当然,你也可以引入三四五六七八更多层的降级,对应一些复杂的大型应用,比如淘系很多核心系统,多级降级是很常见的,根据系统故障的严重程度采取更精细粒度的降级方案。
那假如这一连串降级全部都失败了,难道要牢底坐穿不成?对这种一错再错无药可救的顽固分子,锦衣卫也是没有办法啊,那只好放你走了,将异常抛到最外层。
番外篇-Request Cache
Request Cache 并不是让你在 fallback 里访问缓存,它是 Hystrix 的一个特殊功能。我们可以通过@CacheResult
和@CacheKey
两个注解实现,配置如下
@CacheResult
@HystrixCommand
public Friend requestCache(@CacheKey Integer id) {
}
@CacheResult
注解的意思是该方法的结果可以被Hystrix
缓存起来,@CacheKey
指定了这个缓存结果的业务 ID 是什么。在一个 Hystrix 上下文范围内,如果使用相同的参数对@CacheResult
修饰的方法发起了多次调用,Hystrix
只会在首次调用时向服务节点发送请求,后面的几次调用实际上是从 Hystrix 的本地缓存里读取数据。
Request Cache
并不是由调用异常或超时导致的,而是一种主动的可预知的降级手段,严格的说,这更像是一种性能优化而非降级措施。
本文已收录至我的个人网站:程序员波特,主要记录Java相关技术系列教程,共享电子书、Java学习路线、视频教程、简历模板和面试题等学习资源,让想要学习的你,不再迷茫。