learn-tech/专栏/深入理解Sentinel(完)/15自定义ProcessorSlot实现开关降级.md
2024-10-16 09:22:22 +08:00

13 KiB
Raw Permalink Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        15 自定义 ProcessorSlot 实现开关降级
                        开关降级在我们公司的电商项目中是每个微服务都必须支持的一项功能,主要用于活动期间、每日流量高峰期间、主播带货期间关闭一些无关紧要功能,降低数据库的压力。

开关降级实现起来很简单,例如,我们可以使用 Spring AOP 或者动态代理模式拦截目标方法的执行,在方法执行之前,先根据 key 从 Redis 读取 value如果 value 是 true则不执行目标方法直接响应服务降级。这种方式付出的性能损耗就只有一次 redis 的 get 操作,如果不想每个请求都读 Redis 缓存,也可以通过动态配置方式,使用配置中心去管理开关。

使用 Spring AOP 实现开关降级功能

以 Redis 缓存开关为例,使用切面实现开关降级只需要三步:定义注解、实现开关降级切面、在需要使用开关降级的接口方法上添加开关降级注解。

\1. 定义开关降级注解 @SwitchDegrade代码如下

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface SwitchDegrade { // 缓存 key String key() default ""; }

提示:如果是应用在实际项目中,建议为 @SwitchDegrade 注解添加一个前缀属性,限制同一个应用下的开关 key 都是有同一个前缀,避免多个应用之间的缓存 key 冲突。

\2. 实现切面 SwitchDegradeAspect拦截接口方法的执行代码如下

@Aspect public class SwitchDegradeAspect { // 切点定义 @Pointcut("@annotation(com.wujiuye.demo.common.sentinel.SwitchDegrade)") public void degradePointCut() { }

/**
 * 拦截请求判断是否开启开关降级
 */
@Around("degradePointCut()&&@annotation(switchDegrade)")
public Object around(ProceedingJoinPoint point, SwitchDegrade switchDegrade) throws Throwable {
    String cacheKey = switchDegrade.key();
    RedisTemplate redisTemplate = SpringContextUtils.getBean(RedisTemplate.class);
    String value = redisTemplate.get(cacheKey);
    if ("true".equalsIgnoreCase(value)) {
        throw new SwitchDegradeException(cacheKey, "开关降级打开");
    }
    return point.proceed();
}

}

如代码所示SwitchDegradeAspect 拦截目标方法的执行,先从方法上的 @SwitchDegrade 注解获取开关的缓存 key根据 key 从 redis 读取当前开关的状态,如果 key 存在且 value 为 true则抛出一个开关降级异常。

当开关打开时SwitchDegradeAspect 并不直接响应请求,而是抛出异常由全局异常处理器处理,这是因为并不是每个接口方法都会有返回值,且返回值类型也不固定。所以还需要在全局异常处理器处理开关降级抛出的异常,代码如下:

@ExceptionHandler(SwitchDegradeException.class)
public BaseResponse handleSwitchDegradeException(SwitchDegradeException ex) {
    log.error("Switch Degrade! switch key is:{}, message:{}", ex.getSwitchKey(), ex.getMessage());
    return new BaseResponse(ResultCode.SERVICE_DEGRADE, ex.getMessage());
}

提示:如果是整合 OpenFeign 使用,且配置了 Fallback则全局异常可以不配置。

\3. 在需要被开关控制的接口方法上使用 @SwitchDegrade 注解,例如:

@RestController @RequestMapping("/v1/test") public class DemoController { @SwitchDegrade(key = "auth:switch") @PostMapping("/demo") public GenericResponse demo(@RequestBody Invocation invocation) { //..... } }

这种方式虽然能满足需求,但也有一个缺点,就是必须要在方法上添加 @SwitchDegrade 注解,配置不够灵活,但也不失为一个好方法。

基于 Sentinel 自定义 ProcessorSlot 实现开关降级功能

Sentinel 将降级功能的实现抽象为处理器插槽ProcessorSlot由一个个 ProcessorSlot 提供丰富的降级功能的实现,并且使用 SPI 机制提供扩展功能,使用者可通过自定义 SlotChainBuilder 自己构建 ProcessorSlotChain这相当于给我们提供插件的功能。因此我们可以通过自定义 ProcessorSlot 为 Sentinel 添加开关降级功能。

与熔断降级、限流降级一样,我们也先定义开关降级规则类,实现 loadRules API然后提供一个 Checker由 Checker 判断开关是否打开,是否需要拒绝当前请求;最后自定义 ProcessorSlot 与 SlotChainBuilder。

与使用切面实现开关降级有所不同,使用 Sentinel 实现开关降级我们不需要再在接口方法或者类上添加注解,我们想要实现的是与熔断降级、限流降级一样全部通过配置规则实现,这也是我们为什么选择基于 Sentinel 实现开关降级功能的原因。

通常,一个开关会控制很多的接口,而不仅仅只是一个,所以,一个开关对应一个降级规则,一个降级规则可配置多个资源。开关降级规则类 SwitchRule 实现代码如下:

@Data @ToString public class SwitchRule { public static final String SWITCH_KEY_OPEN = "open"; public static final String SWITCH_KEY_CLOSE = "close"; // 开关状态 private String status = SWITCH_KEY_OPEN; // 开关控制的资源 private Resources resources; @Data @ToString public static class Resources { // 包含 private Set include; // 排除 private Set exclude; } }

灵活不仅仅只是去掉注解的使用更需要可以灵活指定开关控制某些资源因此配置开关控制的资源应支持这几种情况指定该开关只控制哪些资源、除了某些资源外其它都受控制、控制全部。所以SwitchRule 的资源配置与 Sentinel 的熔断降级、限流降级规则配置不一样SwitchRule 支持三种资源配置方式:

如果资源不配置,则开关作用到全部资源; 如果配置 inclode则作用到 include 指定的所有资源; 如果不配置 inclode 且配置了 exclude则除了 exclude 指定的资源外,其它资源都受这个开关的控制。

接着实现 loadRules API。在 Sentinel 中,提供 loadRules API 的类通常命名为 XxxRuleManager即 Xxx 规则管理器,所以我们定义的开关降级规则管理器命名为 SwitchRuleManager。SwitchRuleManager 的实现代码如下:

public class SwitchRuleManager { private static volatile Set switchRuleSet = new HashSet<>(); public static void loadSwitchRules(Set rules) { SwitchRuleManager.switchRuleSet = rules; } static Set getRules() { return switchRuleSet; } }

SwitchRuleManager 提供两个接口:

loadSwitchRules用于更新或者加载开关降级规则 getRules获取当前生效的全部开关降级规则

同样地,在 Sentinel 中,一般会将检查规则是否达到触发降级的阈值由 XxxRuleChecker 完成,即 Xxx 规则检查员,所以我们定义的开关降级规则检查员命名为 SwitchRuleChecker由 SwitchRuleChecker 检查开关是否打开如果开关打开则触发开关降级。SwitchRuleChecker 的代码实现如下:

public class SwitchRuleChecker {

public static void checkSwitch(ResourceWrapper resource, Context context) throws SwitchException {
    Set<SwitchRule> switchRuleSet = SwitchRuleManager.getRules();
    // 遍历规则
    for (SwitchRule rule : switchRuleSet) {
        // 判断开关状态,开关未打开则跳过
        if (!rule.getStatus().equalsIgnoreCase(SwitchRule.SWITCH_KEY_OPEN)) {
            continue;
        }
        if (rule.getResources() == null) {
            continue;
        }
        // 实现 include 语意
        if (!CollectionUtils.isEmpty(rule.getResources().getInclude())) {
            if (rule.getResources().getInclude().contains(resource.getName())) {
                throw new SwitchException(resource.getName(), "switch");
            }
        }
        // 实现 exclude 语意
        if (!CollectionUtils.isEmpty(rule.getResources().getExclude())) {
            if (!rule.getResources().getExclude().contains(resource.getName())) {
                throw new SwitchException(resource.getName(), "switch");
            }
        }
    }
}

}

如代码所示SwitchRuleChecker 从 SwitchRuleManager 获取配置的开关降级规则,遍历开关降级规则,如果开关打开,且匹配到当前资源名称被该开关控制,则抛出 SwitchException。

SwitchException 需继承 BlockException抛出的 SwitchException 如果不被捕获,则由全局异常处理器处理。一定是要抛出 BlockException 的子类,否则抛出的异常会被资源指标数据统计收集,会影响到熔断降级等功能的准确性。

虽然 SwitchRuleChecker 使用了 for 循环遍历开关降级规则,但一个项目中的开关是很少的,一般就一个或者几个。

与熔断降级、限流降级一样,开关降级也支持一个资源被多个开关规则控制。

最后,还需要自定义实现开关降级功能的切入点 SwitchSlot。SwitchSlot 继承 AbstractLinkedProcessorSlot在 entry 方法中调用 SwitchRuleChecker#checkSwitch 方法检查当前资源是否已经降级。SwitchSlot 的代码实现如下:

public class SwitchSlot extends AbstractLinkedProcessorSlot