sentinel自定义统一限流降级处理
在使用sentinel进行限流降级处理时,sentinel在发生blockException时默认返回仅仅是一句Blocked by Sentinel (flow limiting),而我们大部分的应用一般都会统一返回一个固定json格式的数据,因此需要对spring-cloud-starter-alibaba-sentinel中的部分源码进行一些扩展来满足需求。
1.依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-apollo</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2.配置
在apollo控制台中修改sentinel.flowRules及sentinel.degradeRules并发布,可以动态修改限流降级规则
连接sentinel-dashboard,非必须
spring.cloud.sentinel.transport.dashboard=http://127.0.0.1:8829
#配置apollo作为规则存储的数据源
#流控规则key设置
spring.cloud.sentinel.datasource.ds.apollo.namespace-name = application
spring.cloud.sentinel.datasource.ds.apollo.flow-rules-key = sentinel.flowRules
spring.cloud.sentinel.datasource.ds.apollo.default-flow-rule-value = test
spring.cloud.sentinel.datasource.ds.apollo.rule-type=flow
#降级规则key设置
spring.cloud.sentinel.datasource.ds1.apollo.namespace-name = application
spring.cloud.sentinel.datasource.ds1.apollo.flow-rules-key = sentinel.degradeRules
spring.cloud.sentinel.datasource.ds1.apollo.default-flow-rule-value = test
spring.cloud.sentinel.datasource.ds1.apollo.rule-type=degrade
#每个字段的具体意思参考sentinel官网
sentinel.flowRules = [{"strategy":0,"grade":1,"controlBehavior":0,"resource":"POST:http://demo-service/test","limitApp":"default","count":3}]
sentinel.degradeRules = [{"resource":"POST:http://demo-service/test","limitApp":"default","grade":2,"count":4,"timeWindow":12,"strategy":0,"controlBehavior":0,"clusterMode":false}]
#添加sentinel拦截器,拦截所有url作为埋点资源,关闭后则不能对server端进行降级限流。默认为true
spring.cloud.sentinel.filter.enabled=true
#自定义sentinel feign限流开关,默认为true
creis.feign.sentinel.enabled=true
#关闭懒加载,体现在项目启动后dashboard就能看到
spring.cloud.sentinel.eager=true
3.sentinel-dashboard安装
参考官网
4.源码改造
1.server端统一限流降级返回值
-
原理
这里给出server端限流原理的源码查看流程,可以看出spring-cloud-starter-alibaba-sentinel中自动装配了拦截器来拦截所有http请求,最终的异常处理类是BlockExceptionHandler。
SentinelWebAutoConfiguration ->SentinelWebInterceptor->AbstractSentinelInterceptor->BaseWebMvcConfig->BlockExceptionHandler
sentinel给了一个默认实现类,这也就是我们看到的"Blocked by Sentinel (flow limiting)"。
public class DefaultBlockExceptionHandler implements BlockExceptionHandler { public DefaultBlockExceptionHandler() { } public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { response.setStatus(429); StringBuffer url = request.getRequestURL(); if ("GET".equals(request.getMethod()) && StringUtil.isNotBlank(request.getQueryString())) { url.append("?").append(request.getQueryString()); } PrintWriter out = response.getWriter(); out.print("Blocked by Sentinel (flow limiting)"); out.flush(); out.close(); } }
-
改造
定义自己的异常处理类并加入的spring容器中
@Component public class MyBlockExceptionHandler implements BlockExceptionHandler { private BlockExceptionUtil blockExceptionUtil; public MyBlockExceptionHandler(BlockExceptionUtil blockExceptionUtil) { this.blockExceptionUtil = blockExceptionUtil; } public MyBlockExceptionHandler() { } @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception { BaseDtoResponse baseDtoResponse = blockExceptionUtil.getResponseDto(e, null); httpServletResponse.setStatus(HttpStatus.OK.value()); httpServletResponse.setCharacterEncoding("UTF-8"); httpServletResponse.setContentType("application/json;charset=utf-8"); httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8"); new ObjectMapper().writeValue(httpServletResponse.getWriter(),baseDtoResponse); } }
2.restTemplate统一限流降级返回值
sentinel官方给了一个@SentinelRestTemplate 用以支持对 RestTemplate 的服务调用使用 Sentinel 进行保护,核心代码在org.springframework.cloud.alibaba.sentinel.custom.SentinelBeanPostProcessor中,实现了MergedBeanDefinitionPostProcessor接口,MergedBeanDefinitionPostProcessor接口实现了BeanPostProcessor接口。核心方法就是重写的postProcessMergedBeanDefinition和postProcessAfterInitialization来对原生的RestTemplate 进行增强。如果想要对其统一自定义返回值处理进行会相对复杂,因此这里采取另一种手段,对@SentinelResource做一些改造来实现。
-
原理
SentinelAutoConfiguration ->SentinelResourceAspect
-
改造
继承SentinelResourceAspect重写其默认处理
@Component public class MySentinelResourceAspect extends SentinelResourceAspect { @Resource private BlockExceptionUtil blockExceptionUtil; @Override protected Object handleDefaultFallback(ProceedingJoinPoint pjp, String defaultFallback, Class<?>[] fallbackClass, Throwable e) throws Throwable { Method method = resolveMethod(pjp); if (StringUtil.isBlank(defaultFallback)) { //默认处理逻辑 Class<?> returnType = method.getReturnType(); BaseDtoResponse baseDtoResponse = blockExceptionUtil.getResponseDto(e, returnType); return baseDtoResponse; }else{ return super.handleDefaultFallback(pjp, defaultFallback, fallbackClass, e); } } }
3.feign统一限流降级返回值
sentinel对feign进行了增强,利用动态代理生成增强类来进行流控降级
-
原理
SentinelFeignAutoConfiguration ->SentinelFeign ->SentinelInvocationHandler
-
改造
@Bean @Scope("prototype") @ConditionalOnClass({SphU.class, Feign.class}) @ConditionalOnMissingBean @ConditionalOnProperty(name = {"creis.feign.sentinel.enabled"},matchIfMissing = true) @Primary public Feign.Builder feignSentinelBuilder() { return CloudSentinelFeign.builder(); }
public class CloudSentinelFeign { private CloudSentinelFeign() { } public static CloudSentinelFeign.Builder builder() { return new CloudSentinelFeign.Builder(); } public static final class Builder extends Feign.Builder implements ApplicationContextAware { private Contract contract = new Contract.Default(); private ApplicationContext applicationContext; private FeignContext feignContext; public Builder() { } @Override public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) { throw new UnsupportedOperationException(); } @Override public CloudSentinelFeign.Builder contract(Contract contract) { this.contract = contract; return this; } @Override public Feign build() { super.invocationHandlerFactory(new InvocationHandlerFactory() { @Override public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { Object feignClientFactoryBean = CloudSentinelFeign.Builder.this.applicationContext.getBean("&" + target.type().getName()); Class fallback = (Class) CloudSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "fallback"); Class fallbackFactory = (Class) CloudSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "fallbackFactory"); String beanName = (String) CloudSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "contextId"); if (!StringUtils.hasText(beanName)) { beanName = (String) CloudSentinelFeign.Builder.this.getFieldValue(feignClientFactoryBean, "name"); } if (Void.TYPE != fallback) { Object fallbackInstance = this.getFromContext(beanName, "fallback", fallback, target.type()); return new CloudSentinelInvocationHandler(target, dispatch, new FallbackFactory.Default(fallbackInstance)); } else if (Void.TYPE != fallbackFactory) { FallbackFactory fallbackFactoryInstance = (FallbackFactory)this.getFromContext(beanName, "fallbackFactory", fallbackFactory, FallbackFactory.class); return new CloudSentinelInvocationHandler(target, dispatch, fallbackFactoryInstance); } else { return new CloudSentinelInvocationHandler(target, dispatch); } } private Object getFromContext(String name, String type, Class fallbackType, Class targetType) { Object fallbackInstance = CloudSentinelFeign.Builder.this.feignContext.getInstance(name, fallbackType); if (fallbackInstance == null) { throw new IllegalStateException(String.format("No %s instance of type %s found for feign client %s", type, fallbackType, name)); } else if (!targetType.isAssignableFrom(fallbackType)) { throw new IllegalStateException(String.format("Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", type, fallbackType, targetType, name)); } else { return fallbackInstance; } } }); super.contract(new SentinelContractHolder(this.contract)); return super.build(); } private Object getFieldValue(Object instance, String fieldName) { Field field = ReflectionUtils.findField(instance.getClass(), fieldName); field.setAccessible(true); try { return field.get(instance); } catch (IllegalAccessException var5) { return null; } } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; this.feignContext = (FeignContext)this.applicationContext.getBean(FeignContext.class); } } }
public class CloudSentinelInvocationHandler implements InvocationHandler { private final Target<?> target; private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch; private FallbackFactory fallbackFactory; private Map<Method, Method> fallbackMethodMap; CloudSentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory fallbackFactory) { this.target = (Target) Util.checkNotNull(target, "target", new Object[0]); this.dispatch = (Map)Util.checkNotNull(dispatch, "dispatch", new Object[0]); this.fallbackFactory = fallbackFactory; this.fallbackMethodMap = toFallbackMethod(dispatch); } CloudSentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) { this.target = (Target)Util.checkNotNull(target, "target", new Object[0]); this.dispatch = (Map)Util.checkNotNull(dispatch, "dispatch", new Object[0]); } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return this.equals(otherHandler); } catch (IllegalArgumentException var21) { return false; } } else if ("hashCode".equals(method.getName())) { return this.hashCode(); } else if ("toString".equals(method.getName())) { return this.toString(); } else { InvocationHandlerFactory.MethodHandler methodHandler = (InvocationHandlerFactory.MethodHandler)this.dispatch.get(method); Object result; if (!(this.target instanceof Target.HardCodedTarget)) { result = methodHandler.invoke(args); } else { Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget)this.target; MethodMetadata methodMetadata = (MethodMetadata) SentinelContractHolder.METADATA_MAP.get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method)); if (methodMetadata == null) { result = methodHandler.invoke(args); } else { String resourceName = methodMetadata.template().method().toUpperCase() + ":" + hardCodedTarget.url() + methodMetadata.template().path(); Entry entry = null; Object var12; try { Throwable ex; try { ContextUtil.enter(resourceName); entry = SphU.entry(resourceName, EntryType.OUT, 1, args); result = methodHandler.invoke(args); return result; } catch (Throwable var22) { ex = var22; if (!BlockException.isBlockException(var22)) { Tracer.trace(var22); } } if (this.fallbackFactory == null) { //加入默认处理逻辑 BlockExceptionUtil blockExceptionUtil = (BlockExceptionUtil) AppContextUtil.getBean("blockExceptionUtil"); return blockExceptionUtil.getResponseDto(ex, method.getReturnType()); } try { Object fallbackResult = ((Method)this.fallbackMethodMap.get(method)).invoke(this.fallbackFactory.create(ex), args); var12 = fallbackResult; } catch (IllegalAccessException var19) { throw new AssertionError(var19); } catch (InvocationTargetException var20) { throw new AssertionError(var20.getCause()); } } finally { if (entry != null) { entry.exit(1, args); } ContextUtil.exit(); } return var12; } } return result; } } @Override public boolean equals(Object obj) { if (obj instanceof SentinelInvocationHandler) { CloudSentinelInvocationHandler other = (CloudSentinelInvocationHandler)obj; return this.target.equals(other.target); } else { return false; } } @Override public int hashCode() { return this.target.hashCode(); } @Override public String toString() { return this.target.toString(); } static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) { Map<Method, Method> result = new LinkedHashMap(); Iterator var2 = dispatch.keySet().iterator(); while(var2.hasNext()) { Method method = (Method)var2.next(); method.setAccessible(true); result.put(method, method); } return result; } }
5.使用
在加入限流降级机制后调用方一定要考虑被调用方法的返回值有可能是限流降级后的返回结果,后续对这个结果做处理时需进行判断
server端
使用方式
sentinel会抓取所有请求过的url作为埋点资源,可以在dashboard上对其做限流降级处理,需要注意的是通过dashboard增加的规则并不会持久化(sentinel-dashboard与apollo没有通信机制),*如果发现server端降级并不会生效,则检查自己是否在接口层做了异常处理逻辑**(因为sentinel的异常数统计是在拦截器中做的,导致统计不到异常)
自定义限流降级逻辑
在接口入口处使用@SentinelResource即可,参考下方restTemplate方式自定义限流降级逻辑
client端
restTemplate方式
使用方式
在发起restTemplate调用的方法上添加 @SentinelResource即可,此时限流降级走默认逻辑
@Component
public class DemoService {
@Resource
private ServiceRestTemplate restTemplate;
@SentinelResource
public DtoResponse<DemoInfoDto> getDemoInfo(CurrentBoardInfoBo bo) {
ResponseEntity<DtoResponse<DemoInfoDto>> entity = restTemplate.servicePost("http://demo-service/test", bo, new ParameterizedTypeReference<DtoResponse<DemoInfoDto>>() {
});
if(entity!= null && entity.getBody()!= null){
return entity.getBody();
}
return null;
}
}
自定义限流降级逻辑
- 第一种方式
在原来中blockHandler方法,入参为原方法入参的基础上加上异常类,注意blockHandler方法的参数必须同原方法一致,异常类参数也必须为BlockException exception;fallbackClass同理,注意异常参数为Throwable throwable。
@Component
public class DemoService {
@Resource
private ServiceRestTemplate restTemplate;
@SentinelResource(value = "sentinel01", blockHandler = "blockHandler" ,fallback = "fackback" )
public DtoResponse<DemoInfoDto> getDemoInfo(CurrentBoardInfoBo bo) {
DtoResponse<DemoInfoDto> dtoDtoResponse = restTemplate.servicePostDtoResponse("http://demo-service/test", bo, DemoInfoDto.class);
return dtoDtoResponse;
}
public DtoResponse<DemoInfoDto> blockHandler(CurrentBoardInfoBo bo, BlockException exception){
DtoResponse<DemoInfoDto> dtoDtoResponse = new DtoResponse<>();
//限流逻辑,例如返回固定值或者从redis等拿缓存等来应对大流量
DemoInfoDto DemoInfoDto = new DemoInfoDto();
DemoInfoDto.setVersion("v3");
dtoDtoResponse.success(DemoInfoDto);
return dtoDtoResponse;
}
public DtoResponse<DemoInfoDto> fackback(CurrentBoardInfoBo bo, Throwable throwable){
DtoResponse<DemoInfoDto> dtoDtoResponse = new DtoResponse<>();
//降级逻辑,例如返回固定值或者从redis等拿缓存等来应对下游服务不可用
DemoInfoDto DemoInfoDto = new DemoInfoDto();
DemoInfoDto.setVersion("v3");
dtoDtoResponse.success(DemoInfoDto);
return dtoDtoResponse;
}
}
- 第二种方式
限流降级逻辑放到专门的处理类中,注意此时方法需为静态的,
@Component
public class DemoService {
@Resource
private ServiceRestTemplate restTemplate;
@SentinelResource(value = "sentinel01", blockHandler = "blockHandler" , blockHandlerClass = IndustryblockHandler.class,fallback = "fackback",fallbackClass = DemofallbackHandler.class)
public DtoResponse<DemoInfoDto> getDemoInfo(CurrentBoardInfoBo bo) {
DtoResponse<DemoInfoDto> dtoDtoResponse = restTemplate.servicePostDtoResponse("http://demo-service/test", bo, DemoInfoDto.class);
return dtoDtoResponse;
}
}
public class DemoblockHandler {
public static DtoResponse<DemoInfoDto> blockHandler(CurrentBoardInfoBo bo, BlockException exception){
DtoResponse<DemoInfoDto> dtoDtoResponse = new DtoResponse<>();
//限流逻辑,例如返回固定值或者从redis等拿缓存等来应对大流量
DemoInfoDto DemoInfoDto = new DemoInfoDto();
DemoInfoDto.setVersion("v3");
dtoDtoResponse.success(DemoInfoDto);
return dtoDtoResponse;
}
}
public class DemofallbackHandler {
public static DtoResponse<DemoInfoDto> fackback(TestInfoBo bo, Throwable throwable){
DtoResponse<DemoInfoDto> dtoDtoResponse = new DtoResponse<>();
//降级逻辑,例如返回固定值或者从redis等拿缓存等来应对下游服务不可用
DemoInfoDto DemoInfoDto = new DemoInfoDto();
DemoInfoDto.setVersion("v3");
dtoDtoResponse.success(DemoInfoDto);
return dtoDtoResponse;
}
}
feign方式
使用方式
与原来集成方式相同,此时限流降级走默认逻辑
@FeignClient(value = "demo-service")
public interface DemoApi {
@PostMapping(value = "/test", produces = "application/json")
DtoResponse<DemoInfoDto> getDemoInfo(@RequestBody DemoInfoBo bo);
}
默认逻辑返回格式
同restTemplate
自定义限流降级规则
-
第一种方式
IndustryDimensionApiFallBack类需在spring容器中存在
@FeignClient(value = "demo-service" ,fallback = DemoApiFallBack.class)
public interface DemoApi {
@PostMapping(value = "/test", produces = "application/json")
DtoResponse<DemoInfoDto> getDemoInfo(@RequestBody DemoInfoBo bo);
}
@Component
public class DemoApiFallBack implements DemoApi {
@Override
public DtoResponse<DemoInfoDto> getDemoInfo(DemoInfoBo bo) {
DtoResponse dtoResponse = new DtoResponse();
DemoInfoDto DemoInfoDto = new DemoInfoDto();
DemoInfoDto.setVersion("v3");
return dtoResponse.success(DemoInfoDto);
}
}
- 第二种方式
@FeignClient(value = "demo-service" ,fallbackFactory = DemofallbackFactory.class)
public interface DemoApi {
@PostMapping(value = "/test", produces = "application/json")
DtoResponse<DemoInfoDto> getDemoInfo(@RequestBody DemoInfoBo bo);
}
@Component
public class DemofallbackFactory implements FallbackFactory<DemoApiFallBack> {
@Override
public DemoApiFallBack create(Throwable cause) {
return new DemoApiFallBack();
}
}
public class DemoApiFallBack implements DemoApi {
@Override
public DtoResponse<DemoInfoDto> getDemoInfo(DemoInfoBo bo) {
DtoResponse dtoResponse = new DtoResponse();
DemoInfoDto DemoInfoDto = new DemoInfoDto();
DemoInfoDto.setVersion("v3");
return dtoResponse.success(DemoInfoDto);
}
}