背景
在系统运行时,为了保证核心服务能正常提供服务,不至于因为一些非核心功能而阻塞服务,需要对服务进行分级。当非核心服务影响到核心服务时,能通过配置或者其他手段快速切断非核心服务从而保证核心服务能正常对用户提供服务。
如何切断非核心服务呢?常用的方法有限流、熔断、降级,市面上也有很多的组件能提供相应的功能,这些组件都提供了很多强大的功能,但引入这些开源组件的同时也会带来一些复杂的配置以及学习成本,另外公司微服务是 dubbo 构建的,引入 spring-cloud 的一些组件会比较复杂。
基于此,我们决定自研一个降级组件,集成到公司的各个服务里面,提供最基础的降级服务。
服务故障的场景:
服务故障分为接口级故障和系统级故障
接口级故障的典型表现就是系统并没有宕机,网络也没有中断,但业务却出现问题了。
例如,业务响应缓慢、大量访问超时、大量访问出现异常,这类问题的主要原因在于系统压力太大、负载太高,导致无法快速处理业务请求,由此引发更多的后续问题。
例如,最常见的数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库、要么超时,最终用户看到的现象就是访问很慢,一会访问抛出异常,一会访问又是正常结果。接口故障如果处理不及时,严重的时候甚至会引起系统级故障。如数据库慢查询导致数据库 cpu 升高,查询的服务短时间内频繁 fullgc,并因此形成连锁反应,牵一发而动全身,依赖该该服务的其他服务全都不可用,蝴蝶效应引起核心服务的不可用
故障应对策略
优先保证核心业务和优先保证绝大部分用户
降级
降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。**降级的核心思想就是丢车保帅,优先保证核心业务。**例如,对于教育类 App 学习主链路是核心服务,其他的各种礼品活动弹窗,老师点评服务等如果出问题后不应该影响主学习链路,这时可以停掉这些非核心服务。常见的实现降级的方式有:
为每一个可降级服务提供一个业务开关配置,在业务出现故障后通过切换业务开关配置进行手动降级,但主要缺点是如果服务器数量多,需要一台一台去操作,效率比较低,这在故障处理争分夺秒的场景下是比较浪费时间的。
为了解决系统后门降级方式的缺点,将降级操作独立到一个单独的系统中,可以实现复杂的权限管理、批量操作等功能,但引入独立系统运维,集成等复杂度会相应提高 Hystrix,sentinel 等都有相应功能实现
熔断
熔断和降级是两个比较容易混淆的概念,因为单纯从名字上看好像都有禁止某个功能的意思,但其实内在含义是不同的,原因在于降级的目的是应对系统自身的故障,而熔断的目的是应对依赖的外部系统故障的情况。
假设一个这样的场景:A 服务的 X 功能依赖 B 服务的某个接口,当 B 服务的接口响应很慢的时候,A 服务的 X 功能响应肯定也会被拖慢,进一步导致 A 服务的线程都被卡在 X 功能处理上,此时 A 服务的其他功能都会被卡住或者响应非常慢。这时就需要熔断机制了,即:A 服务不再请求 B 服务的这个接口,A 服务内部只要发现是请求 B 服务的这个接口就立即返回错误,从而避免 A 服务整个被拖慢甚至拖死。
由 API 调用层来进行采样或者统计,如果接口调用散落在代码各处就没法进行统一处理了。
例如 1 分钟内 30% 的请求响应时间超过 1 秒就熔断,这个策略中的“1 分钟”“30%”“1 秒”都对最终的熔断效果有影响。实践中一般都是先根据分析确定阈值,然后上线观察效果,再进行调优。
限流
降级是从系统功能优先级的角度考虑如何应对故障,而限流则是从用户访问压力的角度来考虑如何应对故障。限流指只允许系统能够承受的访问量进来,超出系统访问能力的请求将被丢弃。根据限流作用范围,可以分为单机限流和分布式限流;根据限流方式,又分为计数器、滑动窗口、漏桶限令牌桶限流。
限流一般都是系统内实现的,大致可以分为两类:
基于请求限流指从外部访问的请求角度考虑限流,常见的方式有:限制总量、限制时间量。
基于请求限流是从系统外部考虑的,而基于资源限流是从系统内部考虑的,即:找到系统内部影响性能的关键资源,对其使用上限进行限制。常见的内部资源有:连接数、文件句柄、线程数、请求队列等。
基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力,但实践中设计也面临两个主要的难点:如何确定关键资源,如何确定关键资源的阈值。
通常情况下,这也是一个逐步调优的过程,即:设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化。
排队
排队实际上是限流的一个变种,限流是直接拒绝用户,排队是让用户等待一段时间。
最有名的排队当属 12306 网站排队了。
排队虽然没有直接拒绝用户,但用户等了很长时间后进入系统,体验并不一定比限流好。
于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如使用 Kafka,RocketMQ 这类消息队列来消费用户请求。
starter 原理
springBoot starter 基于约定大于配置思想,使用 spi 机制及自动装配原理,可以将一些通用的功能能够封装成一个独立组件并很方便的集成到不同的项目里面,简化开发,提升代码复用能力。
简单来讲就是引入了一些相关依赖和一些初始化的配置。
自定义一个降级 starter 组件
自定义一个 starter 组件名
spring 官方 starter 通常命名为 spring-boot-starter-{name}
如 spring-boot-starter-web
spring 官方建议非官方 starter 命名应遵循 {name}-spring-boot-starter 的格式 例如由 mybatis 提供的 mybatis-spring-boot-starter
因此我们自定义的降级组件就叫 degrade-spring-boot-starter
<dependency>
<groupId>org.degrade.spring.boot</groupId>
<artifactId>degrade-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
复制代码
自动配置类
自动配置类就是 Bean 实例的工厂,将组件涉及的一些 Bean,配置信息交给 spring 容器管理。目前降级组件定义了 4 种降级策略:
@Configuration
@ConditionalOnProperty(
name = {"degrade.enabled"},
matchIfMissing = true
)
public class DegradeAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ServiceDegradeAspect createDegradeAspect() {
//降级切面核心逻辑
return new ServiceDegradeAspect();
}
@Bean(name = "CALL_METHOD")
@ConditionalOnMissingBean
public CallMethodHandler createCallMethodHandler(){
//调用指定方法降级
return new CallMethodHandler();
}
@Bean(name = "DEFAULT_VALUE")
@ConditionalOnMissingBean
public DefaultValueHandler createDefaultValueHandler(){
//取指定的默认值降级
return new DefaultValueHandler();
}
@Bean(name = "FETCH_CONFIG_VALUE")
@ConditionalOnMissingBean
public FetchConfigValueHandler createFetchConfigValueHandler(){
//取apollo上配置的值降级
return new FetchConfigValueHandler();
}
@Bean(name = "THROW_EXCEPTION")
@ConditionalOnMissingBean
public ThrowExceptionHandler createThrowExceptionHandler(){
//抛出异常降级
return new ThrowExceptionHandler();
}
@Bean
@ConditionalOnMissingBean
public NullValueProvider createNullValueProvider(){
return new NullValueProvider();
}
}
复制代码
自定义降级注解
降级注解里面标识了需要降级的业务,场景降级后的结果,降级结果就是核心,支持四种策略的配置,所以降级结果的配置也是放在注解里的
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Degrade {
/**
* 降级业务key
*/
String businessKey();
/**
* 降级场景key
*/
String sceneKey() default StringUtils.EMPTY;
/**
* 降级后的结果(支持多种降级策略)
*/
DegradeResult result();
}
复制代码
降级结果的注解
public @interface DegradeResult {
/**
* 支持的降级处理枚举(降级策略)
*/
DegradeResultEnum resultType();
/**
* 从apollo上获取指定值的key,与DegradeResultEnum#FETCH_CONFIG_VALUE 配合使用
*/
String fetchKey() default StringUtils.EMPTY;
/**
* 将获取的配置内容转变成指定的对象
*/
Class<?> fetchResult() default Void.class;
/**
* 执行回调的方法名称,与DegradeResultEnum#CALL_BACK_VALUE 配合使用
*/
String methodName() default StringUtils.EMPTY;
/**
* 回调的class
*/
Class<?> methodClass() default Void.class;
/**
* 默认值提供者,NullValueProvider默认提供,自定义复杂对象的返回值构建可以实现该接口
*/
Class<? extends DegradeValueProvider> defaultValueProvider() default NullValueProvider.class;
}
class NullValueProvider implements DegradeValueProvider<Void> {
@Override
public Void buildDegradeValue() {
return null;
}
}
复制代码
降级配置
组件采用了从 apollo 上获取业务配置的方式来进行降级,与 Apollo 耦合比较严重。如果不想采用 apollo 配置的方式进行业务降级配置,可以采用 @ConfigurationProperties 把配置在 yml 或者 properties 配置文件中的配置参数信息封装到配置的 bean 里,一般结合 @EnableConfigurationProperties 注解使用
@Data
public class ServiceDegradeConfig implements Serializable {
private static final long serialVersionUID = -1628960982004214364L;
/**
* 降级总开关状态:true-全局开启服务降级;false-全局关闭服务降级
*/
private Boolean state;
/**
* 场景开关
*/
private Map<String, Boolean> sceneState;
}
复制代码
降级处理器
采用策略模式,定义降级处理逻辑,具体的降级策略实现该接口即可,提供可扩展性的降级策略
public interface DegradeHandler {
/**
* 降级处理
*
* @return 处理后的结果
*/
Object doDegrade(Degrade degrade, ProceedingJoinPoint point);
}
复制代码
调用指定方法降级策略
@Slf4j
public class CallMethodHandler implements DegradeHandler {
@Autowired
private ApplicationContext applicationContext;
@Override
public Object doDegrade(Degrade degrade, ProceedingJoinPoint point) {
DegradeResult result = degrade.result();
String methodName = result.methodName();
Class<?> handlerClass = result.methodClass();
Object target = point.getTarget();
Object targetObj = point.getThis();
if (handlerClass == Void.class) {
handlerClass = target.getClass();
} else {
targetObj = applicationContext.getBean(handlerClass);
}
Object[] args = point.getArgs();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
Method m = null;
try {
m = handlerClass.getMethod(methodName, parameterTypes);
return m.invoke(targetObj, args);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
log.error("degrade call method={} error,message={}", methodName, e.getMessage());
e.printStackTrace();
}
return null;
}
}
复制代码
获取 apollo 上的降级配置信息进行降级策略
public class FetchConfigValueHandler implements DegradeHandler {
@Autowired
private ApolloUtil apolloUtil;
@Override
public Object doDegrade(Degrade degrade, ProceedingJoinPoint point) {
DegradeResult result = degrade.result();
Class<?> aClass = result.fetchResult();
String fetchKey = result.fetchKey();
if (StringUtils.isEmpty(fetchKey)) {
return null;
}
Optional<?> resultOpt = apolloUtil.getMessage(fetchKey, aClass);
return resultOpt.orElse(null);
}
}
复制代码
提供默认返回值的降级处理策略(常用)
public class DefaultValueHandler implements DegradeHandler {
@Autowired
private ApplicationContext applicationContext;
@Override
@SuppressWarnings("rawtypes")
public Object doDegrade(Degrade degrade, ProceedingJoinPoint point) {
DegradeResult result = degrade.result();
Class<? extends DegradeValueProvider> providerClass = result.defaultValueProvider();
//获取指定的默认返回值构造提供者进行默认值构建并返回
DegradeValueProvider provider = applicationContext.getBean(providerClass);
return provider.buildDegradeValue();
}
}
复制代码
提供默认返回值的降级处理策略比较常用,但是返回值的类型很多,组件默认提供返回 null 对象的返回值,但业务上存在其他对象,如 Boolean,以及自定义的复杂对象等,因此这里提供了默认返回值提供者函数式接口方便扩展
@FunctionalInterface
public interface DegradeValueProvider<T> {
/**
* 构造服务降级后的返回值
* @return T
*/
T buildDegradeValue();
}
复制代码
降级服务的核心逻辑,切面实现
@Slf4j
@Aspect
public class ServiceDegradeAspect {
@Autowired
private ApplicationContext applicationContext;
//apollo配置业务上的降级场景
@ApolloJsonValue("${app.service.degrade.gray.config:{}}")
private Map<String, ServiceDegradeConfig> appDegradeConfigMap;
@Around("@annotation(degrade)")
public Object doDegrade(ProceedingJoinPoint proceedingJoinPoint, Degrade degrade) throws Throwable {
//获取注解里面配置的降级key标识
String businessKey = degrade.businessKey();
String sceneKey = degrade.sceneKey();
if (StringUtils.isBlank(sceneKey)) {
sceneKey = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod().getName();
}
boolean needDegrade = false;
try {
//检查是否需要降级
needDegrade = checkNeedDegrade(businessKey, sceneKey);
} catch (Exception e) {
log.warn("checkNeedDegrade error。businessKey:{}, sceneKey:{}", businessKey, sceneKey, e);
}
if (needDegrade) {
//执行降级
return doDegradeAction(degrade, proceedingJoinPoint);
}
return proceedingJoinPoint.proceed();
}
private Object doDegradeAction(Degrade degrade, ProceedingJoinPoint point) {
DegradeResult result = degrade.result();
DegradeResultEnum degradeResultEnum = result.resultType();
String name = degradeResultEnum.name();
//使用具体的降级策略进行降级
DegradeHandler handler = applicationContext.getBean(name, DegradeHandler.class);
return handler.doDegrade(degrade, point);
}
private boolean checkNeedDegrade(String businessKey, String sceneKey) {
if (StringUtils.isBlank(businessKey)) {
return false;
}
ServiceDegradeConfig config = appDegradeConfigMap.get(businessKey);
if (config.getState() == null) {
return false;
}
return config.getState() || (StringUtils.isNotBlank(sceneKey) && Optional.ofNullable(config.getSceneState())
.map(m -> m.get(sceneKey)).orElse(false));
}
@Around("@within(org.degrade.spring.boot.Degrade)")
public Object degrade(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//方法上的降级注解优先于类上的
Degrade degrade = AnnotationUtils.findAnnotation(signature.getMethod(), Degrade.class);
if (Objects.isNull(degrade)) {
degrade = AnnotationUtils.findAnnotation(joinPoint.getTarget().getClass(), Degrade.class);
}
Assert.notNull(degrade, "@Degrade must not be null!");
return doDegrade(joinPoint, degrade);
}
}
复制代码
starter 里 Bean 的发现与注册
META-INF 目录下的 spring.factories 文件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.degrade.spring.boot.DegradeAutoConfiguration
复制代码
降级功能使用
例如针对 app 应用里面里程碑的一个活动功能进行降级,当该活动出现问题时,通过配置开关打开降级。即可不发送里程碑活动的相关信息,从而保证核心链路的正常访问,不影响用户的核心学习功能
@Degrade(businessKey = "milestone", sceneKey = "app", result = @DegradeResult(resultType = DegradeResultEnum.DEFAULT_VALUE))
public void sendAppNotifyTemporary(ChallengeActivityMessageParam param) {
//具体业务省略
}
复制代码
总结
本文讲解了服务降级的概念,并通过实际项目中的一个降级组件设计例子,从 0 到 1 实现了一个 starter。
另外通过对不同业务场景的配置,我们的降级组件不仅可以对系统内部服务做降级,还可以针对外部的一些依赖服务做没有阈值的手动熔断操作。如结合限流组件的一些阈值指标下,发现外部服务出故障,即可手动配置降级组件,实现针对外部服务的一个简单熔断。
好了,关于服务降级我们就介绍到这里,喜欢本文的朋友,欢迎点赞和关注哦~~
作者:程序员小灰
链接:https://juejin.cn/post/7210610753777696805
来源:稀土掘金
评论