Hystrix(3)

Circuit Breaker(断路器)

下图显示了 HystrixCommand 或 HystrixObservableCommand 如何与 HystrixCircuitBreaker 交互及其逻辑和决策流程,包括计数器在断路器(circuit breaker)中的行为方式。

 

电路(circuit)开闭发生的具体方式如下:

  1. 假设整个电路的量达到某个阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())...
  2. 并假设错误百分比超过阈值错误百分比(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())...
  3. 然后断路器从 CLOSED 转变为 OPEN。
  4. 当它打开时,它会将针对该断路器的所有请求短路。
  5. 一段时间后(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()),下一个请求被允许通过(这是半开状态)。 如果请求失败,断路器将在睡眠窗口期间返回 OPEN 状态。 如果请求成功,断路器将转换为 CLOSED,并且 1. 中的逻辑再次接管。

Isolation(隔离)

Hystrix 使用隔板模式来隔离彼此的依赖关系并限制对其中任何一个的并发访问。

Threads & Thread Pools

客户端(libraries、网络调用等)在不同的线程上执行。 这将它们与调用线程(Tomcat 线程池)隔离开来,以便调用者可以“离开”耗时过长的依赖调用。

Hystrix 使用单独的、每个依赖项的线程池作为约束任何给定依赖项的一种方式,因此底层执行的延迟将仅使该池中的可用线程饱和。

 

我们可以在不使用线程池的情况下防止失败,但这需要受信任的客户端非常快速地失败(网络连接/读取超时和重试配置)并始终表现良好。

Netflix 在其 Hystrix 的设计中,出于多种原因选择使用线程和线程池来实现隔离,包括:

  • 许多应用程序执行多次(有时甚至超过 100 次)不同的后端服务,调用由许多不同团队开发的数十种不同服务。
  • 每个服务都提供自己的客户端库。
  • 客户端库一直在变化。
  • 客户端库逻辑可以更改以添加新的网络调用。
  • 客户端库可以包含诸如重试、数据解析、缓存(内存中或跨网络)和其他此类行为之类的逻辑。
  • 客户端库往往是“黑匣子”——对于用户来说,实现细节、网络访问模式、配置默认值等都是不透明的。
  • 在几次实际的生产中断中,存在“哦,有些东西发生了变化,应该调整属性”或“客户端库改变了它的行为”。
  • 即使客户端本身没有变化,服务本身也会发生变化,这会影响性能特征,进而导致客户端配置无效。
  • 传递依赖可能会引入其他未预期且可能未正确配置的客户端库。
  • 大多数网络访问是同步执行的。
  • 失败和延迟也可能发生在客户端代码中,而不仅仅是网络调用。

 线程池的好处
通过自己的线程池中的线程进行隔离的好处是:

  • 该应用程序完全不受失控客户端库的影响。 给定依赖库的池可以填满,而不会影响应用程序的其余部分。
  • 该应用程序可以接受风险低得多的新客户端库。 如果出现问题,它会与库隔离,不会影响其他所有内容。
  • 当失败的客户端再次恢复健康时,线程池将清理干净,应用程序立即恢复健康的性能,而不是整个 Tomcat 容器不堪重负时的长时间恢复。
  • 如果客户端库配置错误,线程池的健康状况将很快证明这一点(通过增加的错误、延迟、超时、拒绝等),我们可以在不影响应用程序功能的情况下处理它(通常通过动态属性实时处理) 
  • 如果客户端服务更改了性能特征(这种情况经常发生,足以成为一个问题),进而导致需要调整属性(增加/减少超时、更改重试等),这再次通过线程池指标(错误、延迟、变得可见 、超时、拒绝),并且可以在不影响其他客户端、请求或用户的情况下进行处理。
  • 除了隔离优势之外,拥有专用线程池还提供了内置的并发性,可用于在同步客户端库之上构建异步外观(类似于 Netflix API 如何在 Hystrix 命令之上构建反应式、完全异步的 Java API)

简而言之,线程池提供的隔离允许客户端库和子系统性能特征的不断变化和动态组合得到妥善处理,而不会导致中断。

注意:尽管单独的线程提供了隔离,但我们的底层客户端代码也应该有超时和/或响应线程中断,因此它不会无限期地阻塞并使 Hystrix 线程池饱和。

线程池的缺点
线程池的主要缺点是它们增加了计算开销。 每个命令执行都涉及在单独线程上运行命令所涉及的排队、调度和上下文切换。

Netflix 在设计这个系统时,决定接受这种开销的成本以换取它提供的好处,并认为它足够小,不会对成本或性能产生重大影响。

线程成本
Hystrix 测量子线程上执行construct() 或run() 方法时的延迟以及父线程上的总端到端时间。 通过这种方式,我们可以看到 Hystrix 开销(线程、指标、日志记录、断路器等)的成本。

Netflix API 每天使用线程隔离处理 10+ 亿次 Hystrix 命令执行。 每个 API 实例有 40 多个线程池,每个线程池有 5-20 个线程(大多数设置为 10)。

下图表示一个 HystrixCommand 在单个 API 实例上以每秒 60 个请求的速度执行(每台服务器每秒约 350 个总线程执行):

 在中位数(或更低),拥有一个单独的线程是没有成本的。

在第 90 个百分位,拥有一个单独的线程需要 3 毫秒的成本。

在第 99 个百分位,有一个单独的线程需要 9 毫秒。 但是请注意,成本的增加远小于单独线程(网络请求)的执行时间增加,后者从 2 跳到 28,而成本从 0 跳到 9。

对于大多数 Netflix 用例而言,此类电路的 90% 或更高的开销已被认为是可以接受的,因为可以实现弹性优势。

对于包装非常低延迟请求的电路(例如那些主要命中内存缓存的请求),开销可能太高,在这种情况下,我们可以使用另一种方法,例如可尝试的信号量,虽然它们不允许超时, 无需开销即可提供大部分弹性优势。 然而,一般来说,开销足够小,以至于 Netflix 在实践中通常更喜欢单独线程的隔离优势而不是此类技术。

Semaphores(信号量)

我们可以使用信号量(或计数器)来限制对任何给定依赖项的并发调用数,而不是使用线程池/队列大小。 这允许 Hystrix 在不使用线程池的情况下减轻负载,但它不允许超时和走开。 如果我们信任客户端并且只想要减载,则可以使用这种方法。

HystrixCommand 和 HystrixObservableCommand 在两个地方支持信号量:

  • Fallback:当 Hystrix 使用fallbacks 时,它总是在调用 Tomcat 线程上这样做。
  • Execution:如果将属性 execution.isolation.strategy 设置为 SEMAPHORE,那么 Hystrix 将使用信号量而不是线程来限制调用命令的并发父线程的数量。

我们可以通过定义可以执行多少并发线程的动态属性来配置信号量的这两种用途。 我们应该使用与调整线程池大小时使用的类似计算来调整它们的大小(在亚毫秒时间内返回的内存调用可以在信号量仅为 1 或 2 的情况下执行超过 5000rps ......但默认值为 10)。

注意:如果一个依赖被信号量隔离然后变成潜在的,父线程将保持阻塞,直到底层网络调用超时。

一旦达到限制,信号量拒绝将开始,但填充信号量的线程不能走开。

Request Collapsing

我们可以在 HystrixCommand 前面使用request collapser(HystrixCollapser 是抽象父级),我们可以使用该request collapser将多个request 折叠到单个后端依赖项调用中。

下图显示了两种情况下的线程数和网络连接数:第一个没有request collapsing,第二个有(假设所有连接在很短的时间窗口内“并发”,在本例中为 10 毫秒)。

 Why Use Request Collapsing?

使用request collapsing来减少执行并发 HystrixCommand 执行所需的线程数和网络连接数。request collapsing以自动方式执行此操作,不会强制代码库的所有开发人员协调手动批处理请求。

全局上下文(跨所有 Tomcat 线程)

理想的折叠类型是在全局应用程序级别完成的,因此来自任何 Tomcat 线程上的任何用户的请求都可以折叠在一起。

例如,如果我们配置 HystrixCommand 以支持任何用户对检索电影评分的依赖项的请求进行批处理,那么当同一 JVM 中的任何用户线程发出此类请求时,Hystrix 会将其请求与任何其他请求一起添加到同一 collapsed的网络通话。

请注意,折叠器会将单个 HystrixRequestContext 对象传递给折叠的网络调用,因此下游系统必须处理这种情况才能成为有效的选项。

用户请求上下文(单个 Tomcat 线程)

如果我们将 HystrixCommand 配置为仅处理单个用户的批处理请求,则 Hystrix 可以折叠来自单个 Tomcat 线程(请求)的请求。

例如,如果用户想要为 300 个视频对象加载书签,而不是执行 300 个网络调用,Hystrix 可以将它们全部合并为一个。

对象建模和代码复杂性

有时,当我们创建对对象的消费者具有逻辑意义的对象模型时,这与对象生产者的有效资源利用不匹配。

例如,给定一个包含 300 个视频对象的列表,遍历它们并在每个对象上调用 getSomeAttribute() 是一个明显的对象模型,但如果实施得天真,可能会导致 300 个网络调用都在毫秒内进行(并且很可能饱和 资源)。

我们可以使用手动方式来处理此问题,例如在允许用户调用 getSomeAttribute() 之前,要求他们声明他们想要获取属性的视频对象,以便它们都可以被预取。

或者,我们可以划分对象模型,以便用户必须从一个地方获取视频列表,然后从其他地方请求该视频列表的属性。

这些方法可能会导致笨拙的 API 和对象模型与心智模型和使用模式不匹配。 当多个开发人员在代码库上工作时,它们还可能导致简单的错误和效率低下,因为为一个用例所做的优化可能会被另一个用例的实现和代码中的新路径破坏。

通过将折叠逻辑下推到 Hystrix 层,我们如何创建对象模型、以什么顺序进行调用、或者不同的开发人员是否知道正在完成甚至需要完成的优化都无关紧要。

getSomeAttribute() 方法可以放在最适合的地方,并以适合使用模式的任何方式调用,并且折叠器将自动批量调用时间窗口。

request Collapsing的成本是多少?

启用请求折叠的代价是在执行实际命令之前增加了延迟。 最大成本是批处理窗口的大小。

如果我们有一个执行中位数为 5 毫秒的命令,以及一个 10 毫秒的批处理窗口,那么在最坏的情况下执行时间可能会变为 15 毫秒。 通常,请求不会恰好在窗口打开时被提交到窗口,因此中值惩罚是窗口时间的一半,在本例中为 5 毫秒。

该成本是否值得的确定取决于正在执行的命令。 高延迟命令不会因少量额外的平均延迟而受到太大影响。 此外,给定命令的并发量是关键:如果要批处理的请求很少超过 1 或 2 个,那么付出代价是没有意义的。 事实上,在单线程顺序迭代中,collapsing将是一个主要的性能瓶颈,因为每次迭代都会等待 10 毫秒的批处理窗口时间。

然而,如果一个特定的命令被大量并发使用并且可以同时批处理数十甚至数百个调用,那么成本通常远远超过所获得的吞吐量增加,因为 Hystrix 减少了它所需的线程数和网络连接数 依赖关系。

Collapser Flow

 Request Caching

HystrixCommand 和 HystrixObservableCommand 实现可以定义一个缓存键,然后用于以并发感知的方式对请求上下文中的调用进行重复数据删除。

这是一个示例流程,涉及 HTTP 请求生命周期和在该请求中工作的两个线程:

 请求缓存的好处是:

  • 不同的代码路径可以执行 Hystrix 命令,而不用担心重复工作。
    这在许多开发人员正在实现不同功能的大型代码库中特别有用。

    例如,通过代码的多个路径都需要获取用户的 Account 对象,每个路径都可以像这样请求它:

    Account account = new UserGetAccount(accountId).execute();
    
    //or
    
    Observable<Account> accountObservable = new UserGetAccount(accountId).observe();

    Hystrix RequestCache 将执行底层 run() 方法一次且仅一次,并且执行 HystrixCommand 的两个线程将接收相同的数据,尽管实例化了不同的实例。

  • 数据检索在整个请求中是一致的。

不是每次执行命令时都可能返回不同的值(或回退),而是缓存第一个响应并为同一请求中的所有后续调用返回。

  • 消除重复的线程执行。

由于请求缓存位于construct() 或run() 方法调用的前面,因此Hystrix 可以在调用导致线程执行之前对其进行重复数据删除。

如果 Hystrix 没有实现请求缓存功能,那么每个命令都需要在构造或运行方法中自己实现它,这将把它放在线程排队并执行之后。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值