背景
在系统运行时,为了保证核心服务能正常提供服务,不至于因为一些非核心功能而阻塞服务,需要 对服务进行分级。当非核心服务影响到核心服务时,能通过配置或者其他手段快速切断非核心服务从而保证核心服务能正常对用户提供服务。
如何切断非核心服务呢?常用的方法有 限流、 熔断、 降级,市面上也有很多的组件能提供相应的功能,这些组件都提供了很多强大的功能,但引入这些开源组件的同时也会带来一些复杂的配置以及学习成本,另外公司微服务是dubbo构建的,引入spring-cloud的一些组件会比较复杂。
基于此,我们决定自研一个降级组件,集成到公司的各个服务里面,提供最基础的 降级服务。
服务故障的场景:
服务故障分为 接口级故障和 系统级故障
系统级的故障:例如,机器宕机、机房故障、网络故障等问题,这些系统级的故障虽然影响很大,但发生概率较小。
接口级故障:在实际业务运行过程中,该故障影响可能没有系统级那么大,但发生的概率较高
接口级故障的典型表现就是系统并没有宕机,网络也没有中断,但业务却出现问题了。
例如,
业务响应缓慢、大量访问超时、大量访问出现异常
,这类问题的主要原因在于 系统压力太大、负载太高,导致无法快速处理业务请求,由此引发更多的后续问题。
例如,最常见的 数据库慢查询将数据库的服务器资源耗尽,导致读写超时,业务读写数据库时要么无法连接数据库、要么超时,最终用户看到的现象就是访问很慢,一会访问抛出异常,一会访问又是正常结果。接口故障如果处理不及时,严重的时候甚至会引起系统级故障。如数据库慢查询导致数据库cpu升高,查询的服务短时间内频繁fullgc,并因此形成连锁反应,牵一发而动全身,依赖该该服务的其他服务全都不可用,蝴蝶效应引起核心服务的不可用
故障应对策略
优先保证核心业务和优先保证绝大部分用户
降级
降级指系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。**降级的核心思想就是丢车保帅,优先保证核心业务。**例如,对于教育类App学习主链路是核心服务,其他的各种礼品活动弹窗,老师点评服务等如果出问题后不应该影响主学习链路,这时可以停掉这些非核心服务。常见的实现降级的方式有:
系统后门(配置)降级
为每一个可降级服务提供一个业务开关配置,在业务出现故障后通过切换业务开关配置进行手动降级,但主要缺点是如果服务器数量多,需要一台一台去操作,效率比较低,这在故障处理争分夺秒的场景下是比较浪费时间的。
![](https://i-blog.csdnimg.cn/blog_migrate/ca5e4699200c200a17f4a78f81031db5.png)
独立降级系统
为了解决系统后门降级方式的缺点,将降级操作独立到一个单独的系统中,可以实现复杂的权限管理、批量操作等功能,但引入独立系统运维,集成等复杂度会相应提高 Hystrix,sentinel等都有相应功能实现
熔断
熔断和降级是两个比较容易混淆的概念,因为单纯从名字上看好像都有禁止某个功能的意思,但其实内在含义是不同的,原因在于降级的目的是应对系统自身的故障,而熔断的目的是应对依赖的外部系统故障的情况。
假设一个这样的场景:A 服务的 X 功能依赖 B 服务的某个接口,当 B 服务的接口响应很慢的时候,A 服务的 X 功能响应肯定也会被拖慢,进一步导致 A 服务的线程都被卡在 X 功能处理上,此时 A 服务的其他功能都会被卡住或者响应非常慢。这时就需要熔断机制了,即:A 服务不再请求 B 服务的这个接口,A 服务内部只要发现是请求 B 服务的这个接口就立即返回错误,从而避免 A 服务整个被拖慢甚至拖死。
熔断机制实现的关键是需要有一个统一的 API 调用层
由 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种降级策略
抛出异常
取指定的默认值
调用指定方法
取apollo上配置的值 apollo相关请参考分布式配置中心 Apollo
@Configuration@ConditionalOnProperty(name = {"degrade.enabled"},matchIfMissing = true)public class DegradeAutoConfiguration {@Bean@ConditionalOnMissingBeanpublic ServiceDegradeAspect createDegradeAspect() {//降级切面核心逻辑return new ServiceDegradeAspect();}@Bean(name = "CALL_METHOD")@ConditionalOnMissingBeanpublic CallMethodHandler createCallMethodHandler(){//调用指定方法降级return new CallMethodHandler();}@Bean(name = "DEFAULT_VALUE")@ConditionalOnMissingBeanpublic DefaultValueHandler createDefaultValueHandler(){//取指定的默认值降级return new DefaultValueHandler();}@Bean(name = "FETCH_CONFIG_VALUE")@ConditionalOnMissingBeanpublic FetchConfigValueHandler createFetchConfigValueHandler(){//取apollo上配置的值降级return new FetchConfigValueHandler();}@Bean(name = "THROW_EXCEPTION")@ConditionalOnMissingBeanpublic ThrowExceptionHandler createThrowExceptionHandler(){//抛出异常降级return new ThrowExceptionHandler();}@Bean@ConditionalOnMissingBeanpublic NullValueProvider createNullValueProvider(){return new NullValueProvider();}}
自定义降级注解
降级注解里面标识了需要降级的业务,场景降级后的结果,降级结果就是核心,支持四种策略的配置,所以降级结果的配置也是放在注解里的
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Degrade {
/**
* 降级业务key
*/StringbusinessKey();
/**
* 降级场景key
*/StringsceneKey() defaultStringUtils.EMPTY;
/**
* 降级后的结果(支持多种降级策略)
*/DegradeResultresult();
}
复制代码
降级结果的注解
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;
}
classNullValueProviderimplementsDegradeValueProvider<Void> {
@Override
public Void buildDegradeValue() {
returnnull;
}
}
复制代码
降级配置
组件采用了从apollo上获取业务配置的方式来进行降级,与Apollo耦合比较严重。如果不想采用apollo配置的方式进行业务降级配置,可以采用@ConfigurationProperties把配置在yml或者properties配置文件中的配置参数信息封装到配置的bean里,一般结合@EnableConfigurationProperties注解使用
@Data
publicclass ServiceDegradeConfig implements Serializable {
privatestatic final long serialVersionUID = -1628960982004214364L;
/**
* 降级总开关状态:true-全局开启服务降级;false-全局关闭服务降级
*/
privateBoolean state;
/**
* 场景开关
*/
private Map<String, Boolean> sceneState;
}
复制代码
降级处理器
采用策略模式,定义降级处理逻辑,具体的降级策略实现该接口即可,提供可扩展性的降级策略
public interface DegradeHandler {
/**
* 降级处理
*
* @return 处理后的结果
*/Object doDegrade(Degrade degrade, ProceedingJoinPoint point);
}
复制代码
1.调用指定方法降级策略
@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;
}
}
复制代码
2.获取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);
}
}
复制代码
3.提供默认返回值的降级处理策略(常用)
publicclassDefaultValueHandlerimplementsDegradeHandler {
@Autowired
private ApplicationContext applicationContext;
@Override@SuppressWarnings("rawtypes")
public Object doDegrade(Degrade degrade, ProceedingJoinPoint point) {
DegradeResultresult = degrade.result();
Class<? extendsDegradeValueProvider> providerClass = result.defaultValueProvider();
//获取指定的默认返回值构造提供者进行默认值构建并返回DegradeValueProviderprovider = applicationContext.getBean(providerClass);
returnprovider.buildDegradeValue();
}
}
复制代码
提供默认返回值的降级处理策略比较常用,但是返回值的类型很多,组件默认提供返回null对象的返回值,但业务上存在其他对象,如Boolean,以及自定义的复杂对象等,因此这里提供了默认返回值提供者函数式接口方便扩展
@FunctionalInterface
publicinterfaceDegradeValueProvider<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 ObjectdoDegradeAction(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 Configureorg.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。
另外通过对不同业务场景的配置,我们的降级组件不仅可以对系统内部服务做降级,还可以针对外部的一些依赖服务做没有阈值的手动熔断操作。如结合限流组件的一些阈值指标下,发现外部服务出故障,即可手动配置降级组件,实现针对外部服务的一个简单熔断。
好了,关于服务降级我们就介绍到这里,喜欢本文的朋友,欢迎点赞和关注哦~~
作者:程序员小灰