Skip to content

Spring Cloud教程 - 服务容错保护-Hystrix #14

@TFdream

Description

@TFdream

服务雪崩

微服务架构下,会存在服务之间相互依赖调用的情况,当某个服务不可用时,很容易因为服务之间的依赖关系使故障扩大,甚至造成整个系统不可用的情况,这种现象称为服务雪崩效应。

image

如上图所示,为服务雪崩效应发生的过程,首先是服务正常状态,当客户端对服务 A 发起请求,服务 A 依赖了服务 B,服务 B 又依赖了服务 C,当所有服务都处于正常状态时,整个请求链路是通畅的,结果会很快返回给客户端。

如果这时服务 C 发生故障或出现性能问题,就会出现延迟,刚开始时延迟较小,随着时间的推移,延迟会越来越大,服务 B 对服务 C 的调用就会堵塞,服务 C 此时已经疲惫不堪。

由于请求都堵在服务 C 上,服务 B 作为调用方,却迟迟等不到服务 C 的结果,服务 A 对服务 B 的请求又源源不断的发送过来,最终导致服务 B 的资源耗尽,从正常状态变成不正常状态,再也无法及时响应服务 A 的请求结果。

依此类推,最终服务 A 也会被拖垮,导致整个系统不可用,这个过程就是服务雪崩效应。如果能从最开始的小问题进行预防,就不会出现后面的级联效果,本课时的主要内容就是讲解如何通过服务的容错降级来保证系统的可用性。

产生原因

我们从两个方面来分析服务雪崩产生的原因:

  • 服务提供者
  • 服务消费者

起因肯定是服务提供者出了问题才导致后面的雪崩问题,在实际应用中无法预料服务提供者可能会出现什么样的问题,我们只能分析一些比较常见的问题。

比如说代码的 Bug 问题,由于某些代码问题导致 CPU 飙升,将资源耗尽等,比如服务器出现问题,磁盘出问题,导致数据读写特别慢,一下就拉高了响应时间。比如说某个新来的同事对业务不太熟悉,写了个查询的 SQL 语句,join 了多个表,并且没用到索引,出现慢 SQL。又比如请求量太大了,已经超出了系统本身的承受能力。

从服务消费者这方面来分析的话,主要表现在同步调用等待结果导致资源耗尽,还有就是自己即是服务消费者,同时也是服务提供者。

解决方案

既然分析了一些比较常见的会导致服务雪崩的问题,那么就需要出对应的策略来解决这些问题。正所谓兵来将挡水来土掩,有问题一定要解决。

服务提供者方面,对于这种请求量超出承受能力的问题,我们可以进行扩容来支持高并发或者进行限流,自己能处理多少请求就处理多少,处理不了的请求直接拒绝,这样才不会将自己拖垮。

对于代码的 Bug 问题,我们可以通过测试、Code Review 等方式来避免,对于慢 SQL 这种问题,我们需要去做数据库性能优化。对于服务器硬件故障问题,我们可以加大运维粒度,通过监控等手段来提前预防。

服务消费者方面,我们需要做的就是资源隔离,快速失败,这也是最有效的方式,当我们发现被调用方迟迟不响应出现问题的时候,就不要再继续发起调用请求了,此时应该停止,等待被调用方恢复后再发起调用,你可能会说,那我是不是在每个调用的地方都要做一些逻辑处理并判断要不要发起调用,如果你有这种想法就太小看 Spring Cloud 的生态圈了,我们只需要使用 Hystrix 就能轻松搞定这种场景。

Hystrix

Hystrix 是由 Netflix 发布的针对微服务分布式系统的熔断保护中间件,相当于电路中的保险丝,它的关注度也非常高,在 GitHub 上已经有超过 18000 颗星,也经过了 Netflix 线上大规模流量的验证,性能非常稳定。

在微服务架构下,很多服务都相互依赖,如果不能对依赖的服务进行隔离,那么服务本身也有可能发生故障,Hystrix 通过 HystrixCommand 对调用进行隔离,这样可以阻止故障的连锁反应,能够快速失败并迅速恢复服务或者进行回退并优雅降级。

Spring Cloud 将原生 Hystrix 整合进来,提供了最简洁的使用方式,并且跟 Feign、Zuul 等组件做了集成,极大的降低了使用的难度。

设计原则

image

这里总结了 Hystrix 的 5 条设计原则,首先我们看看第一条设计原则避免线程耗尽,由于被调用方出现问题,调用方无法及时获取响应结果,而一直在发送请求,最终会耗尽所有线程的资源。

快速失败指的是当被调用方出现问题后,调用方发起的请求可以快速失败并返回,这样就不用一直阻塞住,同时也释放了线程资源。

支持回退指的是在失败后,我们可以让用户有回退的逻辑,比如获取备用数据,从缓存中获取数据,记录日志等操作。

资源隔离是设计原则里最重要的,当你的服务依赖了 A、B、C 三个服务,当只有 C 服务出问题的时候,如果没做隔离,最终也会发生雪崩效应,导致整个服务不可用,如果我们进行了资源隔离,A、B、C 三个服务都是相互隔离的,即使 C 服务出问题了,那也不影响 A 和 B。这其实就跟不要把所有的鸡蛋放进一个篮子里是一样的道理。

近实时监控也非常重要,它能帮助我们了解整个系统目前的状态,有哪些服务有问题,当前流量有多大,出问题后及时告警等。

容错实现 

前面我们讲了 Hystrix 的一些设计原则,这些原则实际就是为了解决遇到的问题。那么要解决这些问题,就必须去实现这些设计,你现在看到的脑图是我简单整理的一些实现点。

image

封装请求会将用户的操作进行统一封装,统一封装的目的在于进行统一控制。

资源隔离会将对应的资源按照指定的类型进行隔离,比如线程池和信号量,详细的隔离机制会在后面进行讲解。

失败回退其实是一个备用的方案,就是说当请求失败后,有没有备用方案来满足这个请求的需求。Hystrix 中会让用户去自定义备用方案。

断路器这个是最核心的,断路器决定了请求是否需要真正的执行,如果断路器处于打开的状态,那么所有请求都将失败,执行回退逻辑。如果断路器处于关闭状态,那么请求将会被正常执行,断路器的原理后面会进行讲解。

指标监控会对请求的生命周期进行监控,请求是成功了,还是失败了,是超时了,还是被拒绝了,都会被监控起来。

工作原理

image

这张图是 Hystrix 在 GitHub 主页上提供的,详细的展示了 Hystrix 的工作原理。通过此图可以看出,整个工作流程分为 9 个主要步骤。

Hystrix 使用

Hystrix 的使用主要有三种方式,分别是

  • HystrixCommand 注解方式;
  • 在 Feign 中使用;
  • 以及在 Zuul 中使用。

本课时主要讲解 HystrixCommand 注解方式的使用,Feign 和 Zuul 的集成使用会在后面对应的课时中进行讲解。

首先我们需要在 pom 中增加 spring-cloud-starter-netflix-hystrix 的依赖,然后在启动类上增加 @EnableHystrix 注解,HystrixCommand 注解作用于方法上,哪个方法想要使用 Hystrix 来进行保护,就在这个方法上增加 HystrixCommand 注解。

HystrixCommand 都有默认的配置,我们可以手动指定配置信息,比如 commandKey、groupKey、fallbackMethod 等。

最简便的配置方式还是统一将配置信息放入配置文件中进行管理,我们可以为 HystrixCommand 设置一个 commandKey,然后在配置文件中为这个 commandKey 指定对应的配置信息。比如我们配置 getUser 这个 commandKey 的超时时间和隔离方式,在配置文件中增加

hystrix.command.getUser.execution.isolation.thread.timeoutInMilliseconds = 3000
hystrix.command.getUser.execution.isolation.strategy = SEMAPHORE

这 2 行代码即可。

Hystrix 配置

image

Hystrix 的配置项比较多,在这里整理了几个比较常用的配置,所有的配置基本上都有默认值,如果我们需要调整这些配置,可以参考 PPT 中给出的链接。

隔离策略,我们需要根据不同的场景来选择合适的策略,可选择的策略有线程和信号量。命令执行的超时时间,这个只对线程隔离有效,信号量隔离不支持超时,当命令执行的时间超过了我们设定的时间,那么就会超时。

信号量隔离需要给信号量设定一个值,这个值表示最大的并发请求数量,超出这个数量就会拒绝请求,很多人在做压力测试的时候没有去调整该配置,压测时发现性能上不去,这就是没仔细看文档的后果。

断路器开关的控制这个也挺常用的,在某些场景下,我们可能需要手动进行降级,这个时候就可以将断路器强制打开,这样就会拒绝所有请求。

如果使用线程隔离方式,需要调整好线程池的参数,否则跟信号量一样,并发量大的时候性能上不去。设置最大的线程数,默认为 10,然后就是队列的大小,这决定了能够堆积多少个请求。但请求也不能一直堆积,所以我们还需要设置一个阈值来进行拒绝。

@HystrixCommand原理

使用 HystrixCommand 注解确实比较方便,这样我们就不用自己去将请求包装成 Command 执行,将这个包装的过程交给框架内部处理,这也是框架存在的价值。

HystrixCommand 注解的原理其实很简单,在 Hystrix 中有一个 HystrixCommandAspect 专门负责将加了 HystrixCommand 的方法在执行时进行包装。我们打开 HystrixCommandAspect 的源码,可以看到加入了 HystrixCommand 和 HystrixCollapser 两个注解,内部就是获取 Method、判断、包装,最后执行。

Hystrix 隔离方式            

image

Hystrix 支持线程池和信号量两种隔离方式,线程池隔离是当用户请求到 A 服务后,A 服务需要调用其他服务,这个时候可以为不同的服务创建独立的线程池,假如 A 需要调用 B 和 C,那么可以创建 2 个独立的线程池,将调用 B 服务的线程丢入到一个线程池,将调用 C 服务的线程丢入到另一个线程池,这样就起到隔离效果,就算其中某个线程池请求满了,无法处理请求了,对另一个线程池也没有影响。

信号量隔离就比较简单了,信号量就是一个计数器,比如初始化值是 100,那么每次请求过来的时候就会减 1,当信号量计数为 0 的时候,请求就会被拒绝,等之前的请求处理完成后,信号量会加 1,同时也起到了限流的作用,这就是信号量隔离,信号量隔离是在请求主线程中执行的。

线程池隔离的特点是 Command 运行在独立的线程池中,可以支持超时,是单独的线程,支持异步。信号量隔离运行在调用的主线程中,不支持超时,只能同步调用。

Hystrix 使用小经验

Hystrix 的使用还是比较简单的,很多工作框架都已经帮我们实现好了,但在实际使用中,我们还是会遇到很多问题,我总结了一些 Hystrix 使用中的小经验,在这边分享给你,这些对你在面试中也很有帮助,如果你只说用了 HystrixCommand 注解,却说不出其他经验,面试效果可想而知,如果你能说出最基本的使用,还能说自己在工作中遇到了一些什么样的问题,然后怎么去解决的,总结了以下几点经验,详细的讲给面试官听,强势扭转面试局势。

配置可以对接配置中心进行动态调整。

Hystrix 的配置项非常多,在前面我建议大家只设置一个 CommandKey,其他的都在配置中进行指定,不要在注解中写上所有的配置,这种方式可行,但是不是最好的,因为它只能在代码中修改。像很多配置,我们都会根据当时的流量情况来进行调整,如果不对接配置中心,这个工作太难了。其实 Hystrix 内部默认使用 Archaius 来实现的动态配置, Archaius 是 Netflix 的配置框架,也可以直接用 Archaius 来动态管理 Hystrix 的配置信息。

回退逻辑中可以手动埋点或者通过输出日志进行告警。

当请求失败或者超时,会执行回退逻辑,如果有大量的回退,则证明某些服务出问题了,这个时候我们可以在回退的逻辑中进行埋点操作,上报数据给监控系统,也可以输出回退的日志,统一由日志收集的程序去进行处理,这些方式都可以将问题暴露出去,然后通过实时数据分析进行告警操作,当然这只是一个入口,对 Hystrix 进行监控的方式有很多种,我们可以扩展 Hystrix 的插件进行数据收集,也可以分析 Hystrix.stream 端点的数据来进行告警。

用了线程池隔离模式再用 ThreadLocal 会有坑。

一个请求进来,这时是容器的线程在负责执行,对于同一个线程传递上下文 ThreadLocal 是没有问题的,当我们用了线程池隔离模式的时候,被隔离的方法会包装成一个 Command 丢入到独立的线程池中进行执行,这个时候就是从 A 线程切换到了 B 线程,ThreadLocal 的数据就会丢失,这个问题的解决方案也有多种,可以参考 PPT 中我给的博客链接,这是我之前写的解决方案。

 

网关中尽量用信号量隔离。

之所以建议在网关中用信号量隔离,是因为网关是所有请求的入口,路由的服务数量会很多,几十个到上百个都有可能,如果用线程池隔离,那么需要创建上百个独立的线程池,开销太大了。用信号量隔离开销就小很多,还能起到限流的作用。

插件机制可以实现很多扩展。

Hystrix 提供了插件机制,可以通过插件来改变 Hystrix 的行为,比如我们可以使用事件通知的插件来做一些数据收集和告警的工作,可以使用配置插件来改变配置的默认行为,目前默认是 Archaius,我们可以将其改变成其他的配置组件。可以使用并发插件来改变线程池的行为,可以对 Callable 进行装饰,来解决 ThreadLocal 跨线程传递的问题。

 

Hystrix 各种超时配置方式。

Hystrix 中用的最多的配置可能就是超时时间,可以配置全局的默认超时时间,那么在 HystrixCommand、Feign 以及 Zuul  中超时时间怎么配置呢?最核心的点在于你需要知道Hystrix CommandKey 是怎么生成的,知道了 CommandKey,就可以对 CommandKey 进行超时的配置,由于时间关系,就不具体展开讲解了,我之前写了一篇详细的文章,大家可以参考下,链接在 PPT 中。

 
commandKey、groupKey、threadPoolKey 的使用。

在使用 HystrixCommand 注解的时候,我们会配置 commandKey、groupKey、threadPoolKey,当然这些也可以不用配置,因为有默认值。commandKey 表示这个请求被封装成了 Command 去执行,commandKey 就是这个 command 的名称,我们可以给指定的 commandKey 进行参数的配置。比如 commandKey1 的超时时间我们设置成 3 秒,commandKey2 的超时时间我们可以设置成 10 秒。

groupKey 是将一组 command 进行分组,groupKey 就是组的名称,同时如果没有设置 threadPoolKey 的话,那么线程池的名称会用 groupKey。

threadPoolKey 是线程池的名称,多个 command的threadPoolKey 相同,那么会使用同一个线程池。建议大家手动配置一个简短的、友好的 threadPoolKey,同时使用 threadPoolKey 来对 command 进行线程池隔离的划分。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions