一、Hystrix介绍
在微服务架构中,我们可能将系统拆分中多个服务单元,由于每个服务单元都运行在不同的进程中,他们彼此之间通过远程调用的方式执行,这样就有可能因为网络原因或者是依赖服务自身问题出现调用故障或延迟,而这些问题 会直接 导致调用方对外提供服务也出现延迟,若此时调用方的请求不断累加,最后会造成任务积压导致自身系统瘫痪。
针对上述问题,Spring Cloud Hystrix实现了断路器、线程隔离等一系列服务保护功能。Hystrix主要作用如下:
1)服务隔离和服务熔断
服务隔离:每次 接收到请求,都找一个单独的空间执行,这样你出了问题,就不会影响其他的业务。
服务熔断:发现某个服务不可用时,执行兜底方法,触发降级,返回我们预置的内容或异常。
2)服务降级、限流和快速失败。其实,服务降级和快速失败是差不多的意思。
3)请求合并和请求缓存
4)本身整合了对单体和集群的监控
二、Hystrix架构和处理流程解析
1、架构图
Hystrix整个工作流程概括如下:
1、创建HystrixCommand或HystrixObservableCommand对象,用于封装请求,并且在构造方法 中配置请求执行需要的参数;
2、执行命令,H ystrix提供了4种执行命令的方法,后面我们在介绍。
3、判断结果是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存, 但是需要用户 自定义启动。
4、判断断路器是否打开,如果打开了, 直接跳到第8步
5、判断线程池/队列/信号量是否占满,如果已经占满,直接跳转到8步
6、执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳转到第8步;否则走9步
7、计算 断路器的健康度
8、走Fallback处理逻辑
9、返回请求响应
三、Hyxstrix的 两种命令模式
Command会以隔离的形式完成run方法的调用,用在依赖的服务返回单个操作结果的时候。
ObservableCommand会使用当前 线程进行调用,用在依赖的服务返回多个操作结果的 时候。
命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。他可以被用于试下 “行为请求者” 与 “行为实现着”的解耦,以便使两者可以适应变化。
四、 断路器详细执行流程
下图展示了HystrixCommand或HystrixObservableCommand如何与HystrixCircuitBreaker(断路器)以及与它的逻辑和决策流程进行交互,包括在计数器在断路器上的行为方式。
下面我们对上图中涉及到的 几个方法的执行逻辑进行介绍:
isOpen():判断 断路器的打开/关闭状态。详细逻辑如下所示:
如果断路器打开标识未true,则直接返回true,标识断路器处于打开状态。否则,就从度量指标对象 metrics中获取HealthCounts统计对象做进一步判断(该对象记录了一个滚动时间窗内的请求信息快照,默认时间窗为10s)
1、如果请求总数(QPS)在预设的阈值范围内就返回false,表示断路器处于未打开状态。该阈值的配置参数circuitBreakerRequestVolumeThreshold,默认值为20;
2、 如果错误百分比在 阈值范围内就返回false,表示断路器处于未打开状态。该阈值的配置参数为circuitBreakerErrorThresholdPercentage,默认值为50;
3、如果上面两个条件都不满足,则将断路器设置打开状态。同时,如果是从关闭状态切换到打开状态的话,就将当前时间记录到上面提到的circuitOpenedOrLastTestedTime对象中。
allowRequest():判断请求是否被允许,具体执行逻辑如下:
先根据配置对象properties中的断路器判断强制打开或关闭属性是否被设置。如果强制打开,就直接返回false,拒绝请求。 如果强制关闭,它会允许所有请求,但是, 同时也会调用isOpen()来执行 断路器的计算逻辑,如果模拟断路器 打开/关闭的行为。在默认情况下,断路器并不会进入这两个强制打开或者关闭的分支中去,而是通过 !Open() || allowSingleTest()来判断是否允许请求访问。
通过我们查看allowSingleTest()的具体实现,我们可以看到,这里使用了在isOpen()函数中当断路器从闭合到打开时候记录的时间戳。当断路器在打开状态的时候,这里会判断断开时的时间戳 + 配置中的circuitBreakerSleepWindowInMilliseconds时间是否小于当前时间,是的话, 就将当前时间更新到记录断路器打开的时间对象circuitOpenedOrLastTestedTime中,并且 允许此次请求。简单的来说,通过circuitBreakerSleepWindowInMilliseconds属性设置了一个断路器 打开之后的休眠时间(默认5s),在该休眠时间到达之后,将再次允许请求尝试访问,此时断路器处于 “半开状态”,若此时请求继续失败,断路器又进入打开状态,并继续等待下一个休眠窗口过去之后再次尝试,若请求成功,则将断路器重新置于关闭状态,所以通过allowSingleTest()与isOpen()方法的配合,实现了断路器打开和关闭状态的切换。
markSuccess():该函数用来在 “半开路” 状态时使用。若Hystrix命令调用成功,通过调用它将打开的断路器关闭,并重置度量指标对象。
五、依赖隔离
“舱壁模式”对于属性Docker的人一定不陌生,Docker通过该模式 实现进程的隔离,使得容器之间不互互相影响。而Hystrix则使用该模式实现线程池的隔离,他会为每一个依赖服务从创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也是对该依赖的服务调用产生影响,而不会拖慢其他的服务。
1、线程池和信号量
Hystrix 提供了两种隔离策略,一种是线程和线程池(Thread & Thread Pools),另一种是信号量(Semaphores)。Hystrix推荐 使用的隔离策略是Semaphores。
线程池之间是相互独立的,每个线程池默认包含10个线程。对于每一个依赖项,都使用一个线程池来处理,如果这个依赖项拒绝服务或者超时(快速失败、失效不返回结果、回退),那由它导致的线程满载, 不会影响到其他线程池。
简而言之,线程池提供的隔离允许在不导致停机的情况下优雅的处理客户 机库和子系统性能特征的持续变化和动态组合。
2.隔离策略中的三种“key”
CommandKey,针对相同的接口一般CommandKey值相同,目的是把HystrixCommand,HystrixCircuitBreaker,HytrixCommandMerics以及其他相关对象关联在一起,形成一个原子组。
CommandGroupKey,对CommandKey分组,用于真正的隔离。相同CommandGroupKey会使用同一个线程池或者信号量。一般情况相同业务功能会使用相同的CommandGroupKey。
ThreadPoolKey,如果说CommandGroupKey只是逻辑隔离,那么ThreadPoolKey就是物理隔离,当没有设置ThreadPoolKey的时候,线程池或者信号量的划分按照CommandGroupKey,当设置了ThreadPoolKey,那么线程池和信号量的划分就按照ThreadPoolKey来处理,相同ThreadPoolKey采用同一个线程池或者信号量。
简而言之,ThreadPoolKey是用作于物理隔离(距离很远的机房)的,CommandGroupKey是用于做逻辑隔离的。Hystrix在底层用一个Map集合的方式,来管理这些线程池的集合,Map的key对应ThreadPoolKey(未设置ThreadPoolKey的时候,CommandGroupKey就是ThreadPoolKey)或者CommandGroupKey,value对应具体线程池,类似“Map<String,pool>”的形式,线程(pool)里面对应的就是一个一个的线程(Thread)。
六、请求缓存
对于单次请求里面执行的多次相同命令,可以使用缓存来获取结果。譬如在一次商品购买逻辑中,需要反复查询同一个商品3次,每次查询的结果都是一致的,对于这种读取数据的形式,就可以使用考虑使用缓存。缓存可以减少重复请求,降低数据库压力。
在org.init.springCloud包下新建cache包,创建CacheCommandTest测试类,内部类CacheCommand的构造器中传入一个自定义的缓存key值,我们在同一次请求上下文中,执行三次同样的命令,来查看是否使用了缓存:
package org.init.springCloud.cache; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext; public class CacheCommandTest { public static void main(String[] args) { HystrixRequestContext context = HystrixRequestContext.initializeContext();//开启一个上下文 String caCheKey = "myCaCheKey"; CacheCommand cc1 = new CacheCommand(caCheKey); CacheCommand cc2 = new CacheCommand(caCheKey); CacheCommand cc3 = new CacheCommand(caCheKey); cc1.execute(); cc2.execute(); cc3.execute(); System.out.println("是否是从缓存中读取的数据:"+cc1.isResponseFromCache()); System.out.println("是否是从缓存中读取的数据:"+cc2.isResponseFromCache()); System.out.println("是否是从缓存中读取的数据:"+cc3.isResponseFromCache()); context.shutdown();//关闭上下文 } static class CacheCommand extends HystrixCommand<String>{ String cacheKey; public CacheCommand(String cacheKey){ super(HystrixCommandGroupKey.Factory.asKey("myGroup")); this.cacheKey = cacheKey; } @Override protected String run() throws Exception { System.out.println("执行成功"); return "success"; } @Override protected String getFallback() { System.out.println("执行失败"); return "fail"; } @Override protected String getCacheKey() { return this.cacheKey; } } }
七、请求合并
微服务架构中依赖通常通过远程调用的方式实现,而远程 调用中最常见的 问题就是通信消耗和连接数 占用。在高并发情况 之下,因为通信次数的增加,总的通信时间消耗会变得 不那么理想。同时,因为依赖服务的线程资源有限,将出现排队等待和响应延迟的情况。为了优化这两个问题,Hystrix提供了HystrixCollapser来实现请求合并,以减少通信消耗和线程数的占用。
HystrixCollapser实现了在HystrixCommand之前放置一个 合并处理器,将处于一个很短时间窗(默认是10s)内对同一个依赖服务的多个请求进行整合并以批量方式发起请求功能(服务提供方需要提供响应的批量查询接口)。通过HystrixCollapser封装,开发者不需要关注线程合并的细节过程,只需要关注批量化服务和处理。
八、Hystrix Dashboard仪表盘
Hystrix Dashboard主要用来实时监控Hystrix的各项指标信息。通过该仪表盘反馈的实时信息,恶意帮助我们快速发现系统中存在的问题, 从及时的采取应对措施。
项目地址:https://github.com/zhenghaoxiao/spring-cloud-in-action/tree/dev