流程图
下图显示了通过Hystrix向服务依赖项请求时发生的情况:
以下各节将更详细地说明此流程:
1.构造一个HystrixCommand或HystrixObservableCommand对象
第一步是构造一个HystrixCommand或HystrixObservableCommand对象,以表示您对依赖项提出的请求。向构造函数传递发出请求时所需的任何参数。
HystrixCommand如果期望依赖项返回单个响应,则构造一个对象。例如:
HystrixCommand command = new HystrixCommand(arg1, arg2);
HystrixObservableCommand如果期望依赖项返回一个发出响应的Observable,则构造一个对象。例如:
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
2.执行命令
通过使用Hystrix命令对象的以下四种方法之一,可以执行命令的四种方法(前两种仅适用于简单HystrixCommand对象,不适用于HystrixObservableCommand):
- execute() —阻止,然后返回从依赖项接收到的单个响应(或在发生错误的情况下引发异常)
- queue()—返回一个Future,您可以从中获取依赖项的单个响应
- observe()—订阅,该Observable代表了来自依赖项的响应,并返回了一个Observable复制该源的。Observable
- toObservable()—返回一个Observable,当您订阅它时,将执行Hystrix命令并发出其响应
K value = command.execute();Future fValue = command.queue();Observable ohValue = command.observe(); //hot observableObservable ocValue = command.toObservable(); //cold observable
同步调用execute()调用queue().get()。queue()依次调用toObservable().toBlocking().toFuture()。就是说,最终每个实现HystrixCommand都由Observable实现支持,即使是那些旨在返回单个简单值的命令也是如此
3.响应是否已缓存?
如果为此命令启用了请求缓存,并且如果对请求的响应在缓存中可用,则该缓存的响应将立即以的形式返回Observable。(请参见下面的“请求缓存”。)
4.电路是否开路?
当您执行该命令时,Hystrix会检查断路器,以查看电路是否断开。
如果电路开路(或“跳闸”),那么Hystrix将不执行命令,而是将流路由到(8)获取回退。
如果电路是闭合的,则流程进行到(5),以检查是否有足够的容量来运行该命令。
5.线程池/队列/信号量是否已满?
如果与该命令关联的线程池和队列(或信号量,如果未在线程中运行)已满,则Hystrix将不执行该命令,但会立即将流路由到(8)获取回退。
6. HystrixObservableCommand.construct()或HystrixCommand.run()
在这里,Hystrix通过为此目的编写的方法(以下之一)调用对依赖项的请求:
- HystrixCommand.run() —返回单个响应或引发异常
- HystrixObservableCommand.construct()—返回一个Observable,它发出响应或发送onError通知
如果run()or construct()方法超出命令的超时值,则该线程将抛出TimeoutException(如果命令本身未在其自己的线程中运行,则该线程将抛出单独的计时器线程)。在那种情况下,Hystrix通过8路由响应。获取Fallback,如果该方法没有取消/中断,它将丢弃最终的返回值run()或construct()方法。
请注意,没有办法强制潜在线程停止工作-Hystrix在JVM上能做的最好的事情就是将其抛出InterruptedException。如果Hystrix封装的工作不遵守InterruptedExceptions,尽管客户端已收到TimeoutException,Hystrix线程池中的线程仍将继续工作。尽管负载已“正确释放”,但此行为可能会使Hystrix线程池饱和。大多数Java HTTP客户端库不解释InterruptedExceptions。因此,请确保在HTTP客户端上正确配置连接和读取/写入超时。
如果命令没有引发任何异常并且返回了响应,则Hystrix在执行一些日志记录和度量标准报告后将返回此响应。在的情况下run(),Hystrix返回Observable发出单个响应并发出onCompleted通知的;在的情况下construct()猬返回相同Observable的返回construct()。
7.计算Circuit健康
Hystrix向断路器报告成功,失败,拒绝和超时,断路器保持滚动的一组计算统计信息的计数器。
它使用这些统计信息来确定电路何时应“跳闸”,在此点它会将随后的所有请求短路,直到经过恢复期为止,在此之后,在首先检查某些运行状况检查之后,它将再次闭合电路。
8.获取后备
Hystrix尝试在命令执行失败时恢复到您的后备状态:当construct()或引发异常run()(6.),由于电路断开而使命令短路(4.),命令的线程池和队列或信号量为最大容量(5.),或命令已超过其超时长度。
编写后备,以从内存高速缓存或其他静态逻辑提供通用响应,而无需任何网络依赖性。如果必须在回退中使用网络呼叫,则应通过另一个HystrixCommand或进行HystrixObservableCommand。
对于HystrixCommand,要提供回退逻辑,您可以实现HystrixCommand.getFallback(),该逻辑返回单个回退值。
对于HystrixObservableCommand,要提供后备逻辑,您可以实现HystrixObservableCommand.resumeWithFallback()该逻辑,该逻辑返回一个Observable,它可能会发出一个或多个后备值。
如果fallback方法返回响应,则Hystrix将把此响应返回给调用方。对于a HystrixCommand.getFallback(),它将返回一个Observable,它发出从方法返回的值。在这种情况下,HystrixObservableCommand.resumeWithFallback()它将返回从方法返回的相同Observable。
如果尚未为Hystrix命令实现后备方法,或者后备本身引发异常,则Hystrix仍会返回一个Observable,但它不发出任何内容并立即以onError通知终止。通过该onError通知,将导致命令失败的异常发送回调用方。(实施回退实现可能会失败,这是一种糟糕的做法。您应该实施回退,以使其不执行任何可能失败的逻辑。)
后备失败或不存在的后备结果将因调用Hystrix命令的方式而异:
- execute() -引发异常
- queue()—成功返回Future,但是Future如果get()调用其方法,则会抛出异常
- observe()—返回一个Observable,当您订阅它时,将通过调用订阅者的onError方法立即终止
- toObservable()—返回一个Observable,当您订阅它时,它将通过调用订阅者的onError方法终止
9.返回成功的回应
如果Hystrix命令成功执行,它将以的形式将一个或多个响应返回给调用方Observable。根据您在上面的步骤2中调用命令的方式,在Observable返回给您之前,可能会对其进行转换:
- execute()-获得Future的相同方式一样.queue(),然后调用get()在此Future以获得由所发射的单个值Observable
- queue()—将转换Observable为,BlockingObservable以便可以将其转换为Future,然后返回Future
- observe()— Observable立即订阅并开始执行命令的流程;返回一个Observable,当您subscribe重播时,它会排放和通知
- toObservable()—返回Observable不变;您必须先subscribe执行此命令才能真正开始导致执行命令的流程
顺序图
上述流程可用官方的时序图来表示
https://design.codelytics.io/hystrix/how-it-works
断路器
下图显示了a HystrixCommand或HystrixObservableCommand与a交互的方式HystrixCircuitBreaker以及其逻辑和决策流程,包括计数器在断路器中的行为。
电路打开和关闭的精确方式如下:
- 假设电路上的音量达到某个阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())...
- 并假设误差百分比超过阈值误差百分比(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())...
- 然后,断路器从转换CLOSED为OPEN。
- 当它断开时,它会使针对该断路器的所有请求短路。
- 经过一段时间(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds())后,下一个单个请求被允许通过(这是HALF-OPEN状态)。如果请求失败,断路器将OPEN在睡眠窗口期间返回到该状态。如果请求成功,断路器将切换到,CLOSED并且1.中的逻辑将再次接管。
隔离
Hystrix使用隔板模式将依赖关系彼此隔离,并限制对其中任何一个的并发访问。
线程和线程池
客户端(库,网络调用等)在单独的线程上执行。这样可以将它们与调用线程(Tomcat线程池)隔离,以便调用者可以“摆脱”花费太长时间的依赖项调用。
Hystrix使用单独的,每个依赖关系的线程池作为约束任何给定依赖关系的方式,因此对基础执行的延迟将仅使该池中的可用线程饱和。
您可以在不使用线程池的情况下防止失败,但这需要信任的客户端非常迅速地失败(网络连接/读取超时和重试配置),并且始终表现良好。
Netflix在Hystrix的设计中选择使用线程和线程池来实现隔离的原因很多,其中包括:
- 许多应用程序会针对许多不同团队开发的数十种不同服务执行数十种(有时甚至超过100种)不同的后端服务调用。
- 每个服务都提供自己的客户端库。
- 客户端库一直在变化。
- 客户端库逻辑可以更改以添加新的网络调用。
- 客户端库可以包含诸如重试,数据解析,缓存(内存中或跨网络)以及其他此类行为的逻辑。
- 客户端库往往是“黑匣子”-用户对其实现细节,网络访问模式,配置默认值等不透明。
- 在实际的几次生产中断中,确定为“哦,某些更改并且应该调整属性”或“客户端库更改了其行为”。
- 即使客户端本身未更改,服务本身也会更改,这可能会影响性能特征,进而导致客户端配置无效。
- 传递依赖项可能会引入其他意外的客户端库,这些客户端库可能不是预期的,也可能配置不正确。
- 大多数网络访问是同步执行的。
- 故障和延迟也可能在客户端代码中发生,而不仅仅是在网络调用中。
线程池的好处
通过自己线程池中的线程进行隔离的好处是:
- 该应用程序受到完全保护,不受客户端库的攻击。给定依赖库的池可以填满,而不会影响应用程序的其余部分。
- 该应用程序可以接受风险更低的新客户端库。如果发生问题,它将隔离到库中并且不会影响其他所有内容。
- 当发生故障的客户端再次恢复正常运行时,线程池将被清除,应用程序将立即恢复运行正常的性能,而整个Tomcat容器不堪重负的情况下,恢复时间很长。
- 如果客户端库配置错误,线程池的运行状况将迅速证明这一点(通过增加错误,延迟,超时,拒绝等),您可以在不影响应用程序功能的情况下进行处理(通常是通过动态属性实时进行)。 。
- 如果客户端服务更改了性能特征(通常会经常出现问题),进而导致需要调整属性(增加/减少超时,更改重试次数等),则可以通过线程池指标(错误,延迟)再次看到该特征,超时,拒绝),并且可以在不影响其他客户端,请求或用户的情况下进行处理。
- 除了隔离优势之外,拥有专用线程池还可以提供内置的并发性,可以利用这些并发性在同步客户端库之上构建异步外观(类似于Netflix API如何在Hystrix命令之上构建反应性,完全异步的Java API)。 。
简而言之,线程池提供的隔离允许客户端库和子系统性能特征的不断变化和动态组合得到优雅处理,而不会造成中断。
注意:尽管有单独的线程提供了隔离,但是您的基础客户端代码也应具有超时和/或对线程中断的响应,因此它不能无限期地阻塞并使Hystrix线程池饱和。
线程池的缺点
线程池的主要缺点是它们增加了计算开销。每个命令执行都涉及在单独的线程上运行命令所涉及的队列,调度和上下文切换。
Netflix在设计此系统时,决定接受此间接费用,以换取其提供的好处,并认为它很小,不会对成本或性能造成重大影响。
线程成本
Hystrix测量在子线程上执行construct()or run()方法时的延迟以及父线程上的总的端到端时间。这样,您可以看到Hystrix开销(线程,度量,日志记录,断路器等)的成本。
Netflix API使用线程隔离每天处理10+亿次Hystrix Command执行。每个API实例有40多个线程池,每个线程池中有5-20个线程(大多数设置为10)。
下图表示一个HystrixCommand在单个API实例上以每秒60个请求的速度执行的情况(每个服务器每秒约350个线程执行总数):
在中位数(或更低)处,拥有单独的线程没有任何成本。
在第90 个百分位数处,拥有一个单独的线程要花费3ms的时间。
在第99 个百分位数处,拥有单独的线程需要9毫秒的时间。但是请注意,成本的增加远远小于单独线程(网络请求)的执行时间的增加,后者从2跃升至28,而成本从0跃升至9。
此开销在90 个百分位和更高的对电路,诸如这些已被认为对大多数Netflix的用例的弹性实现的益处可以接受的。
对于包装延迟非常低的请求的电路(例如那些主要访问内存缓存的请求),开销可能会过高,在这种情况下,您可以使用其他方法,例如可尝试的信号量,尽管它们不允许超时,提供最大的弹性优势,而没有开销。但是,总的来说开销很小,以至于Netflix实际上通常比这种技术更喜欢使用单独线程的隔离优势。
信号量
您可以使用信号量(或计数器)将并发调用的数量限制为任何给定的依赖项,而不是使用线程池/队列大小。这使Hystrix无需使用线程池就可以减轻负载,但它不允许超时和退出。如果您信任客户端,并且只希望减少负载,则可以使用这种方法。
HystrixCommand并HystrixObservableCommand在2个地方支持信号灯:
- 后备: Hystrix检索后备时,总是在调用Tomcat线程上进行。
- 执行:如果将该属性设置为execution.isolation.strategy,SEMAPHORE则Hystrix将使用信号量而不是线程来限制调用该命令的并发父线程的数量。
您可以通过定义可以执行多少个并发线程的动态属性来配置信号灯的这两种用法。您应该使用与调整线程池大小时类似的计算来确定它们的大小(在不到毫秒的时间内返回的内存中调用的性能可以超过5000rps,并且信号量仅为1或2…但默认值为10)。
注意:如果依赖关系被信号量隔离,然后变为潜在状态,则父线程将保持阻塞状态,直到基础网络调用超时为止。
一旦达到限制,信号灯拒绝将开始,但是填充信号灯的线程无法释放。
请求折叠
您可以HystrixCommand在请求折叠器(HystrixCollapser是抽象父项)的前面加上,可以将多个请求折叠到单个后端依赖项调用中。
下图显示了两种情况下的线程和网络连接数:首先是没有连接,然后是请求折叠(假设所有连接在较短的时间窗口内(在这种情况下为10ms)是“并发的”)。
为什么使用请求折叠?
使用请求折叠可减少执行并发HystrixCommand执行所需的线程数和网络连接数。请求折叠以一种自动化的方式完成,不会强制代码库的所有开发人员协调手动的请求批处理。
全局上下文(跨所有Tomcat线程)
理想的折叠类型是在全局应用程序级别完成的,因此可以将任何Tomcat线程上任何用户的请求折叠在一起。
例如,如果将a配置HystrixCommand为在对检索电影分级的依赖项的请求中支持任何用户的批处理,则当同一JVM中的任何用户线程发出这样的请求时,Hystrix都会将其请求与其他任何请求一起添加到同一折叠中网络通话。
请注意,折叠器会将单个HystrixRequestContext对象传递给折叠的网络调用,因此下游系统必须处理这种情况才能使其成为有效的选择。
用户请求上下文(单个Tomcat线程)
如果将a配置HystrixCommand为仅处理单个用户的批处理请求,则Hystrix可以折叠单个Tomcat线程内的请求(请求)。
例如,如果用户想为300个视频对象加载书签,而不是执行300个网络调用,Hystrix可以将它们全部合并为一个。
对象建模和代码复杂度
有时,当您创建对对象的使用者具有逻辑意义的对象模型时,这与对象的生产者的有效资源利用并不十分匹配。
例如,给定一个300个视频对象的列表,对其进行遍历并调用getSomeAttribute()每个对象是一个显而易见的对象模型,但是如果天真地实现,则可能导致300个网络调用全部在彼此之间的毫秒之内进行(并且很可能会占用资源)。
有一些手动方法可以处理此问题,例如在允许用户调用之前getSomeAttribute(),要求他们声明要为其获取属性的视频对象,以便可以全部提取它们。
或者,您可以划分对象模型,以便用户必须从一个地方获取视频列表,然后从其他地方询问该视频列表的属性。
这些方法可能导致笨拙的API和对象模型与思维模型和使用模式不匹配。当多个开发人员在一个代码库上工作时,它们还可能导致简单的错误和效率低下,因为针对一个用例进行的优化可能会因另一个用例的实现和代码的新路径而中断。
通过将折叠逻辑向下推到Hystrix层,无论如何创建对象模型,以什么顺序进行调用,或者不同的开发人员是否知道正在完成甚至需要进行优化,都无关紧要。
该getSomeAttribute()方法可以放在最合适的位置,并以适合使用模式的任何方式调用,然后折叠器会自动将调用批量处理到时间窗口中。
请求崩溃的成本是多少?
启用请求崩溃的代价是在执行实际命令之前增加了等待时间。最大成本是批处理窗口的大小。
如果您有一条命令需要花费5ms的中位数执行时间和10ms的批处理窗口,则在最坏的情况下执行时间可能变为15ms。通常,一个请求不会在打开时就被提交到窗口,因此中值损失是窗口时间的一半,在这种情况下为5ms。
确定此成本是否值得取决于所执行的命令。高延迟命令不会受到少量额外平均延迟的影响。同样,给定命令的并发量很关键:如果很少有超过1或2个请求被一起批处理,那么付出代价是没有意义的。实际上,在单线程顺序迭代中,折叠将是主要的性能瓶颈,因为每次迭代将等待10ms的批处理窗口时间。
但是,如果特定命令同时大量使用,并且可以将数十个甚至数百个呼叫分批处理,则由于Hystrix减少了所需的线程数和与之连接的网络数,因此获得的吞吐量通常远远超过成本。依赖性。
塌方流
请求缓存
HystrixCommand和HystrixObservableCommand实施方式可以定义一个缓存键,然后将其用于去重复数据删除在并发感知方式的请求范围内的呼叫。
这是一个示例流程,涉及HTTP请求生命周期和两个在该请求中执行工作的线程:
请求缓存的好处是:
- 不同的代码路径可以执行Hystrix命令,而无需担心重复的工作。
这在大型代码库中特别有用,在该代码库中,许多开发人员正在实现不同的功能。
例如,所有需要获取用户Account对象的代码的多个路径都可以这样请求:
Account account = new UserGetAccount(accountId).execute();//orObservable accountObservable = new UserGetAccount(accountId).observe();
Hystrix RequestCache将run()一次且仅执行一次底层方法,并且HystrixCommand尽管实例化了不同的实例,但执行的两个线程将接收相同的数据。
- 在整个请求中,数据检索是一致的。
而不是每次执行命令时都可能返回不同的值(或回退),而是将第一个响应缓存并为同一请求内的所有后续调用返回。
- 消除重复的线程执行。
由于请求缓存位于construct()或run()方法调用的前面,因此Hystrix可以在导致线程执行之前对重复项进行重复数据删除。
如果Hystrix没有实现请求缓存功能,则每个命令都需要自己在constructor run方法中实现它,这会将其放在线程排队和执行之后。