下面的流程图展现了当使用Hystrix的依赖请求,Hystrix是如何工做的。java
下面将更详细的解析每个步骤都发生哪些动做:后端
构建一个HystrixCommand
或者HystrixObservableCommand
对象。缓存
第一步就是构建一个HystrixCommand
或者HystrixObservableCommand
对象,该对象将表明你的一个依赖请求,向构造函数中传入请求依赖所须要的参数。tomcat
若是构建HystrixCommand
中的依赖返回单个响应,例如:服务器
HystrixCommand command = new HystrixCommand(arg1, arg2);
若是依赖须要返回一个Observable
来发射响应,就须要经过构建HystrixObservableCommand
对象来完 成,例如:网络
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
执行命令并发
有4种方式能够执行一个Hystrix命令。异步
execute()
—该方法是阻塞的,从依赖请求中接收到单个响应(或者出错时抛出异常)。queue()
—从依赖请求中返回一个包含单个响应的Future对象。observe()
—订阅一个从依赖请求中返回的表明响应的Observable对象。toObservable()
—返回一个Observable对象,只有当你订阅它时,它才会执行Hystrix命令并发射响应。K value = command.execute(); Future<K> fValue = command.queue(); Observable<K> ohValue = command.observe(); //hot observable Observable<K> ocValue = command.toObservable(); //cold observable
同步调用方法execute()
实际上就是调用queue().get()
方法,queue()
方法的调用的是toObservable().toBlocking().toFuture()
.也就是说,最终每个HystrixCommand都是经过Observable来实现的,即便这些命令仅仅是返回一个简单的单个值。函数
若是这个命令的请求缓存已经开启,而且本次请求的响应已经存在于缓存中,那么就会当即返回一个包含缓存响应的Observable
(下面将Request Cache部分将对请求的cache作讲解)。性能
当命令执行执行时,Hystrix会检查回路器是否被打开。
若是回路器被打开(或者tripped),那么Hystrix就不会再执行命名,而是直接路由到第8
步,获取fallback方法,并执行fallback逻辑。
若是回路器关闭,那么将进入第5
步,检查是否有足够的容量来执行任务。(其中容量包括线程池的容量,队列的容量等等)。
若是与该命令相关的线程池或者队列已经满了,那么Hystrix就不会再执行命令,而是当即跳到第8
步,执行fallback逻辑。
HystrixObservableCommand.construct() 或者 HystrixCommand.run()
在这里,Hystrix经过你写的方法逻辑来调用对依赖的请求,经过下列之一的调用:
HystrixCommand.run()
—返回单个响应或者抛出异常。HystrixObservableCommand.construct()
—返回一个发射响应的Observable或者发送一个onError()
的通知。若是执行run()
方法或者construct()
方法的执行时间大于命令所设置的超时时间值,那么该线程将会抛出一个TimeoutException
异常(或者若是该命令没有运行在它本身的线程中,[or a separate timer thread will, if the command itself is not running in its own thread])。在这种状况下,Hystrix将会路由到第8
步,执行fallback逻辑,而且若是run()
或者construct()
方法没有被取消或者中断,会丢弃这两个方法最终返回的结果。
请注意,没有任何方式能够强制终止一个潜在[latent]的线程的运行,Hystrix可以作的最好的方式是让JVM抛出一个InterruptedException
异常,若是你的任务被Hystrix所包装,并不意味着会抛出一个InterruptedExceptions
异常,该线程在Hystrix的线程池内会进行执行,虽然在客户端已经接收到了TimeoutException
异常,这个行为可以渗透到Hystrix的线程池中,[though the load is 'correctly shed'],绝大多数的Http Client不会将这一行为视为InterruptedExceptions
,因此,请确保正确配置链接或者读取/写入的超时时间。
若是命令最终返回了响应而且没有抛出任何异常,Hystrix在返回响应后会执行一些log和指标的上报,若是是调用run()
方法,Hystrix会返回一个Observable,该Observable会发射单个响应而且会调用onCompleted
方法来通知响应的回调,若是是调用construct()
方法,Hystrix会经过construct()
方法返回相同的Observable对象。
Hystrix会报告成功、失败、拒绝和超时的指标给回路器,回路器包含了一系列的滑动窗口数据,并经过该数据进行统计。
它使用这些统计数据来决定回路器是否应该熔断,若是须要熔断,将在必定的时间内不在请求依赖[短路请求](译者:这必定的时候能够经过配置指定),当再一次检查请求的健康的话会从新关闭回路器。
获取FallBack
当命令执行失败时,Hystrix会尝试执行自定义的Fallback逻辑:
construct()
或者run()
方法执行过程当中抛出异常。写一个fallback方法,提供一个不须要网络依赖的通用响应,从内存缓存或者其余的静态逻辑获取数据。若是再fallback内必须须要网络的调用,更好的作法是使用另外一个HystrixCommand
或者HystrixObservableCommand
。
若是你的命令是继承自HystrixCommand
,那么能够经过实现HystrixCommand.getFallback()
方法返回一个单个的fallback值。
若是你的命令是继承自HystrixObservableCommand
,那么能够经过实现HystrixObservableCommand.resumeWithFallback()
方法返回一个Observable,而且该Observable可以发射出一个fallback值。
Hystrix会把fallback方法返回的响应返回给调用者。
若是你没有为你的命令实现fallback方法,那么当命令抛出异常时,Hystrix仍然会返回一个Observable,可是该Observable并不会发射任何的数据,而且会当即终止并调用onError()
通知。经过这个onError
通知,能够将形成该命令抛出异常的缘由返回给调用者。
失败或不存在回退的结果将根据您如何调用Hystrix命令而有所不一样:
execute()
:抛出一个异常。queue()
:成功返回一个Future,可是若是调用get()方法,将会抛出一个异常。observe()
:返回一个Observable,当你订阅它时,它将当即终止,并调用onError()方法。toObservable()
:返回一个Observable,当你订阅它时,它将当即终止,并调用onError()方法。若是Hystrix命令执行成功,它将以Observable形式返回响应给调用者。根据你在第2
步的调用方式不一样,在返回Observablez以前可能会作一些转换。
execute()
:经过调用queue()
来获得一个Future对象,而后调用get()
方法来获取Future中包含的值。queue()
:将Observable转换成BlockingObservable
,在将BlockingObservable
转换成一个Future。observe()
:订阅返回的Observable,而且当即开始执行命令的逻辑,toObservable()
:返回一个没有改变的Observable,你必须订阅它,它才可以开始执行命令的逻辑。下面的图展现了HystrixCommand
和HystrixObservableCommand
如何与HystrixCircuitBroker
进行交互。
回路器打开和关闭有以下几种状况:
HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
)HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
CLOSE
变换成OPEN
HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()
,下一个的请求会被经过(处于半打开状态),若是该请求执行失败,回路器会在睡眠窗口期间返回OPEN
,若是请求成功,回路器会被置为关闭状态,从新开启1
步骤的逻辑。Hystrix采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。
客户端(第三方包、网络调用等)会在单独的线程执行,会与调用的该任务的线程进行隔离,以此来防止调用者调用依赖所消耗的时间过长而阻塞调用者的线程。
[Hystrix uses separate, per-dependency thread pools as a way of constraining any given dependency so latency on the underlying executions will saturate the available threads only in that pool]
您能够在不使用线程池的状况下防止出现故障,可是这要求客户端必须可以作到快速失败(网络链接/读取超时和重试配置),并始终保持良好的执行状态。
Netflix,设计Hystrix,而且选择使用线程和线程池来实现隔离机制,有如下几个缘由:
使用线程池的好处
经过线程在本身的线程池中隔离的好处是:
简而言之,由线程池提供的隔离功能可使客户端库和子系统性能特性的不断变化和动态组合获得优雅的处理,而不会形成中断。
注意:虽然单独的线程提供了隔离,但您的底层客户端代码也应该有超时和/或响应线程中断,而不能让Hystrix的线程池处于无休止的等待状态。
线程池最主要的缺点就是增长了CPU的计算开销,每一个命令都会在单独的线程池上执行,这样的执行方式会涉及到命令的排队、调度和上下文切换。
Netflix在设计这个系统时,决定接受这个开销的代价,来换取它所提供的好处,而且认为这个开销是足够小的,不会有重大的成本或者是性能影响。
Hystrix在子线程执行construct()
方法和run()
方法时会计算延迟,以及计算父线程从端到端的执行总时间。因此,你能够看到Hystrix开销成本包括(线程、度量,日志,断路器等)。
Netflix API天天使用线程隔离的方式处理10亿多的Hystrix Command任务,每一个API实例都有40多个线程池,每一个线程池都有5-20个线程(大多数设置为10)
下图显示了一个HystrixCommand在单个API实例上每秒执行60个请求(每一个服务器每秒执行大约350个线程执行总数):
在中间位置(或者下线位置)不须要单独的线程池。
在第90线上,单独线程的成本为3ms。
在第99线上,单独的线程花费9ms。可是请注意,线程成本的开销增长远小于单独线程(网络请求)从2跳到28而执行时间从0跳到9的增长。
对于大多数Netflix用例来讲,这样的请求在90%以上的开销被认为是能够接受的,这是为了实现韧性的好处。
对于很是低延迟请求(例如那些主要触发内存缓存的请求),开销可能过高,在这种状况下,可使用另外一种方法,如信号量,虽然它们不容许超时,提供绝大部分的有点,而不会产生开销。然而,通常来讲,开销是比较小的,以致于Netflix一般更偏向于经过单独的线程来做为隔离实现。
您可使用请求合并器(HystrixCollapser是抽象父代)来提早发送HystrixCommand,经过该合并器您能够将多个请求合并为一个后端依赖项调用。
下面的图展现了两种状况下的线程数和网络链接数,第一张图是不使用请求合并,第二张图是使用请求合并(假定全部链接在短期窗口内是“并发的”,在这种状况下是10ms)。
为何使用请求合并
事情请求合并来减小执行并发HystrixCommand请求所须要的线程数和网络链接数。请求合并以自动方式执行的,不须要代码层面上进行批处理请求的编码。
理想的合并方式是在全局应用程序级别来完成的,以便来自任何用户的任何Tomcat线程的请求均可以一块儿合并。
例如,若是将HystrixCommand配置为支持任何用户请求获取影片评级的依赖项的批处理,那么当同一个JVM中的任何用户线程发出这样的请求时,Hystrix会将该请求与其余请求一块儿合并添加到同一个JVM中的网络调用。
请注意,合并器会将一个HystrixRequestContext对象传递给合并的网络调用,为了使其成为一个有效选项,下游系统必须处理这种状况。
若是将HystrixCommand配置为仅处理单个用户的批处理请求,则Hystrix仅仅会合并单个Tomcat线程的请求。
例如,若是一个用户想要加载300个影片的标签,Hystrix可以把这300次网络调用合并成一次调用。
有时候,当你建立一个对象模型对消费的对象而言是具备逻辑意义的,这与对象的生产者的有效资源利用率不匹配。
例如,给你300个视频对象,遍历他们,而且调用他们的getSomeAttribute()
方法,可是若是简单的调用,可能会致使300次网络调用(可能很快会占满资源)。
有一些手动的方法能够解决这个问题,好比在用户调用getSomeAttribute()
方法以前,要求用户声明他们想要获取哪些视频对象的属性,以便他们均可以被预取。
或者,您能够分割对象模型,以便用户必须从一个位置获取视频列表,而后从其余位置请求该视频列表的属性。
这些方法能够会使你的API和对象模型显得笨拙,而且这种方式也不符合心理模式与使用模式(译者:不太懂什么意思)。因为多个开发人员在代码库上工做,可能会致使低级的错误和低效率开发的问题。由于对一个用例的优化能够经过执行另外一个用例和经过代码的新路径来打破。
经过将合并逻辑移到Hystrix层,无论你如何建立对象模型,调用顺序是怎样的,或者不一样的开发人员是否知道是否完成了优化或者是否完成。
getSomeAttribute()方法能够放在最适合的地方,并以任何适合使用模式的方式被调用,而且合并器会自动将批量调用放置到时间窗口。
####请求Cache
*
HystrixCommand和HystrixObservableCommand实现能够定义一个缓存键,而后用这个缓存键以并发感知的方式在请求上下文中取消调用(不须要调用依赖便可以获得结果,由于一样的请求结果已经按照缓存键缓存起来了)。
如下是一个涉及HTTP请求生命周期的示例流程,以及在该请求中执行工做的两个线程:
请求cache的好处有:
这在许多开发人员实现不一样功能的大型代码库中尤为有用。
例如,多个请求路径都须要获取用户的Account对象,能够像这样请求:
Account account = new UserGetAccount(accountId).execute(); //or Observable<Account> accountObservable = new UserGetAccount(accountId).observe();
Hystrix RequestCache将只执行一次底层的run()方法,执行HystrixCommand的两个线程都会收到相同的数据,尽管实例化了多个不一样的实例。
每次执行该命令时,再也不会返回一个不一样的值(或回退),而是将第一个响应缓存起来,后续相同的请求将会返回缓存的响应。
因为请求缓存位于construct()或run()方法调用以前,Hystrix能够在调用线程执行以前取消调用。
若是Hystrix没有实现请求缓存功能,那么每一个命令都须要在构造或者运行方法中实现,这将在一个线程排队并执行以后进行。