暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Hystrix入门与实践

糖爸的架构师之路 2021-06-24
426

写在前面

分布式系统环境下,服务间依赖非常常见。单个应用通常会有多个不同类型的外部依赖服务,内部通常依赖于各种RPC服务,外部则依赖于各种HTTP服务。这些依赖服务不可避免的会出现调用失败,比如超时、异常等情况。因此,为了构建稳定、可靠的分布式系统,我们的服务应当具有自我保护能力,当依赖服务不可用时,当前服务启动自我保护功能,如何在外部依赖出问题的情况下仍然保证自身应用的稳定,避免出现服务资源耗尽的情况呢?可以阅读一下本文


服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,我们把这种微服务之间的调用叫做服务扇出。如果扇出的链路上某个微服务的调用响应时间过长或者不可用时,对微服务A的调用就会占用越来越多的系统资源,并且这种不可用可能沿请求调用链向上传递,这种现象我们称之为雪崩效应

雪崩效应常见场景
  • 硬件故障:如服务器宕机,机房断电,光纤被挖断等。

  • 流量激增:如异常流量,重试加大流量等。

  • 程序BUG:如程序逻辑导致内存泄漏,JVM长时间FullGC等。

  • 同步等待:服务间采用同步调用模式,同步等待造成的资源耗尽。

  • 缓存穿透:一般发生在应用重启,所有缓存失效或短时间内大量缓存失效时导致的服务提供者超负荷运行,引起服务不可用


雪崩效应应对策略

一般情况对于服务依赖的保护主要有三种解决方案:

  • 熔断模式:这种模式主要是参考电路熔断。如果一条线路电压过高,保险丝会熔断,防止火灾。类比到我们的系统中,如果某个目标服务调用慢或者有大量超时便熔断该服务的调用。对于后续调用请求,不在继续调用目标服务而是直接返回,快速释放资源。如果目标服务情况好转则恢复调用。

  • 隔离模式:对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响。如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回。不再调用后续资源。

  • 限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。


Hystrix入门

Hystrix [hɪst'rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix是Netflix开源的一款容错框架,同样具有自我保护能力。为了实现容错和自我保护,下面我们看看Hystrix如何设计和实现的。


Hystrix设计目标:

  • 对来自依赖的延迟和故障进行防护和控制

  • 阻止故障的连锁反应

  • 快速失败并迅速恢复

  • 回退并优雅降级

  • 提供接近实时的监控与告警


Hystrix遵循的设计原则:

  • 防止任何单独的依赖耗尽资源(线程)

  • 过载立即切断并快速失败,防止排队

  • 尽可能提供回退以保护用户免受故障

  • 使用隔离技术(例如隔板,泳道和断路器模式)来限制任何一个依赖的影响

  • 通过近实时的指标,监控和告警,确保故障被及时发现

  • 通过动态修改配置属性,确保故障及时恢复

  • 防止整个依赖客户端执行失败,而不仅仅是网络通信


Hystrix如何实现这些设计目标?

  • 使用命令模式将所有对外部服务(或依赖关系)的调用包装在

    HystrixCommand或HystrixObservableCommand对象中,并将该对象放在单独的线程中执行

  • 每个依赖都维护着一个线程池(或信号量),线程池被耗尽则拒绝请求(而不是让请求排队)

  • 记录请求成功、失败、超时和线程拒绝。

  • 服务错误百分比超过了阈值,熔断器开关自动打开,一段时间内停止对该服务的所有请求。

  • 请求失败、被拒绝、超时或熔断时执行降级逻辑。

  • 近实时地监控指标和配置的修改。



Hystrix容错

Hystrix的容错主要是通过添加容许延迟和容错方法,帮助控制这些分布式服务之间的交互。 还通过隔离服务之间的访问点,阻止它们之间的级联故障以及提供回退选项来实现这一点,从而提高系统的整体弹性。Hystrix主要提供了以下几种容错方法:
  • 资源隔离

  • 熔断

  • 降级


资源隔离
资源隔离主要指对线程的隔离。Hystrix提供了两种线程隔离方式:线程池和信号量。
线程隔离-线程池
Hystrix通过命令模式对发送请求的对象和执行请求的对象进行解耦,将不同类型的业务请求封装为对应的命令请求。当第一次创建Command时,根据配置创建一个线程池,后续再次调用时,将会重用已创建的线程池。通过将发送请求线程与执行请求的线程分离,可有效防止发生级联故障。当线程池或请求队列饱和时,Hystrix将拒绝服务,使得请求线程可以快速失败,从而避免依赖问题扩散。

线程隔离优缺点
优点:
  • 保护应用程序以免受来自依赖故障的影响,指定依赖线程池饱和不会影响应用程序的其余部分。

  • 当依赖从故障恢复正常时,应用程序会立即恢复正常的性能。

  • 当应用程序一些配置参数错误时,线程池的运行状况会很快检测到这一点(通过增加错误、延迟、超时、拒绝等),同时可以通过动态属性进行实时纠正错误的参数配置。

  • 如果服务的性能有变化,需要实时调整,比如增加或者减少超时时间,更改重试次数,可以通过线程池指标动态属性修改,而且不会影响到其他调用请求。

注意:尽管线程池提供了线程隔离,我们的客户端底层代码也必须要有超时设置或响应线程中断,不能无限制的阻塞以致线程池一直饱和。


缺点:

线程池的主要缺点是增加了计算开销。每个命令的执行都在单独的线程完成,增加了排队、调度和上下文切换的开销。因此,要使用Hystrix,就必须接受它带来的开销,以换取它所提供的好处。
通常情况下,线程池引入的开销足够小,会有重大的成本或性能影响。但对于一些访问延迟极低的服务,如只依赖内存缓存,线程池引入的开销就比较明显了,这时候使用线程池隔离技术就不适合了,我们需要考虑更轻量级的方式,如信号量隔离。

线程隔离-信号量

使用线程池时,发送请求的线程和执行依赖服务的线程不是同一个,而使用信号量时,发送请求的线程和执行依赖服务的线程是同一个,都是发起请求的线程。

客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。

信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。

线程隔离总结
线程池和信号量都可以做线程隔离,但各有各的优缺点和支持的场景,对比如下:



线程切换
支持异步
支持超时
支持熔断
限流
开销
信号量
NNNYY
线程池
Y
YYYY
线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix会通过响应中断的方式通知线程立即结束并返回。


服务降级

通常指高峰期为了保证核心服务正常运行,需要停掉一些不太重要的业务,或者某些服务不可用时,执行备用逻辑从故障服务中快速失败或快速返回,以保障主体业务不受影响。Hystrix提供的降级主要是为了容错,保证当前服务不受依赖服务故障的影响,从而提高服务的健壮性。

服务降级触发条件:
  • 程序运行异常

  • 调用执行超时

  • 服务熔断触发服务降级

  • 线程池/信号量打满也会导致服务降级


示例
@EnableHystrix/@HystrixCommand/@HystrixProperty
引入pom
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    开启注解驱动

    配置降级方法


    服务熔断

    熔断机制是应对雪崩的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。


    熔断类型
    • 熔断打开:请求不在进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设休眠时长则进入熔断状态

    • 熔断关闭:不会对服务进行熔断

    • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且服务符合规则则任务当前服务恢复正常,关闭熔断

    Hystrix在运行过程中会向每个commandKey对应的熔断器报告成功、失败、超时和拒绝的状态,熔断器维护并统计这些数据,并根据这些统计信息来决策熔断开关是否打开。如果打开,熔断后续请求,快速返回。隔一段时间(默认是5s)之后熔断器尝试半开,放入一部分流量请求进来,相当于对依赖服务进行一次健康检查,如果请求成功,熔断器关闭。


    三个重要参数

    • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒

    • 请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断,默认20次。意味着10秒内,如果Hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开

    • 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了异常,也就是超过了50%,在默认设定50%的阈值下断路器会打开


    示例
    构建服务熔断配置与服务降级配置类似,同样需要以下三个注解,只不过需要多配置一些额外的属性
      @EnableHystrix/@HystrixCommand/@HystrixProperty


      Hystrix最佳实践

      刚才我们提到的@HystrixCommand、@HystrixProperties注解都是针对方法级别的,也就是说,针对每一个微服务接口我们都要提供对应的服务熔断策略和服务降级方法,这样不但代码非常的冗余,而且也会是Controller文件变得非常的庞大。针对这种情况,SpringCloud OpenFeign给我们提供了很好的支持。

      OpenFeign是一个声明式WebService客户端。使用OpenFeign能让编写WebService客户端更加简单,它的使用方法是定义一个接口,然后在上面添加注解即可.
      OpenFeign集成了Ribbon,并利用Ribbon维护了服务列表信息,并且通过轮询实现了客户端的负载均衡能力。在SpringCloud官方文档中提到,OpenFeign针对Hystrix也提供了很好的支持,文档地址
      https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/
      具体内容在1.4和1.5两个章节中,感兴趣的小伙伴可以去看一下,我这里只提一下代码实现:

      fallbackFactory
      • 针对每一个微服务接口提供对应的fallbackFactory实现,当某一个微服务发生异常触发降级时,会找到对应接口的实现方法进行降级操作

      • 实现类要实现FallbackFactory接口,并指定泛型为微服务接口类型,并返回此类型

      说明一下,有好多小伙伴是直接在@FeginClient中指定fallback而非fallbackFactory,这里指定fallbackFactory是因为可以在参数中获取到触发服务降级的异常信息,方便我们针对不同的异常进行不同的处理。

      配置文件application.properties
        #开启feign对hystrix的支持
        feign.hystrix.enabled=true
        ############# 降级&线程隔离相关配置 #############
        ## 是否给方法执行设置超时时间,默认为true
        hystrix.command.default.execution.timeout.enabled=true
        ## 配置请求隔离的方式,默认线程池方式。
        hystrix.command.default.execution.isolation.strategy=threadPool
        ## 配置超时触发降级时间,默认为1s
        hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
        ## 发生超时时是否中断方法的执行,默认值为true
        hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
        ## 是否在方法执行被取消时中断方法,默认值为false
        hystrix.command.default.execution.isolation.thread.interruptOnCancel=false


        ############# 熔断相关配置 #############
        ## 是否启动熔断器,默认为true
        hystrix.command.default.circuitBreaker.enabled=true
        ## 启用熔断器功能窗口时间内的最小请求数,默认20
        hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
        ## 熔断闭合后再次进入半开状态的休眠时间
        hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
        ## 在滚动时间窗口内,在请求数量超过circuitBreaker.requestVoLumeThreshoLd的情况下,如果错误请求数的百分比超过此值,就把断路器设置为"闭合”状态,否则就设置为"关闭"状态.
        hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
        ## 是否强制启用熔断器,默认false
        hystrix.command.default.circuitBreaker.forceOpen=false
        ## 是否强制关闭熔断器,默认false
        hystrix.command.default.circuitBreaker.forceClosed=false


        ############# 统计器相关设置 #############
        ## 设置滚动时间窗口时长,默认设置为10000毫秒
        hystrix.command.default.metrics.rollingStats.timeInMilliseconds=10000

        当然Hystrix还有很多其他的配置,具体可以查看官方文档。通过这样的配置,我们就可以实现配置文件+针对接口的实现来全局配置Hystrix。


        关于超时时间配置
        在SpringCloud中,可配置超时时间的组件涉及到Feign、Ribbon、Hystrix,那么它们之间的超时配置有什么关系呢?在Spring Cloud中使用Feign进行微服务调用分为两层:Ribbon的调用及Hystrix的调用。所以Feign的超时时间就是Ribbon和Hystrix超时时间的结合
        • 如果不启用Hystrix则Ribbon的超时时间就是Feign的超时时间,Feign自身的配置会被覆盖。


        • 如果开启了Hystrix,那么Ribbon的超时时间与Hystrix的超时时间则存在依赖关系,因为涉及到Ribbon的重试机制,所以一般情况下都是Ribbon的超时时间小于Hystrix的超时时间,否则会出现以下错误:

          2019-07-12 11:10:20,238 397194 [http-nio-8084-exec-2] WARN  o.s.c.n.z.f.r.s.AbstractRibbonCommand - The Hystrix timeout of 40000ms for the command operation is set lower than the combination of the Ribbon read and connect timeout, 80000ms.
          那么Ribbon和Hystrix的超时时间配置的关系具体是什么呢?如下:

          Hystrix的超时时间 = 
          Ribbon的重试次数(包含首次) * (ribbon,ReadTimeout + ribbon.ConnecTimeout)
          而Ribbon的重试次数的计算方式为:

          Ribbon重试次数(包含首次) = 

          1 + ribbon.MaxAutoRetries + 

          ribbon.MaxAutoRetriesNextServer + 

          (ribbon.MaxAutoRetries 

          ribbon.MaxAutoRetriesNextServer)

          所以,在Ribbon超时但Hystrix没有超时的情况下,Ribbon便会采取重试机制;而重试期间如果时间超过了Hystrix的超时配置则会立即被熔断.


          Hystrix执行流程
          关于Hystrix的执行流程小伙伴们感兴趣的可以作为了解内容,方便自己更深入的理解Hystrix的工作原理,以下来自图文来自官网


          1. 创建HystrixCommand(用在依赖的服务返回单个操作结果的时候)或

            HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候)对象.

          2. 命令执行。其中HystrixComand实现下面前两种执行方式:

            而HystrixObservableCommand实现了后两种方式

            1. execute():同步执行。从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。

            2. queue():异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。

            3. observe():返回Observable对象,它代表了操作的多个结果,它是一个Hot Obserable(不论"事件源"是否有"订阅者",都会在创建后对事件进行发布,所以对于Hot Observable的每一个"订阅者"都有可能是从"事件源"的中途开始的,并可能只是看到了整个操作的局部过程).

            4. toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有"订阅者"的时候并不会发布事件,而是进行等待,直到有"订阅者"之后才发布事件,所以对于Cold Observable的订阅者,它可以保证从一开始看到整个操作的全部过程

          3. 若当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。

          4. 检查断路器是否为打开状态

            1. 如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步)

            2. 如果断路器是关闭的,检查是否有可用资源来执行命令(第5步)

          5. 线程池、请求队列、信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理逻辑(第8步).

          6. Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务

            1. HystrixCommand.run():返回一个单一结果,或者抛出异常

            2. HystrixObservableCommand.construct(): 返回一个Observable对象来发射多个结果或通过onError发送错误通知

          7. Hystrix会将成功、失败、拒绝、超时等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开来对某个依赖服务的请求进行熔断。

          8. 当命令执行失败的时候,Hystrix会进入fallback尝试回退处理(服务降级),而能够引起服务降级处理的情况有下面几步

            1. 第4步:当前命令处于熔断状态,断路器是打开的时候

            2. 第5步:当前命令的线程池、请求队列、信号量被占满的时候

            3. 第6步:

              HystrixObservableCommand.construct()或

              HystrixCommand.run()抛出异常的时候

          9. 当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回


          以上就是今天Hystrix的内容,谢谢阅读

          文章转载自糖爸的架构师之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论