博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Hystrix
阅读量:2154 次
发布时间:2019-05-01

本文共 9672 字,大约阅读时间需要 32 分钟。

在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有 的时候某些依赖服务出现故障也是很正常的。 Hystrix 可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。

Hystrix 通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时Hystrix 还提供故障时的 fallback 降级机制。 总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。

Hystrix 的设计原则

  • 对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护。
  • 在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延。比如某一个服务故障了,导致其它服务也跟着故障。
  • 提供 fail-fast (快速失败)和快速恢复的支持。
  • 提供 fallback 优雅降级的支持。
  • 支持近实时的监控、报警以及运维操作

举个栗子。

有这样一个分布式系统,服务 A 依赖于服务 B,服务 B 依赖于服务 C/D/E。在这样一个成熟的系统内,比如说最多可能只有 100 个线程资源。正常情况下,40 个线程并发调用服务 C,各 30 个线程并发调用D/E。

调用服务 C,只需要 20ms,现在因为服务 C 故障了,比如延迟,或者挂了,此时线程会 hang 住 2s 左右。40 个线程全部被卡住,由于请求不断涌入,其它的线程也会来调用服务 C,同样也会被卡住。这样导致服务 B 的线程资源被耗尽,无法接收新的请求,甚至可能因为大量线程不断的运转,导致自己宕机。服务 A 也挂。

Hystrix 可以对其进行资源隔离,比如限制服务 B 只有 40 个线程调用服务 C。当此 40 个线程被 hang 住时,其它 60 个线程依然能正常调用工作。从而确保整个系统不会被拖垮。

Hystrix 更加细节的设计原则

  • 阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源。
  • 避免请求排队和积压,采用限流和 fail fast 来控制故障。
  • 提供 fallback 降级机制来应对故障。
  • 使用资源隔离技术,比如 bulkhead (舱壁隔离技术)、 swimlane (泳道技术)、 circuit breaker(断路技术)来限制任何一个依赖服务的故障的影响。
  • 通过近实时的统计/监控/报警功能,来提高故障发现的速度。
  • 通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度。
  • 保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况。

基于 Hystrix 线程池技术实现资源隔离

资源隔离,就是说,你如果要把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了,这就叫资源隔离。哪怕对这个依赖服务,比如说商品服务,现在同时发起的调用量已经到了 1000,但是线程池内就10 个线程,最多就只会用这 10 个线程去执行,不会说,对商品服务的请求,因为接口调用延时,将 tomcat 内部所有的线程资源全部耗尽。Hystrix 进行资源隔离,其实是提供了一个抽象,叫做 command。这也是 Hystrix 最最基本的资源隔离技术。

默认情况下,Hystrix 使用线程池模式,线程池大小默认为10,等待队列积压大于设定大小后,直接 reject,拒绝请求,执行 fallback 降级的逻辑,快速返回。

基于 Hystrix 信号量机制实现资源隔离

信号量的资源隔离只是起到一个开关的作用,比如,服务 A 的信号量大小为 10,那么就是说它同时只允许有 10 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝,从而达到资源隔离和限流保护的作用。

默认值是10,尽量设置的小一些,因为一旦设置的太大,而且有延时发生,可能瞬间导致 tomcat 本身的线程资源被占满。

线程池与信号量区别

线程池隔离技术,并不是说去控制类似 tomcat 这种 web 容器的线程。更加严格的意义上来 说,Hystrix 的线程池隔离技术,控制的是 tomcat 线程执行。Hystrix 线程池满后,会确保说,tomcat 的线程不会因为依赖服务的接口调用延迟或故障而被 hang 住,tomcat 其它的线程不会卡死,可以快速返回,然后支撑其它的事情。

线程池隔离技术,是用Hystrix 自己的线程去执行调用;而信号量隔离技术,是直接让 tomcat 线程去调用依赖服务。信号量隔离,只是一道关卡,信号量有多少,就允许多少个 tomcat 线程通过它,然后去执行。

适用场景:

  • 线程池技术,适合绝大多数场景,比如说我们对依赖服务的网络请求的调口和访问、需要对调用的 timeout 进行控制(捕捉 timeout
    超时异常)。
  • 信号量技术,适合说你的访问不是对外部依赖的访问,而是对内部的一些比较复杂的业务逻辑的访问,并且系统内部的代码,其实不涉及任何的网络请求,那么只要做信号量的普通限流就可以了,因为不需要去捕获
    timeout 类似的问题。

深入Hystrix 执行时内部原理

Hystrix 最基本的支持高可用的技术:资源隔离 + 限流。

  • 创建 command;
  • 执行这个 command;
  • 配置这个 command 对应的 group 和线程池。

Hystrix 底层的执行流程和步骤以及原理

步骤一:创建 command

一个 HystrixCommand 或 HystrixObservableCommand 对象,代表了对某个依赖服务发起的一次请求或者调用。创建的时候,可以在构造函数中传入任何需要的参数。

HystrixCommand 主要用于仅仅只返回一个结果的调用。 HystrixObservableCommand 主要用于可能会返回多条结果的调用。

步骤二:调用 command 执行方法

执行command,就可以发起一次对依赖服务的调用。 要执行command,可以在 4 个方法中选择一个:execute()、queue()、observe()、toObservable()。

其中 execute() 和 queue() 方法仅仅对 HystrixCommand适用

  • execute():调用后直接 block 住,属于同步调用,直到依赖服务返回单条结果,或者抛出异常。
  • queue():返回一个 Future,属于异步调用,后面可以通过 Future 获取单条结果。
  • observe():订阅一个 Observable 对象,Observable 代表的是依赖服务返回的结果,获取到一个那个代表结果的
    Observable 对象的拷贝对象。
  • toObservable():返回一个 Observable 对象,如果我们订阅这个对象,就会执行 command 并且获取返回结果。

execute() 实际上会调用queue().get() 方法

而在 queue() 方法中,会调用 toObservable().toBlocking().toFuture()。
也就是说,先通过 toObservable() 获得 Future 对象,然后调用 Future 的 get() 方法。那么,其实无论是哪种方式执行 command,最终都是依赖于 toObservable() 去执行的。

步骤三:检查是否开启缓存

从这一步开始,就进入到 Hystrix 底层运行原理啦,看一下 Hystrix 一些更高级的功能和特性。如果这个 command 开启了请求缓存 Request Cache,并且这个调用的结果在缓存中存在,那么直接从缓存中返回结果。否则,继续往后的步骤。

步骤四:检查是否开启了断路器

检查这个 command 对应的依赖服务是否开启了断路器。如果断路器被打开了,那么 Hystrix 就 不会执行这个 command,而是直接去执行 fallback 降级机制,返回降级结果。

步骤五:检查线程池/队列/信号量是否已满

如果这个 command 线程池和队列已满,或者 semaphore 信号量已满,那么也不会执行 command,而是直接去调用 fallback 降级机制,同时发送 reject 信息给断路器统计。

步骤六:执行 command

调用 HystrixObservableCommand 对象的 construct() 方法,或者 HystrixCommand 的 run() 方法来实际执行这个 command。

  • HystrixCommand.run() 返回单条结果,或者抛出异常。
  • HystrixObservableCommand.construct() 返回一个 Observable 对象,可以获取多条结果。

如果是采用线程池方式,并且 HystrixCommand.run() 或者 HystrixObservableCommand.construct() 的执行时间超过了 timeout 时间的话,那么 command 所在的线程会抛出一个 TimeoutException,这时会执行 fallback 降级机制,不会去管 run() 或 construct() 返回的值了。另一种情况,如果 command 执行出错抛出了其它异常,那么也会走 fallback 降级。这两种情况下,Hystrix 都会发送异常事件给断路器统计。

注意,我们是不可能终止掉一个调用严重延迟的依赖服务的线程的,只能说给你抛出来一个TimeoutException。

如果没有 timeout,也正常执行的话,那么调用线程就会拿到一些调用依赖服务获取到的结果,然后 Hystrix 也会做一些 logging 记录和 metric 度量统计。

步骤七:断路健康检查

Hystrix 会把每一个依赖服务的调用成功、失败、Reject、Timeout 等事件发送给 circuit breaker 断路器。断路器就会对这些事件的次数进行统计,根据异常事件发生的比例来决定是否要进行断路(熔断)。如果打开了断路器,那么在接下来一段时间内,会直接断路,返回降级结果。

如果在之后,断路器尝试执行 command,调用没有出错,返回了正常结果,那么 Hystrix 就会把断路器关闭。

步骤八:调用 fallback 降级机制

在以下几种情况中,Hystrix 会调用 fallback 降级机制

  • 断路器处于打开状态;
  • 线程池/队列/semaphore满了;
  • command 执行超时;
  • run() 或者 construct() 抛出异常。

一般在降级机制中,都建议给出一些默认的返回值,比如静态的一些代码逻辑,或者从内存中的缓存中提取一些数据,在这里尽量不要再进行网络请求了。 在降级中,如果一定要进行网络调用的话,也应该将那个调用放在一个HystrixCommand 中进行隔离。

HystrixCommand 中,实现 getFallback() 方法,可以提供降级机制。

HystrixObservableCommand 中,实现 resumeWithFallback() 方法,返回一个 Observable 对象,可以提供降级结果。

如果没有实现 fallback,或者 fallback 抛出了异常,Hystrix 会返回一个 Observable,但是不会返回任何数据。

不同的 command 执行方式,其 fallback 为空或者异常时的返回结果不同。

  • 对于 execute(),直接抛出异常。
  • 对于 queue(),返回一个 Future,调用 get() 时抛出异常。
  • 对于 observe(),返回一个 Observable 对象,但是调用 subscribe() 方法订阅它时,立即抛出调用者的
    onError()方法。
  • 对于 toObservable(),返回一个 Observable 对象,但是调用 subscribe() 方法订阅它时,立即抛出调用者的
    onError() 方法。

不同的执行方式

  • execute(),获取一个 Future.get(),然后拿到单个结果。
  • queue(),返回一个 Future。
  • observe(),立即订阅 Observable,然后启动 8大执行步骤,返回一个拷贝的 Observable, 订阅时立即回调给你结果。
  • toObservable(),返回一个原始的 Observable,必须手动订阅才会去执行 8 大步骤。

基于 request cache 请求缓存技术优化批量商品数据查询接口

Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是否有缓存。

首先,有一个概念,叫做 Request Context 请求上下文,一般来说,在一个 web 应用中,如果我们用到了 Hystrix,我们会在一个 filter 里面,对每一个请求都施加一个请求上下文。就是说,每一次请求,就是一次请求上下文。然后在这次请求上下文中,我们会去执行 N 多代码, 调用 N 多依赖服务,有的依赖服务可能还会调用好几次。

在一次请求上下文中,如果有多个 command,参数都是一样的,调用的接口也是一样的,而结果可以认为也是一样的。那么这个时候,我们可以让第一个 command 执行返回的结果缓存在内存中,然后这个请求上下文后续的其它对这个依赖的调用全部从内存中取出缓存结果就可以了。

这样的话,好处在于不用在一次请求上下文中反复多次执行一样的 command,避免重复执行网络请求,提升整个请求的性能。

HystrixCommand 和 HystrixObservableCommand 都可以指定一个缓存 key,然后 Hystrix 会自动进行缓存,接着在同一个 request context 内,再次访问的话,就会直接取用缓存。

两种最经典的降级机制

  • 纯内存数据

在降级逻辑中,你可以在内存中维护一个 ehcache,作为一个纯内存的基于 LRU 自动清理的缓存,让数据放在缓存内。如果说外部依赖有异常,fallback 这里直接尝试从 ehcache 中 获取数据。

  • 默认值

fallback 降级逻辑中,也可以直接返回一个默认值。

深入Hystrix 断路器执行原理

RequestVolumeThreshold

HystrixCommandProperties.Setter() .withCircuitBreakerRequestVolumeThreshold(int)

表示在滑动窗口中,至少有多少个请求,才可能触发断路。

Hystrix 经过断路器的流量超过了一定的阈值,才有可能触发断路。比如说,要求在 10s 内经过断路器的流量必须达到 20 个,而实际经过断路器的流量才 10 个,那么根本不会去判断要不要断路。

ErrorThresholdPercentage

HystrixCommandProperties.Setter() .withCircuitBreakerErrorThresholdPercentage(int)

表示异常比例达到多少,才会触发断路,默认值是 50(%)

如果断路器统计到的异常调用的占比超过了一定的阈值,比如说在 10s 内,经过断路器的流量达到了 30 个,同时其中异常访问的数量也达到了一定的比例,比如 60% 的请求都是异常(报 错 / 超时 / reject),就会开启断路。

SleepWindowInMilliseconds

HystrixCommandProperties.Setter() .withCircuitBreakerSleepWindowInMilliseconds(int)

断路开启,也就是由 close 转换到 open 状态(close -> open)。那么之后在 SleepWindowInMilliseconds 时间内,所有经过该断路器的请求全部都会被断路,不调用后端服务,直接走 fallback 降级机制。

而在该参数时间过后,断路器会变为 half-open 半开闭状态,尝试让一条请求经过断路器, 看能不能正常调用。如果调用成功了,那么就自动恢复,断路器转为 close 状态。

Enabled

HystrixCommandProperties.Setter() .withCircuitBreakerEnabled(boolean)

控制是否允许断路器工作,包括跟踪依赖服务调用的健康状况,以及对异常情况过多时是否允许触发断路。默认值是 true 。

ForceOpen

HystrixCommandProperties.Setter() .withCircuitBreakerForceOpen(boolean)

如果设置为 true 的话,直接强迫打开断路器,相当于是自动断路了,自动降级,默认值是 false 。

ForceClosed

HystrixCommandProperties.Setter() .withCircuitBreakerForceClosed(boolean)

如果设置为 true,直接强迫关闭断路器,相当于手动停止断路了,手动升级,默认值是 false 。

HystrixCommand 配置参数Demo

在 GetProductInfoCommand 中配置 Setter 断路器相关参数。

  • 滑动窗口中,最少 20 个请求,才可能触发断路。
  • 异常比例达到 40% 时,才触发断路。
  • 断路后 3000ms 内,所有请求都被 reject,直接走 fallback 降级,不会调用 run() 方法。 3000ms过后,变为 half-open 状态。

深入 Hystrix 线程池隔离与接口限流

Hystrix 通过判断线程池或者信号量是否已满,超出容量的请求,直接 Reject 走降级,从而达到限流的作用。 限流是限制对后端的服务的访问量,比如说你对 MySQL、Redis、Zookeeper 以及其它各种后端中间件的资源的访问的限制,其实是为了避免过大的流量直接打死后端的服务。

线程池隔离技术的设计

Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进行资源隔离,进而避免任何外部依赖的故障导致本服务崩溃。

Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。

Hystrix 应用线程池机制的场景

  • 每个服务都会调用几十个后端依赖服务,那些后端依赖服务通常是由很多不同的团队开发 的。
  • 每个后端依赖服务都会提供它自己的 client 调用库,比如说用 thrix 的话,就会提供对应的 thrix 依赖。
  • client 调用库随时会变更
  • client 调用库随时可能会增加新的网络请求的逻辑。
  • client 调用库可能会包含诸如自动重试、数据解析、内存中缓存等逻辑。
  • client 调用库一般都对调用者来说是个黑盒,包括实现细节、网络访问、默认配置等等。
  • 在真实的生产环境中,经常会出现调用者,突然间惊讶的发现,client 调用库发生了某些变化。
  • 即使 client 调用库没有改变,依赖服务本身可能有会发生逻辑上的变化。
  • 有些依赖的 client 调用库可能还会拉取其他的依赖库,而且可能那些依赖库配置的不正确。
  • 大多数网络请求都是同步调用的。
  • 调用失败和延迟,也有可能会发生在 client 调用库本身的代码中,不一定就是发生在网络请求中。

简单来说,就是你必须默认 client 调用库很不靠谱,而且随时可能发生各种变化,所以就要用强制隔离的方式来确保任何服务的故障不会影响当前服务。

线程池机制的优点

  1. 任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用。
  2. 服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用。
  3. 当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是 tomcat 线程池被占满,再恢复就很麻烦。
  4. 如果一个 client 调用库配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机。
  5. 基于线程池的异步本质,可以在同步的调用之上,构建一层异步调用层。

简单来说,最大的好处,就是资源隔离,确保说任何一个依赖服务故障,不会拖垮当前的这个 服务。

线程池机制的缺点

  1. 线程池机制最大的缺点就是增加了 CPU 的开销。 除了 tomcat 本身的调用线程之外,还有 Hystrix 自己管理的线程池。
  2. 每个 command 的执行都依托一个独立的线程,会进行排队,调度,还有上下文切换。

Hystrix 官方自己做了一个多线程异步带来的额外开销统计,通过对比多线程异步调用+同步调用得出,Netflix API 每天通过 Hystrix 执行 10 亿次调用,每个服务实例有 40 个以上的线程池,每个线程池有 10 个左右的线程。)最后发现说,用 Hystrix 的额外开销,就是给请求带来了 3ms 左右的延时,最多延时在 10ms 以内,相对于可用性和稳定性的提升,这是可以接受的。

我们可以用Hystrix semaphore 技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量。 semaphore 技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行 timeout 和隔离。

execution.isolation.strategy 设置为 SEMAPHORE ,那么 Hystrix 就会用 semaphore 机制来替代线程池机制,来对依赖服务的访问进行限流。如果通过 semaphore 调用的时候,底层的网络调用延迟很严重,那么是无法 timeout 的,只能一直 block 住。一旦请求数量超过了 semaphore 限定的数量之后,就会立即开启限流。

接口限流 Demo

假设一个线程池大小为 8,等待队列的大小为 10。timeout 时长我们设置大一些,20s。 在 command 内部,写死代码,做一个 sleep,比如 sleep 3s。

  • withCoreSize:设置线程池大小。
  • withMaxQueueSize:设置等待队列大小。
  • withQueueSizeRejectionThreshold:这个与 withMaxQueueSize 配合使用,等待队列的大小,
    取得是这两个参数的较小值。

如果只设置了线程池大小,另外两个 queue 相关参数没有设置的话,等待队列是处于关闭的状态。

基于 timeout 机制为服务接口调用超时提供安全保护

  • TimeoutMilliseconds

在 Hystrix 中,我们可以手动设置 timeout 时长,如果一个 command 运行时间超过了设定的时长,那么就被认为是 timeout,然后 Hystrix command 标识为 timeout,同时执行 fallback 降级逻辑。

TimeoutMilliseconds 默认值是 1000,也就是 1000ms。

  • TimeoutEnabled

这个参数用于控制是否要打开 timeout 机制,默认值是 true

转载地址:http://zhtwb.baihongyu.com/

你可能感兴趣的文章
Linux 查看文件大小
查看>>
Java并发编程:线程池的使用
查看>>
redis单机及其集群的搭建
查看>>
Java多线程学习
查看>>
检查Linux服务器性能
查看>>
Java 8新的时间日期库
查看>>
Chrome开发者工具
查看>>
【LEETCODE】102-Binary Tree Level Order Traversal
查看>>
【LEETCODE】106-Construct Binary Tree from Inorder and Postorder Traversal
查看>>
【LEETCODE】202-Happy Number
查看>>
和机器学习和计算机视觉相关的数学
查看>>
十个值得一试的开源深度学习框架
查看>>
【LEETCODE】240-Search a 2D Matrix II
查看>>
【LEETCODE】53-Maximum Subarray
查看>>
【LEETCODE】215-Kth Largest Element in an Array
查看>>
【LEETCODE】241-Different Ways to Add Parentheses
查看>>
【LEETCODE】312-Burst Balloons
查看>>
【LEETCODE】232-Implement Queue using Stacks
查看>>
【LEETCODE】225-Implement Stack using Queues
查看>>
【LEETCODE】155-Min Stack
查看>>