下面的流程图展示了当使用Hystrix的依赖请求,Hystrix是如何工作的。
图片描述
下面将更详细的解析每一个步骤都发生哪些动作:
HystrixCommand
或者HystrixObservableCommand
对象。
第一步就是构建一个HystrixCommand
或者HystrixObservableCommand
对象,该对象将代表你的一个依赖请求,向构造函数中传入请求依赖所需要的参数。
如果构建HystrixCommand
中的依赖返回单个响应,例如:
HystrixCommand command = new HystrixCommand(arg1, arg2);
如果依赖需要返回一个Observable
来发射响应,就需要通过构建HystrixObservableCommand
对象来完 成,例如:
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
execute()
—该方法是阻塞的,从依赖请求中接收到单个响应(或者出错时抛出异常)。queue()
—从依赖请求中返回一个包含单个响应的Future对象。observe()
—订阅一个从依赖请求中返回的代表响应的Observable对象。toObservable()
—返回一个Observable对象,只有当你订阅它时,它才会执行Hystrix命令并发射响应。
K value = command.execute();
Future fValue = command.queue();
Observable ohValue = command.observe(); //hot observable
Observable ocValue = command.toObservable(); //cold observable同步调用方法execute()
实际上就是调用queue().get()
方法,queue()
方法的调用的是toObservable().toBlocking().toFuture()
.也就是说,最终每一个HystrixCommand都是通过Observable来实现的,即使这些命令仅仅是返回一个简单的单个值。
Observable
(下面将Request Cache部分将对请求的cache做讲解)。8
步,获取fallback方法,并执行fallback逻辑。
如果回路器关闭,那么将进入第5
步,检查是否有足够的容量来执行任务。(其中容量包括线程池的容量,队列的容量等等)。8
步,执行fallback逻辑。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对象。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()方法。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采用舱壁模式来隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。
图片描述
图片描述
您可以在不使用线程池的情况下防止出现故障,但是这要求客户端必须能够做到快速失败(网络连接/读取超时和重试配置),并始终保持良好的执行状态。
Netflix,设计Hystrix,并且选择使用线程和线程池来实现隔离机制,有以下几个原因:
图片描述
简而言之,由线程池提供的隔离功能可以使客户端库和子系统性能特性的不断变化和动态组合得到优雅的处理,而不会造成中断。
注意:虽然单独的线程提供了隔离,但您的底层客户端代码也应该有超时和/或响应线程中断,而不能让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)。
图片描述
getSomeAttribute()
方法,但是如果简单的调用,可能会导致300次网络调用(可能很快会占满资源)。
有一些手动的方法可以解决这个问题,比如在用户调用getSomeAttribute()
方法之前,要求用户声明他们想要获取哪些视频对象的属性,以便他们都可以被预取。
或者,您可以分割对象模型,以便用户必须从一个位置获取视频列表,然后从其他位置请求该视频列表的属性。
这些方法可以会使你的API和对象模型显得笨拙,并且这种方式也不符合心理模式与使用模式(译者:不太懂什么意思)。由于多个开发人员在代码库上工作,可能会导致低级的错误和低效率开发的问题。因为对一个用例的优化可以通过执行另一个用例和通过代码的新路径来打破。
通过将合并逻辑移到Hystrix层,不管你如何创建对象模型,调用顺序是怎样的,或者不同的开发人员是否知道是否完成了优化或者是否完成。
getSomeAttribute()方法可以放在最适合的地方,并以任何适合使用模式的方式被调用,并且合并器会自动将批量调用放置到时间窗口。*
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没有实现请求缓存功能,那么每个命令都需要在构造或者运行方法中实现,这将在一个线程排队并执行之后进行。