面试官:如何实现开关降级
实现开关降级
在电商项目中,开关降级是每个微服务都必须支持的一项功能,主要用于在促销活动期间、每日流量高峰期间、主播带货期间关闭一些无关紧要的功能,降低数据库的压力以换取更高的 TPS。
虽然 Sentinel 不提供开关降级功能,但 Sentinel 使用了 Java SPI 机制,因此不需要修改源码也能扩展 Sentinel 实现开关降级。
使用 AOP 实现开关降级
实现开关降级并不难,在不使用 Sentinel 的前提下,可以使用 Spring AOP 或动态代理模式拦截目标方法的执行,在方法执行之前,根据开关配置决定是否拦截请求。
开关配置可以使用 Redis 缓存,这样可以使集群之间共享配置,这种方式付出的性能损耗也只有一次 Redis 的 GET 操作。而如果不想每个请求都需要读取 Redis 缓存,也可以通过动态配置方式,使用配置中心管理开关。
以使用 Redis 缓存开关配置为例,讲解如何实现开关降级。使用 Spring AOP 实现开关降级只需要进行如下 3 个步骤:
定义注解并将其用作切点,并且在注解中指定开发配置的缓存 key。
实现开关降级切面,在拦截方法时,先根据 key 从 Redis 中读取 value,如果 value 是 true,则不执行目标方法,直接响应服务降级。
在需要使用开关降级的接口方法上添加开关降级注解。
第一步,定义开关降级注解 @SwitchDegrade,代码如下:
在使用 @SwitchDegrade 注解时,key 通常被定义为一个有意义的资源名称。
提示:如果是应用在实际项目中,建议为 @SwitchDegrade 注解添加一个前缀属性,使同一个应用下的开关 key 都使用同一个前缀,避免多个应用之间的缓存 key 冲突。
第二步,实现开关降级切面 SwitchDegradeAspect,该切面用于拦截目标方法的执行,代码如下:
如上述代码所示,SwitchDegradeAspect 用于拦截目标方法的执行,先从方法上的 @SwitchDegrade 注解中获取开关的缓存 key,再根据 key 从 Redis 中读取当前开关的状态,若 key 存在且 value 为 true,则抛出 SwitchDegradeException,拒绝该请求。
在本例中,当开关打开时,SwitchDegradeAspect 并不会直接响应请求,而是会抛出异常并由全局异常处理器处理该异常。这是因为并不是每个接口方法都会有返回值,且返回值类型也不固定,所以还需要由全局异常处理器处理开关降级抛出的异常,代码如下:
提示:在本例中,BaseResponse 作为通用的响应协议,只有 code 和 errMsg 两个字段,code 等于 ResultCode.SERVICE_DEGRADE,作为降级响应状态码。
第三步,在需要使用开关降级的接口方法上添加 @SwitchDegrade 注解,代码如下:
此时,如果在 Redis 缓存中添加配置“auth:switch=true”,然后访问接口“/v1/test/demo”,将会得到如下响应。
使用 Spring AOP 实现开关降级虽然能够满足需求,但是也有一个缺点,就是必须在方法上添加 @SwitchDegrade 注解,因此配置不够灵活。
扩展 Sentinel 实现开关降级
Sentinel 实现了插件功能,支持将自定义处理器插槽(ProcessorSlot)通过 SPI 注册到 ProcessorSlotChain 中,或者通过自实现 SlotChainBuilder 构建 ProcessorSlotChain,将自定义处理器插槽注册到 ProcessorSlotChain 中。因此,我们可以通过自定义 ProcessorSlot 为 Sentinel 添加开关降级功能。
与限流、熔断降级等流量控制的实现一样,首先定义开关降级规则类,实现 loadRulesAPI;然后提供一个 Checker,由 Checker 判断开关是否打开,是否需要拒绝当前请求;最后自定义 ProcessorSlot 与 SlotChainBuilder,实现拦截请求的功能。
与使用 AOP 实现开关降级有所不同,扩展 Sentinel 实现开关降级不需要在接口方法或类上添加注解,可以全部通过配置规则实现,这也是为什么选择扩展 Sentinel 实现开关降级功能的原因。
一个开关通常会控制很多接口,所以一个开关对应一个开关降级规则,一个开关降级规则可配置多个资源。开关降级规则类 SwitchRule 的代码如下:
SwitchRule 类字段说明如下:
status:开关状态,取值为 open 或 close。
resources:开关控制的资源。
Resources 内部类字段说明如下:
include:包含的资源。
exclude:排除的资源。
灵活,不仅只是不需要使用注解,还需要可以灵活地指定开关控制哪些资源,因此,配置开关控制的资源应支持以下几种情况:指定该开关只控制哪些资源(include),或控制除
了哪些资源(exclude)的其他资源,或者控制全部资源。所以,SwitchRule 的资源配置与 Sentinel 的限流、熔断降级规则的资源配置不一样,SwitchRule 支持以下 3 种资源配置方式:
如果不配置 resources,则开关作用于全部资源。
如果配置了 include,则开关作用于 include 指定的所有资源。
如果不配置 include 且配置了 exclude,则除 exclude 指定的资源外,其他资源都受这个开关的控制。
接着实现 loadRulesAPI。在 Sentinel 中,提供 loadRulesAPI 的类通常被命名为 XxxRuleManager,即 Xxx 规则管理器,所以我们定义开关降级规则管理器的名称为 SwitchRuleManager。SwitchRuleManager 的代码如下:
SwitchRuleManager 提供了两个 API:
loadSwitchRules:用于更新或加载开关降级规则。
getRules:获取当前生效的全部开关降级规则。
同样地,Sentinel 一般会将决定规则是否达到触发开关降级的阈值由 XxxRuleChecker 完成,即 Xxx 规则检查员。所以我们定义开关降级规则检查员的名称为 SwitchRuleChecker,由 SwitchRuleChecker 检查开关是否打开,若开关打开,则抛出 SwitchException,拒绝请求。
SwitchRuleChecker 的代码如下:
遍历规则,每个打开状态的开关都有可能控制当前资源,只要其中一个关联到当前资源,就拒绝当前请求。
判断开关状态,如果开关未打开,则跳过。
实现 include 语意,如果规则配置了 include,并且 include 包含当前资源,则匹配成功,抛出 SwitchException 以拒绝当前请求。
实现 exclude 语意,如果规则配置了 exclude,并且 exclude 不包含当前资源,则抛出 SwitchException 以拒绝当前请求。
整个 checkSwitch 方法实现的功能:SwitchRuleChecker 从 SwitchRuleManager 中获取配置的开关降级规则,并遍历开关降级规则,如果开关打开,且匹配到当前资源名称被该开关控制,则抛出 SwitchException。
SwitchException 需继承 BlockException,如果抛出的 SwitchException 没有被捕获,则由全局异常处理器处理。
注意:必须抛出 BlockException 的子类异常,否则抛出的异常会被资源指标数据统计收集,会影响到熔断降级等功能的准确性。
虽然 SwitchRuleChecker 使用了 for 循环遍历开关降级规则,但是一个项目中的开关是很少的,一般只有一个或几个,并且使用 hash 匹配 include 与 exclude,时间复杂度接近 O(1),所以这样实现的开关降级对性能影响并不大。
要使用开关功能,我们还需要自定义处理器插槽:SwitchSlot。
SwitchSlot 类继承 AbstractLinkedProcessorSlot 抽象类,负责在 entry 方法中调用 SwitchRuleChecker#checkSwitch 方法,检查是否需要拒绝当前请求。SwitchSlot 类的代码如下:
现在,我们需要自定义 SlotChainBuilder,即 MySlotChainBuilder,将自定义的 SwitchSlot 添加到 ProcessorSlotChain 的末尾。当然,可以将 SwitchSlot 添加到任何位置,因为 SwitchSlot 没有用到资源指标数据,所以将 SwitchSlot 放在哪里都不会影响 Sentinel 的正常工作。
MySlotChainBuilder 的代码如下:
如上述代码所示,MySlotChainBuilder 继承 DefaultSlotChainBuilder 只是为了使用 DefaultSlotChainBuilder#build 方法。若要简化 ProcessorSlotChain 的构造步骤,则只需要在 DefaultSlotChainBuilder 构造好的链表尾部添加一个 SwitchSlot 即可。
但是 MySlotChainBuilder 还没有生效,只有将 MySlotChainBuilder 注册到 SlotChainBuilder 接口的 SPI 配置文件中后,MySlotChainBuilder 才会生效。具体操作为在当前项目的 resources/META-INF/service 资源目录下创建一个名称为 com.alibaba.csp.sentinel.slotchain.SlotChainBuilder 的文件,并在该文件中配置 MySlotChainBuilder 类的全名,代码如下:
在上述操作完成后,我们在 MySlotChainBuilder#build 方法中添加断点,然后启动项目,在正常情况下,程序会在该断点停下。但是因为我们并未配置开关降级规则,所以还看不到效果。
为了验证开关降级效果,我们通过硬编码方式添加一个开关配置。创建 SpringBoot 配置类,并在配置类的初始化方法中调用 SwitchRuleManager#loadRules API 添加开关降级规则,代码如下:
上述代码配置了一个开关降级规则,配置了该规则的 status 字段值为 open,该开关降级规则只控制接口(资源)“/v1/test/demo”。
注意:这种配置方式只适用于本地测试,在实际项目中需要通过动态配置实现。
小结
本篇主要介绍如何使用 Spring AOP 实现开关降级,以及扩展 Sentinel 实现开关降级。
使用 Spring AOP 实现开关降级的缺点是配置不灵活:如果让一个开关控制多个接口,则无法实现动态为开关添加或移除其管理的接口;如果让一个开关仅控制一个接口,则需要多个开关配置,不易于管理。
在不使用 Sentinel 的项目中,使用 Spring AOP 实现开关降级是一种不错的方式,而如果在项目中已经使用了 Sentinel,建议采用扩展 Sentinel 实现开关降级,并结合动态配置,实现更为灵活的开关降级。
评论