Sentinel 隔离和降级
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
如果有需要可以查看官方文档sentinelguard.io/zh-cn/docs/…
流量控制是一种预防措施,能够减缓雪崩发生的情况,但是无法完全隔绝。如果想要将雪崩问题控制在一定的范围,需要通过线程隔离和熔断降级这两个手段了。
-
**线程隔离:**消费者远程调用提供者方法的时候,为每个远程调用的请求分配独立线程池。当某一个提供者出现故障时,最多消耗该提供者对应请求的独立线程池资源,避免消费者的所有资源被耗尽。
-
**熔断降级:**在消费者远程调用提供者的过程中,加入一个断路器。断路器统计提供者响应失败的次数,若 失败次数/总请求次数 比例过高,则熔断该业务,不允许再访问该服务的提供者。
无论是线程隔离或者熔断降级都是对提供者的限制和监管,对消费者的保护。在提供者出现故障的时候对消费者通过线程隔离、熔断降级进行保护。
我们的微服务远程调用通过 Feign 完成,因此我们将 Feign 和 Sentinel 进行整合,在 Feign 中实现线程隔离和熔断降级。
1. FeignClient 整合 Sentinel
- Feign 整合 Sentinel 步骤:
- 在application.yml中配置:feign.sentienl.enable=true
- 给FeignClient编写FallbackFactory并注册为Bean
- 将FallbackFactory配置到FeignClient
1.1 修改配置,开启 Sentinel 功能
-
修改消费者的 application.yml 文件,开启 Feign 的 Sentinel 功能:
feign: sentinel: enabled: true # 开启 feign 对 sentinel 的支持 复制代码
1.2 编写失败降级逻辑
当提供者出现故障的时候,对应的请求返回的是异常信息。我们不能让异常信息赤裸裸的展示在用户面前,通常会给用户返回一个友好提示或默认结果。这个就是请求失败后的降级逻辑,也叫做失败降级逻辑。
-
给 Feign 编写失败后的降级逻辑,通常有两种方法:
- FallbackClass,无法对远程调用做异常处理
- FallbackFactory,可以对远程调用的异常做处理
我们通常选择第二种方法做失败降级处理
接下来我们将演示第二种方法 FallbackFactory 处理方法
-
在 Feign 里定义 FallbackFactory
代码:
package cn.itcast.feign.clients.fallback; import cn.itcast.feign.clients.UserClient; import cn.itcast.feign.pojo.User; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; /** * @author HGD * @date 2022/8/17 12:09 */ @Slf4j public class UserClientFallbackFactory implements FallbackFactory<UserClient> { @Override public UserClient create(Throwable throwable) { return new UserClient() { @Override public User findById(Long id) { log.error("查询用户失败", throwable); return new User(); } }; } } 复制代码
-
将上面的 FallbackFactory 注册成 Bean
package cn.itcast.feign.config; import cn.itcast.feign.clients.fallback.UserClientFallbackFactory; import feign.Logger; import org.springframework.context.annotation.Bean; public class DefaultFeignConfiguration { @Bean public Logger.Level logLevel(){ return Logger.Level.BASIC; } /** * 将 FallbackFactory 注册到 Bean 中 * @return new UserClientFallbackFactory(); */ @Bean public UserClientFallbackFactory userClientFallbackFactory() { return new UserClientFallbackFactory(); } } 复制代码
-
在 Feign 中提供者的接口中添加
@FeignClient(fallbackFactory = FallbackFactory.class)
注解代码:
package cn.itcast.feign.clients; import cn.itcast.feign.clients.fallback.UserClientFallbackFactory; import cn.itcast.feign.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class) public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); } 复制代码
完成所有步骤之后重启消费者的服务,访问一下涉及远程调用的业务,然后查看 Sentinel 控制台
2. 线程隔离(舱壁模式)
2.1 线程隔离的实现方式
线程隔离通过两种方式实现:
-
线程池隔离
为每个远程调用的请求分配独立线程池,利用线程池本身实现隔离效果
-
信号量隔离
不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求
方式二是 Sentinel 默认方法
-
两者优缺点
优点 缺点 场景 信号量隔离 轻量级,无额外开销 不支持主动超时,不支持异步调用 高频使用,高输出 线程池隔离 支持主动超时,支持异步调用 线程的额外开销比较大 低扇出,调用较少的情况 低扇出:一个类,尽可能不要去依赖别的类,就是所谓的low fan out
主动超时:能主动控制方法执行的超时时间,如果超时了或有异常就抛出异常
2.2 Sentinel 的线程隔离
-
配置规则
-
案例
-
配置隔离规则
选择 Feign 接口后面的流控按钮:
填写表单:
-
使用 JMeter 测试
选择《阈值类型-线程数<2》:
一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
查看运行结果:
发现虽然结果都是通过了,不过部分请求得到的响应是降级返回的null信息。
-
2.3.总结
线程隔离的两种手段是?
-
信号量隔离
-
线程池隔离
信号量隔离的特点是?
- 基于计数器模式,简单,开销小
线程池隔离的特点是?
- 基于线程池模式,有额外开销,但隔离控制更强
3. 熔断降级
现代微服务架构基本都是分布式的,整个分布式系统由非常多的微服务组成。不同服务之间相互调用,组成复杂的调用链路。前面描述的问题在分布式链路调用中会产生放大的效果。整个复杂链路中的某一环如果不稳定,就可能会层层级联,最终可能导致整个链路全部挂掉。因此我们需要对不稳定的 弱依赖服务调用 进行 熔断降级,暂时切断不稳定的服务调用,避免局部不稳定因素导致整个分布式系统的雪崩。熔断降级作为保护服务自身的手段,通常在客户端(调用端)进行配置。
熔断降级的思路就是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务,即拦截一切访问该服务的请求。当服务恢复时,断路器会放行访问该服务的请求。
-
断路器控制熔断和放心是通过状态机来完成:
状态机包括三个状态:
-
Closed:关闭状态。断路器处于这个状态会放行所有请求,并且开始统计异常比例、慢请求比例。比例超过阈值则将状态切换至 Open 状态。
-
Open:打开状态。这个状态下对应提供者的所有请求都会被拒绝、快速失败,直接走上述的失败降级逻辑。Open 状态 5 秒后进入 Half-Open 状态。
-
Half-Open:半开状态。这个状态下将放行一次请求,根据结果来判断接下来操作
-
请求成功:进入 Closed 状态
-
请求失败:进入 Open 状态
慢请求:请求的响应时间大于规定的响应之间
-
-
断路器的熔断策略有三种:慢调用、异常比例、异常数。
3.1 慢调用
-
慢调用
慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。就是说,业务的响应时长(RT)大于指定时长的请求被认定为慢调用请求。在指定时间内,如果请求数量超过 QPS 且慢调用比例大于设定阈值,则触发熔断。
我认为就是请求超量且响应缓慢就熔断。
-
规则配置
RT 超过 50ms 的调用是慢调用。统计最近 1000ms 内的请求,如果 QPS 超过 5 次且慢调用比例不低于 0.4 则触发熔断,熔断时长为 5 秒。然后进入 Half-Open 状态,放行一次请求做测试
-
案例
需求:给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5
1)设置慢调用
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:
此时,orderId=101的订单,关联的是id为1的用户,调用时长为60ms:
orderId=102的订单,关联的是id为2的用户,调用时长为非常短;
2)设置熔断规则
下面,给feign接口设置降级规则:
规则:
超过50ms的请求都会被认为是慢请求
3)测试
在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:
触发了熔断,请求时长缩短至5ms,快速失败了,并且走降级逻辑,返回的null
在浏览器访问:http://localhost:8088/order/102,竟然也被熔断了:
3.2 异常比例、异常数
-
异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。也就是说,统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
我认为就是请求超量且异常过多就熔断
-
规则配置
-
异常比例
统计最近 1000ms 内的请求,如果请求次数超过 10 次且异常比例高于 0.4 则触发熔断
-
异常数
统计最近 1000ms 内的请求,如果请求量超过 10 次且异常比例不低于 2 次则触发熔断
-
-
案例
需求:给 UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5s
1)设置异常请求
首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例的熔断:
也就是说,id 为 2时,就会触发异常
2)设置熔断规则
下面,给feign接口设置降级规则:
规则:
在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。
3)测试
在浏览器快速访问:http://localhost:8088/order/102,快速刷新5次,触发熔断:
此时,我们去访问本来应该正常的103:
3.3 总结
- 慢调用触发情况:统计最近时间段内的请求,如果请求数量超过最小请求数且慢调用比例高于阈值则熔断。
- 异常比例\异常数触发情况:统计最近时间对内,如果异常请求的数量超过 指定异常数\异常比例 则熔断。
- 通常的使用场景
- 分布式系统中降级:假设存在应用A需要调用应用B的接口(特别是一些对接外部公司或者业务的接口时候),那么一般用于A调用B的接口时的防护;
- 数据库慢调用的防护: 假设应用需要读/写数据库,但是该读写SQL存在潜在慢SQL的可能性,那么可以对该读写接口做防护,当接口不稳定时候(存在慢SQL),那么基于熔断器做降级。
- 也可以是应用中任意弱依赖接口做降级防护(即自动降级后不影响业务核心链路)。