写点什么

SpringCloud 微服务架构实战:Feign+Hystrix 实现 RPC 调用保护

用户头像
小Q
关注
发布于: 2021 年 05 月 17 日

公众号:Java 架构师联盟,每日更新技术好文

Feign+Hystrix 实现 RPC 调用保护

在 Spring Cloud 微服务架构下,RPC 保护可以通过 Hystrix 开源组件来实现,并且 Spring Cloud 对 Hystrix 组件进行了集成,使用起来非常方便。


Hystrix 翻译过来是豪猪,由于豪猪身上长满了刺,因此能保护自己不受天敌的伤害,代表了一种防御机制。Hystrix 开源框架是 Netflix 开源的一个延迟和容错的组件,主要用于在远程 Provider 服务异常时对消费端的 RPC 进行保护。有关 Hystrix 的详细资料,可参考其官方网站(https://github.com/Netflix/Hystrix),本文只对它的基本原理和使用进行介绍。


使用 Hystrix 之前需要在 Maven 的 pom 文件中增加以下 Spring CloudHystrix 集成模块的依赖:


 <!--引入Spring Cloud Hystrix依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
复制代码


在 Spring Cloud 架构中,Hystrix 是和 Feign 组合起来使用的,所以需要在应用的属性配置文件中开启 Feign 对 Hystrix 的支持:


feign: hystrix: enabled: true #开启Hystrix对Feign的支持
复制代码


在启动类上添加 @EnableHystrix 或者 @EnableCircuitBreaker。注意,@EnableHystrix 中包含了 @EnableCircuitBreaker。作为示例,下面是 Demo-provider 启动类的部分代码:


package com.crazymaker.springcloud.demo.start;.../** *在启动类上启用Hystrix */@EnableHystrixpublic class DemoCloudApplication{ public static void main(String[] args) { SpringApplication.run(DemoCloudApplication.class, args); ... }}
复制代码


Spring Cloud Hystrix 的 RPC 保护功能包括失败回退、熔断、重试、舱壁隔离等,接下来学习一下 Hystrix 的失败回退和熔断两大功能。

Spring Cloud Hystrix 失败回退

什么是失败回退呢?当目标 Provider 实例发生故障时,RPC 的失败回退会产生作用,返回一个后备的结果。一个失败回退的演示如图 2-16 所示,有 A、B、C、D 四个 Provider 实例,A-Provider 和 B-Provider 对 D-Provider 发起 RPC 远程调用,但是 D-Provider 发生了故障,在 A、B 收到失败回退保护的情况下,最终会拿到失败回退提供的后备结果(或者 Fallback 回退结果)。



图 2-16 RPC 远程调用失败回退示意图


如何设置 RPC 调用的回退逻辑呢?有两种方式:


(1)定义和使用一个 Fallback 回退处理类。


(2)定义和使用一个 FallbackFactory 回退处理工厂类。首先来看第一种方式:定义和使用一个 Fallback 回退处理类。


第一种方式具体的实现可以分为两步:第一步是实现 Feign 客户端远程调用接口,编写一个 Fallback 回退处理类,并将 RPC 失败后的回退逻辑编写在回退处理类对应的实现方法中;第二步是在 Feign 客户端接口的关键性注解 @FeignClient 上配置失败处理类,具体来说,将该注解的 Fallback 属性的值配置为上一步定义的 Fallback 回退处理类。


下面介绍具体的实例,演示如何定义和使用一个 Fallback 回退处理类。在 crazy-springcloud 脚手架的 uaa-client 模块中,有一个用于对 uaa-provider 进行 RPC 调用的 Feign 客户端远程调用接口 UserClient,其目的是获取用户信息。


第一步为 UserClient 接口定义一个简单的 Fallback 回退处理实现类,代码如下:


package com.crazymaker.springcloud.user.info.remote.fallback;//省略import/** *Feign客户端接口的Fallback回退处理类 */@Componentpublic class UserClientFallback implements UserClient{ /** *获取用户信息RPC失败后的回退方法 */ @Override public RestOut<UserDTO> detail(Long id) { return RestOut.error("failBack:user detail rest服务调用失败" ); }}
复制代码


第二步是在 UserClient 客户端接口的 @FeignClient 注解中,将 Fallback 属性的值配置为上一步定义的 Fallback 回退处理类 UserClientFallback,代码如下:


package com.crazymaker.springcloud.user.info.remote.client;//省略import/** *Feign客户端接口 *@description:获取用户信息的RPC接口类*/@FeignClient(value = "uaa-provider", configuration = FeignConfiguration.class, fallback = UserClientFallback.class, #配置回退处理类 path = "/uaa-provider/api/user")public interface UserClient{ @RequestMapping(value = "/detail/v1", method = RequestMethod.GET) RestOut<UserDTO> detail(@RequestParam(value = "userId") Long userId);}
复制代码


回退处理类的实现已经完成,如何进行验证呢?仍然使用前面定义的 demoprovider 的 REST 接口/api/call/uaa/user/detail/v2,该接口通过 UserClient 对 uaa-provider 进行远程调用。具体的演示方式为:


停掉所有 uaa-provider 服务,然后在 demo-provider 的 swagger-ui 界面访问其 REST 接口/api/call/uaa/user/detail/v2,该接口的内部代码会通过 UserClient 远程调用 Feign 接口对目标 uaa-provider 的 REST 接口/api/user/detail/v1 发起 FeignRPC 远程调用,而 uaa-provider 全部服务处于宕机状态,因此 Feign 将会触发 Hystrix 回退,执行 Fallback 回退处理类 UserClientFallback 的回退实现方法,返回 Fallback 回退处理的内容,输出的内容如图 2-17 所示。



图 2-17 UserClientFallback 回退处理类生效后的示意图


接下来看第二种方式,定义和使用一个 Fallback 回退处理工厂类。


第二种方式具体的实现也可以分为两步:第一步创建一个 Fallback 回退处理工厂类,该工厂类需要实现 Hystrix 的 FallbackFactory 回退工厂接口,实现其抽象的 create 创建方法,在该方法的实现代码中,需要返回一个 Feign 客户端接口的实现类,方法中的具体实现即为回退处理实例,可以通过匿名类的方式创建一个新的回退处理类,并在该匿名类的每个方法的实现代码中编写好 RPC 回退逻辑;第二步在 Feign 客户端接口的关键性注解 @FeignClient 上配置失败处理工厂类,将 fallbackFactory 属性的值配置为上一步定义的 FallbackFactory 回退处理工厂类。


下面介绍具体的实例,演示如何定义和使用一个 FallbackFactory 回退处理工厂类。这里任意以 uaa-client 模块中的 RPC 调用接口 UserClient 为例进行演示。第一步为其定义一个简单的 FallbackFactory 回退处理工厂类,代码如下:


package com.crazymaker.springcloud.user.info.remote.fallback;//省略import/** *Feign客户端接口的回退处理工厂类 */@Slf4j@Componentpublic class UserClientFallbackFactory implements FallbackFactory<UserClient>{ /** *创建UserClient客户端的回退处理实例 */ @Override public UserClient create(final Throwable cause) {log.error("RPC异常了,回退!",cause); /** *创建一个UserClient客户端接口的匿名回退实例 */ return new UserClient() { /** *方法: 获取用户信息RPC失败后的回退方法 */ @Override public RestOut<UserDTO> detail(Long userId) { return RestOut.error("FallbackFactory fallback:user detail rest服务调用失败" ); } }; }}
复制代码


第二步是在 Feign 客户端接口 UserClient 的 @FeignClient 注解上,将 fallbackFactory 属性的值配置为上一步定义的 UserClientFallbackFactory 回退处理工厂类,代码如下:


package com.crazymaker.springcloud.user.info.remote.client;//省略import/** *Feign客户端接口 *@description:获取用户信息的RPC接口类*/@FeignClient(value = "uaa-provider", configuration = FeignConfiguration.class,配置回退处理厂类 fallbackFactory = UserClientFallbackFactory.class, #配置回退处理工厂类 path = "/uaa-provider/api/user")public interface UserClient{ @RequestMapping(value = "/detail/v1", method = RequestMethod.GET) RestOut<UserDTO> detail(@RequestParam(value = "userId") Long userId);}
复制代码


第二种方式回退工厂类的具体验证过程与第一种方式回退类的验证相同:


停掉所有的 uaa-provider 服务,然后在 demo-provider 的 swagger-ui 界面访问其 REST 接口/api/call/uaa/user/detail/v2,此 REST 接口的内部代码会通过 UserClient 远程调用 Feign 接口对目标 uaa-provider 的 REST 接口/api/user/detail/v1 发起 Feign RPC 远程调用,而 uaa-provider 全部服务处于宕机状态,因此 Feign 将会触发 Hystrix 回退,执行 fallback 回退处理工厂类 UserClientFallbackFactory 的 create 方法创建一个回退处理类实例,并执行回退处理类实例中的回退处理逻辑,返回回退处理的结果。


在进行失败回退时,使用第一种方式的回退类和使用第二种方式的回退工厂类有什么区别呢?


答案是:在使用第一种方式的回退类时,远程调用 RPC 过程中所引发的异常已经被回退逻辑彻底地屏蔽掉了。应用程序不太方便干预,也看不到 RPC 过程中的具体异常,尽管这些异常对于问题的排除非常有帮助。在使用第二种方式的回退工厂类时,应用程序可以通过 Java 代码对 RPC 异常进行拦截和处理,包括进行日志输出。

分布式系统面临的雪崩难题

在分布式系统中,一个服务可能会依赖很多其他的服务,并且这些服务不可避免有失效的可能。假如一个应用运行 30 个 Provider 实例,每个实例 99.99%的时间处于正常服务状态,即使只有 0.01%的失败率,每个月仍然有几个小时不可用。另外,还有一个大问题:流量洪峰过来时,服务有可能被其他服务所依赖。如果这个 Provider 实例出现延迟响应,就会导致其他 Provider 发生更多级联故障,从而导致这个分布式系统不可用。


举一个简单的例子,在一个秒杀系统中,商品(goodprovider)、订单(order-provider)、秒杀(seckill-provider)3 个 Provider 都会通过 RPC 远程调用到用户账号与认证(uaa-provider)的相关接口,查询用户的相关信息,如图 2-18 所示。



图 2-18 秒杀系统中,商品、订单、秒杀、用户 4 个 Provider 之间的依 赖示意图


若在流量洪峰过来之时 uaa-provider 出现响应迟钝(甚至宕机),则商品、订单、秒杀 3 个 Provider 都会出现等待超时而导致响应缓慢,由于排队的请求越来越多、单个请求时间变得很长(因为内部都有超时等待),因此各服务节点的系统资源(CPU、内存等)很快会耗尽,最后进入系统性雪崩状态,如图 2-19 所示。



图 2-19 流量洪峰过来时因 uaa-provider 响应缓慢导致整体雪崩


总体来说,在微服务架构中,根据业务拆分成一个个 Provider 微服务,由于网络原因或者自身的原因,服务并不能保证 100%可用,为了保证服务提供者高可用,单个 Provider 服务通常会多体部署。由于 Provider 与 Provider 之间的依赖性,故障或者不可用会沿请求调用链向上传递,对整个系统造成瘫痪的灾难性后果,这就是故障的雪崩效应。


引发雪崩效应的原因比较多,下面是常见的几种:


(1)硬件故障:如服务器宕机、机房断电、光纤被挖断等。


(2)流量激增:如流量异常、巨量请求瞬时涌入(如秒杀)等。


(3)缓存穿透:一般发生在系统重启所有缓存失效时,或者发生在短时间内大量缓存失效时,前端过来的大量请求没有命中缓存,直击后端服务和数据库,造成服务提供者和数据库超负荷运行,引起整体瘫痪。


(4)程序 BUG:如程序逻辑 BUG 导致内存泄漏等原因引发的整体瘫痪。


(5)JVM 卡顿:JVM 的 FullGC 时间较长,极端的情况长达数十秒,这段时间内 JVM 不能提供任何服务。


为了解决雪崩效应,业界提出了熔断器模型。通过熔断器,当一些非核心服务出现响应迟缓或者宕机等异常时,对服务进行降级并提供有损服务,以保证服务的柔性可用,避免引起雪崩效应。

Spring Cloud Hystrix 熔断器

在物理学上,熔断器本身是一个开关装置,用在电路上保护线路过载,当线路中有电器发生短路时,熔断器能够及时切断故障,防止发生过载、发热甚至起火等严重后果。分布式架构中的熔断器主要用于 RPC 接口上,为接口安装上“保险丝”,以防止 RPC 接口出现拥塞时导致系统压力过大而引起的系统瘫痪,当 RPC 接口流量过大或者目标 Provider 出现异常时,熔断器及时切断故障可以起到自我保护的作用。


为什么说熔断器非常重要呢?如果没有过载保护,在分布式系统中,当被调用的远程服务无法使用时,就会导致请求的资源阻塞在远程服务器上而耗尽。很多时候刚开始可能只是出现了局部小规模的故障,然而由于种种原因,故障影响范围越来越大,最终导致全局性的后果。


熔断器通常也叫作熔断器,其具体的工作机制为:统计最近 RPC 调用发生错误的次数,然后根据统计值中的失败比例等信息来决定是否允许后面的 RPC 调用继续或者快速地失败回退。


熔断器的 3 种状态如下:


(1)关闭(closed):熔断器关闭状态,这也是熔断器的初始状态,此状态下 RPC 调用正常放行。


(2)开启(open):失败比例到一定的阈值之后,熔断器进入开启状态,此状态下 RPC 将会快速失败,然后执行失败回退逻辑。


(3)半开启(half-open):在打开一定时间之后(睡眠窗口结束),熔断器进入半开启状态,小流量尝试进行 RPC 调用放行。如果尝试成功,熔断器就变为关闭状态,RPC 调用正常;如果尝试失败,熔断器就变为开启状态,RPC 调用快速失败。


熔断器状态之间的相互转换关系如图 2-20 所示。



图 2-20 熔断器状态之间的相互转换关系


下面重点介绍熔断器的半开启状态。在半开启状态下,允许进行一次 RPC 调用的尝试,如果实际调用成功,熔断器就会复位到关闭状态,回归正常的模式;但是如果这次 RPC 调用的尝试失败,熔断器就会返回到开启状态,一直等待到下次半开启状态。


Spring Cloud Hystrix 中的熔断器默认是开启的,但是可以通过配置熔断器的参数进行定制。下面是 demo-provider 微服务中熔断器示例的相关配置:


hystrix: ... command: default: ... circuitBreaker: #熔断器相关配置 enabled: true #是否使用熔断器,默认为true requestVolumeThreshold: 20 #窗口时间内的最小请求数 sleepWindowInMilliseconds: 5000 #打开后允许一次尝试的睡眠时间,默认配置为5秒 errorThresholdPercentage: 50 #窗口时间内熔断器开启的错误比例,默认配置为50 metrics: rollingStats: timeInMilliseconds: 10000 #滑动窗口时间
复制代码


numBuckets: 10 #滑动窗口的时间桶数以上用到的 Hystrix 熔断器相关参数分为两类:熔断器相关参数和滑动窗口相关参数。对示例中用到的熔断器的相关参数大致介绍如下:


(1)hystrix.command.default.circuitBreaker.enabled:该配置用来确定熔断器是否用于跟踪 RPC 请求的运行状态,或者说用于配置是否启用熔断器,默认值为 true。


(2)hystrix.command.default.circuitBreaker.requestVolumeThreshold:


该配置用于设置熔断器触发熔断的最少请求次数。如果设置为 20,那么当一个滑动窗口时间内(比如 10 秒)收到 19 个请求时,即使 19 个请求都失败,熔断器也不会打开变成 open 状态,默认值为 20。


(3)hystrix.command.default.circuitBreaker.errorThresholdPercentage:该配置用于设置错误率阈值,在滑动窗口时间内,当错误率超过此值时,熔断器进入 open 状态,所有请求都会触发失败回退(fallback),错误率阈值百分比的默认值为 50。


(4)hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds:该配置用于设置熔断器的睡眠窗口,具体指的是确定熔断器打开之后多长时间才允许一次请求尝试执行,默认值为 5 000 毫秒,表示当熔断器打开后,5 000 毫秒内会拒绝所有请求,5 000 毫秒后熔断器才会进行入 half-open 状态。


(5)hystrix.command.default.circuitBreaker.forceOpen:如果配置为 true,熔断器就会被强制打开,所有请求将触发失败回退(Fallback),默认值为 false。


熔断器的状态转换与 Hystrix 的滑动窗口的健康统计值(比如失败比例)相关。接下来对示例中使用到的 Hystrix 健康统计相关配置大致介绍如下:


(1)hystrix.command.default.metrics.rollingStats.timeInMilliseconds:设置统计滑动窗口的持续时间(以毫秒为单位),默认值为 10 000 毫秒。熔断器的打开会根据一个滑动窗口的统计值来计算,若滑动窗口时间内的错误率超过阈值,则熔断器进入开启状态。滑动窗口将被进一步细分为时间桶(Bucket),滑动窗口的统计值等于窗口内所有时间桶的统计信息的累加,每个时间桶的统计信息包含请求成功(Success)、失败(Failure)、超时(Timeout)、被拒(Rejection)的次数。


(2)hystrix.command.default.metrics.rollingStats.numBuckets:设置一个滑动窗口被划分的时间桶数量,默认值为 10。若滑动窗口的持续时间为 10 000 毫秒,并且一个滑动窗口被划为 10 个时间桶,则一个时间桶的时间为 1 秒。所设置的 numBuckets(时间桶数量)和 timeInMilliseconds(滑动窗口时长)的值有一定关系,必须符合 timeInMilliseconds%numberBuckets==0 的规则,否则会抛出异常,例如 70 000(滑动窗口 70 000 毫秒)%700(桶数)==0 是可以的,但是 70000(滑动窗口 70 000 毫秒)%600(桶数)==400 将抛出异常。


以上有关 Hystrix 熔断器的配置选项使用的是 hystrix.command.default 前缀,这些默认配置项将对项目中所有 FeignRPC 接口生效,除非某个 Feign RPC 接口进行单独配置。如果需要对某个 Feign RPC 调用进行特殊的配置,配置项前缀的格式如下:


hystrix.command.类名#方法名(参数类型列表)
复制代码


下面来看一个对单个接口进行特殊配置的例子,以对 UserClient 类中


的 Feign RPC 接口/detail/v1 进行特殊配置为例。该接口的功能是从 user-provider 服务获取用户信息,在配置之前先看一下 UserClient 接口的代码,具体如下:


package com.crazymaker.springcloud.user.info.remote.client;...@FeignClient(value = "uaa-provider", configuration = FeignConfiguration.class, fallback = UserClientFallback.class, path = "/uaa-provider/api/user")public interface UserClient{ /** *远程调用RPC方法:获取用户详细信息 *@param userId用户Id *@return用户详细信息 */ @RequestMapping(value = "/detail/v1", method = RequestMethod.GET) RestOut<UserDTO> detail(@RequestParam(value = "userId") Long userId);}
复制代码


在 demo-provider 中,如果要对 UserClient.detail 接口的 RPC 调用的熔断器参数进行特殊的配置,就不使用 hystrix.command.default 默认前缀,而是使用 hystrix.command.FeignClient#Method 格式的前缀,具体的配置项如下:


hystrix: ... command: UserClient#detail(Long): #格式为:类名#方法名(参数类型列表) ... circuitBreaker: #熔断器相关配置 enabled: true #是否使用熔断器,默认为true requestVolumeThreshold: 20 #至少有20个请求,熔断器才会达到熔断触发的次数阈值 sleepWindowInMilliseconds: 5000 #打开后允许一次尝试的睡眠时间,默认配置为5秒 errorThresholdPercentage: 50 #窗口时间内熔断器开启的错误比例,默认配置为50 metrics: rollingPercentile: timeInMilliseconds: 60000 #滑动窗口时间 numBuckets: 600 #滑动窗口的时间桶数 bucketSize: 200 #时间桶内的统计次数
复制代码


除了熔断器 circuitBreaker 相关参数和 metrics 滑动窗口相关参数之外,其他很多 Hystrix command 参数也可以对特定的 Feign RPC 接口进行特殊配置,配置时仍然使用“类名 #方法名(形参类型列表)”的格式。对于初学者来说,有关滑动窗口的概念和配置理解起来还是比较费劲的。

本文给大家讲解的内容是 springcloud 入门实战:Feign+Hystrix 实现 RPC 调用保护

  1. 下篇文章给大家讲解的是 SpringCloudRPC 远程调用核心原理;

  2. 觉得文章不错的朋友可以转发此文关注小编;

  3. 感谢大家的支持!SpringCloud 微服务架构实战:Feign+Hystrix 实现 RPC 调用保护 

发布于: 2021 年 05 月 17 日阅读数: 700
用户头像

小Q

关注

还未添加个人签名 2020.06.30 加入

小Q 公众号:Java架构师联盟 作者多年从事一线互联网Java开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果能为您提供帮助,请给予支持(关注、点赞、分享)!

评论 (1 条评论)

发布
用户头像
9012年了,早点换到resilience4j 吧……^ ^
2021 年 05 月 18 日 13:32
回复
没有更多了
SpringCloud微服务架构实战:Feign+Hystrix实现RPC调用保护