Netflix 的 Hystrix:互联世界中的容错

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观


在互联世界中,我们的软件解决方案越来越多地由多个分布式组件组成。面向服务的架构并不是什么新鲜事,随着人们对微服务的兴趣越来越大,分布式趋势似乎很快就会消失。

在处理远程组件时,我们需要考虑两类(非常)广泛的问题:

  1. 首先,我们如何应对这些远程系统中的故障,以及

  2. 其次,我们如何管理对这些远程系统的调用以保持我们自己的系统的性能,并将延迟保持在最低限度

什么是 Hystrix?

Hystrix 是一个库,最初由 Netflix 开发,可让您处理复杂分布式系统中的延迟和容错问题。它预期会发生各种故障,并提供许多工具和解决方案来处理不同的常见场景。

Hystrix 是开源的,可以在 Github 上找到。用他们自己的话来说——“Hystrix 是一个延迟和容错库,旨在隔离远程系统、服务和第三方库的访问点,停止级联故障,并在故障不可避免的复杂分布式系统中实现弹性。”

演示应用程序 - 博彩服务

我创建了一个简单的演示应用程序——可以在 Github 上找到。点击以下链接:


示例应用程序使用一个简单的 BettingService 来说明使用 Hystrix 访问远程服务时可用的一些工具。

BettingService.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

该服务公开了 2 个域对象 - Racecourse 和 Horse:

赛马场.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

马.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

Hysterix 的方法

处理失败

当应用程序高度依赖于其他服务时,这些系统中的任何故障都可能对我们自己的调用应用程序产生不利影响。作为调用应用程序的开发人员,这些问题的根源在很大程度上是我们无法控制的,但是我们如何处理它们对于我们自己系统的高效运行至关重要。

Hysterix 主要是围绕使用命令设计模式构建的。每个远程服务调用都包装在一个 HysterixCommand(用于同步调用)或一个 HysterixObserveableCommand(用于异步调用)中

简单的失败

在最简单的场景中 - 远程服务不可用或抛出异常的情况,HysterixCommand 允许您定义您希望在 getFallback() 方法中采取的操作(这可能是传播异常,返回默认设置,或一个特殊的错误值)。

断路器 - 快速失败

调用远程系统可能代价高昂——即使系统返回错误,也可能有一段时间等待错误返回。这会给调用系统带来延迟,还会占用资源——甚至可能用完线程池中的所有线程(请参阅下面的更多信息)。

Hystrix 引入了断路器的概念以促进快速失败。如果它快速连续地检测到许多类似的故障(音量和错误阈值的某种组合)——它可以触发断路器,迫使所有后续调用快速失败,而无需对远程服务器进行远程调用(通过 HystricCommands getFallback( 调用) 方法如上所述)。在一段可配置的时间后,它将再次关闭电路并开始处理远程调用(同样,如果它仍然检测到错误状态,它将重新打开断路器,等等)。这避免了我们在知道远程系统明显存在问题时盲目调用远程系统——消除延迟并允许我们以预定义的方法处理它——这可能包括返回缓存数据,或故障转移到备份系统。

延迟和线程池

如上所述,失败(或什至成功但缓慢的调用)的一个更隐蔽的方面是可能耗尽调用池中的所有线程。由于每个调用在等待来自远程系统的响应时都会阻塞一个线程,因此新调用会占用池中的更多线程。如果该池没有与主应用程序服务器池隔离 - 它有可能使整个调用应用程序停止运行。

Hystrix 允许使用它自己的线程池——因此调用应用程序线程永远不会有被用完的危险,从而允许它关闭耗时过长的调用。不同的命令或命令组可以配置自己的线程池,因此可以隔离不同的服务调用集(例如,如果远程服务 A 有延迟问题,则不会泄漏对远程服务 B 的调用的影响).这有助于隔离和管理跨不同客户端库和网络的调用。

但是,使用线程池会产生开销。 Netflix 做了他们自己的指标(每天使用线程隔离执行 10+ 十亿次 Hystrix 命令),并得出结论,他们系统的开销非常小,隔离带来的好处超过了它。

Hystrix 使用的架构如下图所示。每个依赖项彼此隔离,限制在发生延迟时它可能饱和的资源,并包含在回退逻辑中,该逻辑决定在依赖项中发生任何类型的故障时做出何种响应:

高效编码客户呼叫

响应故障的另一面是尽可能高效地编写远程调用代码,以尽量减少延迟/负载问题。

同样,Hystrix 在库中提供了一些工具来帮助处理这个问题。

请求折叠

如果您想象在我们的客户端应用程序上同时使用该系统的多个用途,很可能某些用户操作会导致类似的请求几乎同时发送到远程服务(例如获取苹果公司纳斯达克股票价格:AAPL)。如果没有任何干预——这将导致几乎实时地通过网络发送几乎重复的请求。

Request collapsing 在请求远程服务调用和执行它之间引入了一个小的等待时间 - 以查看窗口中是否有任何重复请求(这个窗口通常非常小 - 例如 10ms)。可以选择使用“全局”上下文(即折叠任何 Tomcat 线程上任何用户的所有请求)或“用户请求”上下文(即折叠单个 Tomcat 线程的所有请求)。

虽然一个有用的工具 - 请求折叠实际上只在特定命令被频繁调用的情况下才有用,允许它将数十个甚至数百个调用批处理在一起,减少线程和网络负载。对于高延迟调用尤其如此,等待折叠窗口几毫秒的开销不会对调用的整体响应时间产生实际影响。

对于不频繁或低延迟所需的命令,崩溃的成本将超过其好处。

请求缓存

请求缓存有助于确保不同线程不会对外部服务进行重复/冗余调用。因为缓存位于构造/运行方法的前面,这意味着 Hystrix 可以在为每个重复请求创建和执行新线程之前返回缓存的结果。通过简单地覆盖 getCacheKey() 方法,可以很容易地将缓存添加到 HystrixCommand 或 HystrixObserveableCommand(这将提供 Hystrix 用来确定对命令的后续请求是否与先前请求重复的密钥)。

投注服务的例子

调用“getTodaysRaces”

这是客户可以在博彩服务上拨打的电话,以获取当天所有可用比赛的列表。只是扩展 HystrixCommand 以访问远程服务的最简单示例(有关如何调用它的示例,请参见下面的单元测试)。

命令GetTodaysRaces.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

调用“GetHorsesInRaceWithCaching”

这是客户可以在博彩服务上进行的调用,以获取参加特定比赛的所有马匹的列表。我将其用作示例,说明如何通过覆盖 HystrixCommand 上的 getCacheKey() 方法来实现缓存。在此示例中,我们使用 raceId 作为缓存键。可以详细配置缓存、过期等的详细信息(但这超出了本文的范围)。

CommandGetHorsesInRaceWithCaching.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }


以下是一些单元测试,以各种方式说明此命令的用法:

  • testSynchronous() - 显示使用命令的基本同步调用

  • testSynchronousFailSilently() - 显示了一个基本的同步调用,它从远程服务捕获一个异常,吞下它并返回一个空列表

  • testSynchronousFailFast() - 展示了一个基本的同步调用,它从远程服务中捕获异常,并抛出一个新的 RemoteServiceException

  • testAsynchronous() - 显示使用 Futures 的基本异步调用以使用相同的命令

  • testObservable() - 显示使用 Observables 的基本异步调用以使用相同的命令

  • testWithCacheHits() - 说明命令如何缓存来自服务器的响应,从而减少不必要的远程调用


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

调用“GetOddsForHorse”

现在这最后一个命令有点高级——利用批处理和请求折叠。因为这可能会被快速连续地重复调用——我们不太担心它们是否在响应中有一些非常小的延迟——我们不想用类似的请求来淹没服务。使用折叠会在执行命令后引入非常轻微(毫秒)的延迟,因此它可以等待查看是否再次出现相同的请求。如果是这样,它可以对该服务进行一次调用并将相同的结果返回给所有请求者。同样,其细节是可配置的。

下面的类展示了如何为我们的特定示例完成此操作,它们也是一个单元测试,用于执行此操作并确认它正在折叠请求。

GetOddsForHorseRequest.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

BatchCommandGetOddsForHorse.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

CommandCollapserGetOddsForHorse.java


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }

单元测试“CommandCollapserGetOddsForHorse”

下面的代码显示了单元测试,用于检查执行此特定命令时请求是否被折叠。


 package com.cor.hysterix.service;
import java.util.List;
import com.cor.hysterix.domain.Horse;
import com.cor.hysterix.domain.Racecourse;

/**

  • Simulates the interface for a remote betting service.

*/

public interface BettingService {

/**

  • Get a list the names of all Race courses with races on today.
  • @return List of race course names */

List<Racecourse> getTodaysRaces();

/**

  • Get a list of all Horses running in a particular race.
  • @param race Name of race course
  • @return List of the names of all horses running in the specified race */

List<Horse> getHorsesInRace(String raceCourseId);

/**

  • Get current odds for a particular horse in a specific race today.
  • @param race Name of race course
  • @param horse Name of horse
  • @return Current odds as a string (e.g. "10/1") */

String getOddsForHorse(String raceCourseId, String horseId); }


结论

如果您在访问远程服务时从事任何类型的工作——我绝对建议您检查一下 Hystrix。在最基本的层面上 - 它迫使您考虑在开发阶段早期访问服务时可能出现的所有问题 - 当解决方案最容易实施时。这些调用通常是在“理想世界”环境中开发的,只有当它们进入用户验收测试或更糟的是生产时,问题才会变得明显。

本文只是尝试尝试 Hystrix 使用的一些方法 - 并提供一个工作项目来帮助开始使用它。在 Github 上查看 - 运行单元测试来执行模拟的服务调用。

Hystrix 本身是开源的——可以在 Github 上找到—— 这里有一个 wiki 和更详细的解释

所以就...