凌云时刻 · 技术
导读:阿里云上目前已经有 Sentinel,对应阿里云产品为 AHAS。对于部署在阿里云 region、OXS 等区域的内部用户,可方便的迁移做迁移。
作者 | 奋氛
来源 | 凌云时刻(微信号:linuxpk)
Sentinel 的介绍
Sentinel是对资源调用的控制组件,主要涵盖 授权
、限流
、降级
、调用统计
等功能模块,也就是我们常用的限流工具,它的接入是很简单的。
HSF 的接入:
在Pandora中,只需要在pom.xml
的(注意不是
/
)下添加依赖(无须写版本),然后直接导入 sentinel 原生的 xml 配置即可(sentinel-tracer.xml
来自 sentinel 自身的 jar 包)。
@SpringBootApplication
@ImportResource({ "classpath*:sentinel-tracer.xml" })
public class Application
HSF的接入验证方式 http://localhost:7002/sentinel
命令
curl http://localhost:8719/tree?type=root
查看 sentinal 日志
/home/admin/logs/csp/sentinel-block.log
注解方式接入需要引入 Spring AOP 并额外引入依赖(最新特性仅支持 3.9.20+ 版本):
com.taobao.csp
sentinel-annotation-aspectj
3.9.20
然后 Spring AOP 中通过配置的方式将 SentinelResourceAspect
注册为一个 Spring Bean:
@Configuration
public class SentinelAspectConfiguration {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
Sentinel 在 HSF 的 API 层的限流设置
Sentinel 导入的 jar 包采用 SPI 的方式已经对 HSF 的入口即 API 层进行了接入,访问接口可见,如图:
当用户发送 HSF 请求,即可看到我们请求的 API 接口,如图,以下均以 TImeOutFacade
的 createTimeOut
为例,我们可以按照图中的方式进行流控设置:
即当单机的 QPS 超过 1 时,就会触发流量控制,正常返回数据结果为:
{
"traceId": "0.2::1e27979f15827285478321829e",
"innerCode": "LARK-F000-000-0-P000",
"data": {
"class": "com.alipic.toc.api.domain.TimeOutDTO"
},
"success": true,
"extra": {},
"resultCode": "SUCCESS",
"class": "com.alipic.common.result.PlainResult",
"dataPresent": true,
"resultMsg": "成功"
}
当触发限流是,则是抛出异常,被系统上层捕获后返回结果为:
{
"localizedMessage": "SentinelBlockException",
"cause": null,
"stackTrace": [
{
"fileName": "BlockException",
"nativeMethod": false,
"methodName": "block",
"className": "com.alibaba.csp.sentinel.slots.block.BlockException",
"lineNumber": 0,
"class": "java.lang.StackTraceElement"
}
],
"suppressed": [],
"message": "SentinelBlockException",
"class": "java.lang.RuntimeException"
}
对于用户来说,这样的返回结果肯定是不友好的,因为不是按照约定数据格式进行返回的。所以我们需要对流控进行处理,这里我们先简单处理一下,在刚才的 SentinelAspectConfiguration.java
中添加代码,如下:
@Configuration
public class SentinelAspectConfiguration {
private static final String TIME_OUT_API_PATH = "com.alipic.toc.api.TimeOutFacade";
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
// HSF fallback配置示例,全局配置一次即可。
HsfFallbackRegistry.setProviderFallback((invocation, blockException) -> {
if (invocation.getTargetServiceUniqueName().contains(TIME_OUT_API_PATH)) {
//日志统计
MonitorLog.addStat(MonitorParams.TOC_API_MONITOR_NAME, invocation.getMethodName() + MonitorParams.FAILURE_SENTINEL_KEY, null, 1, 0);
invocation.getReturnClass();
return getApiResult(invocation, MonitorParams.SENTINEL_BLOCK_CODE, blockException);
}
//非API直接抛异常
throw BlockException.THROW_OUT_EXCEPTION;
});
return new SentinelResourceAspect();
}
/**
* @param invocation the Hsf invocation, contains all info about current remote method call.
* @param code 错误码
* @param ex 异常
* @param args 参数
* @return
* @throws Exception
*/
private Object getApiResult(Invocation invocation, String code, Exception ex, Object... args) throws Exception {
if (invocation.getReturnClass() == null) {
return null;
}
try {
//获取fail方法,并执行
Method method = invocation.getReturnClass().getMethod("fail", ErrorMessage.class);
Object obj = invocation.getReturnClass().newInstance();
method.invoke(obj, obj, ErrorMessage.code(code, args));
LoggerTemplate.error(BaseTemplate.CallId.create(this, "getApiResult"), "method:{}-{}", obj.getClass().getSimpleName(), JSON.toJSON(obj));
return obj;
} catch (NoSuchMethodException e) {
throw ex;
}
}
}
在系统启动的时候,使用 HsfFallbackRegistry.setProviderFallback
注册一个方法,判断是 API 层的流控,返回一个标准异常输出结果的对象,再次测试,返回结果如图:
{
"traceId": "0.2::1e27038515827919991162585e",
"innerCode": "SENTINEL_BLOCK",
"data": null,
"success": false,
"extra": {},
"resultCode": "LARK-F114-000-4-B429",
"class": "com.alipic.common.result.PlainResult",
"dataPresent": false,
"resultMsg": "流量限制"
}
这样一个标准的限流结果就 OK 了。
Sentinel 对不同的业务参数进行限时
如图:
我们需要对第 0 个参数索引进行热点控制,那么第 0 个参数索引是什么呢?
public PlainResult createTimeOut(TimeOutRequest request)
难道是 TimeOutRequest
对象吗?不是的,注意热点参数只支持基本类型和 String 类型,并且不支持 null 值。所以,我们这个是不会生效的,因为我们的参数不是基础数据类型,咋整,这要都拆成基础数据类型,成本可就相当高了。所以我们针对这种情况,做一个自定义注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SentinelAnnotation {
Class paramAClass() default Object.class;
String paramAField() default "";
Class paramBClass() default Object.class;
String paramBField() default "";
}
按照自定义的注解 SentinelAnnotation
在刚才的方法上加上自定义注解:
@SentinelAnnotation(paramAClass = TimeOutRequest.class, paramAField = "bizId",paramBClass = TimeOutRequest.class,paramBField = "bizType")
public PlainResult createTimeOut(TimeOutRequest request) {
我们将 TimeOutRequest
类的 bizId 和 bizType 分别做第一个和第二个参数,然后我们再看注解的实现:
package com.alipic.toc.common.current.handler;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alipic.common.result.BaseResult;
import com.alipic.common.result.code.ErrorMessage;
import com.alipic.common.utils.BaseTemplate;
import com.alipic.common.utils.LoggerTemplate;
import com.alipic.toc.common.config.MonitorParams;
import com.alipic.toc.common.current.SentinelAnnotation;
import com.alipic.toc.common.util.ReflexObjectUtil;
import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
/**
* 限流注解实现
*
* @author fengcheng.wfc
* @date 2020年01月27日
*/
@Component
@Aspect
@Order(100)
public class SentinelHandler {
private static final String TIME_OUT_API_PATH = "com.alipic.toc.api.impl";
@Pointcut("execution(* com.alipic.toc..*.*(..)))")
private void pointcut() {
}
@Around(value = "pointcut() && @annotation(sentinelAnnotation)")
public Object methodAround(ProceedingJoinPoint invocation, SentinelAnnotation sentinelAnnotation) throws Throwable {
// 2 检查是否含有session
Signature signature = invocation.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
String methodName = targetMethod.getDeclaringClass().getName() + "#"
+ targetMethod.getName();
List paramList = Lists.newArrayList();
Object[] args = invocation.getArgs();
//获取参数
for (Object obj : args) {
if (Objects.nonNull(obj) && obj.getClass().getName().equals(sentinelAnnotation.paramAClass().getName())) {
Object a = ReflexObjectUtil.getValueByKey(obj, sentinelAnnotation.paramAField());
paramList.add(a);
}
if (Objects.nonNull(obj) && obj.getClass().getName().equals(sentinelAnnotation.paramBClass().getName())) {
Object a = ReflexObjectUtil.getValueByKey(obj, sentinelAnnotation.paramBField());
paramList.add(a);
}
}
return getWithSphUHotspot(methodName, invocation, paramList.toArray());
}
private Object getWithSphUHotspot(String resourceName, ProceedingJoinPoint invocation, Object... args) throws Throwable {
Entry entry = null;
// 务必保证 finally 会被执行
try {
// 资源名可使用任意有业务语义的字符串,注意数目不能太多(超过 1K),超出几千请作为参数传入而不要直接作为资源名
// EntryType 代表流量类型(inbound/outbound),其中系统规则只对 IN 类型的埋点生效
entry = SphU.entry(resourceName, EntryType.IN, 1, args);
// 被保护的业务逻辑
return invocation.proceed();
} catch (BlockException ex) {
LoggerTemplate.error(BaseTemplate.CallId.create(this, resourceName), "Block Exception", ex);
//API层防止返回限流异常
if (resourceName.indexOf(TIME_OUT_API_PATH) == 0) {
return getApiResult(invocation, MonitorParams.SENTINEL_BLOCK_CODE, ex);
}
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
throw ex;
} catch (Exception ex) {
LoggerTemplate.error(BaseTemplate.CallId.create(this, resourceName), "System Exception", ex);
// 若需要配置降级规则,需要通过这种方式记录业务异常
Tracer.traceEntry(ex, entry);
//API层防止返回系统异常
if (resourceName.indexOf(TIME_OUT_API_PATH) == 0) {
return getApiResult(invocation, "ERROR", ex);
}
throw ex;
} finally {
// 务必保证 exit,务必保证每个 entry 与 exit 配对
if (entry != null) {
entry.exit();
}
}
}
private Object getApiResult(ProceedingJoinPoint invocation, String code, Exception ex, Object... args) throws Exception {
Signature signature = invocation.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
if (targetMethod.getReturnType() == null) {
return null;
}
//获取fail方法,并执行
Method method = targetMethod.getReturnType().getMethod("fail", BaseResult.class, ErrorMessage.class);
Object obj = targetMethod.getReturnType().newInstance();
method.invoke(obj, obj, ErrorMessage.code(code, args));
LoggerTemplate.error(BaseTemplate.CallId.create(this, "getApiResult"), "method:{}-{}", obj.getClass().getSimpleName(), JSON.toJSON(obj));
return obj;
}
}
我们首先将被注解的类和方法名组成 Sentinel 的 ResourName
(突然发现对重载没有考虑清楚,会有两个一样的 ResourceName
),然后将注解设置的参数 1 和参数 2 反射出来,调用 SphU.entry
,再调用的时候,我们在 Sentinel 中就能看到如图的条目:
这就是我们通过刚刚的注解来实现的一个流控。然后我们对它进行热点设置,如图:
我们对整体改方法的第一个参数机型热点设置,当 QPS 超过 100 是进行流量控制,然后添加例外项,当参数 1 的值为 1(String)时的阈值设置 1,即当调用改方法时,如果传入的参数 1(bizId)为 1 时,进行流量控制。然后回到 hsf 控制台,进行请求,我们会发现,当 bizId 不为 1 时,手工根本出发不了限流,当 bizType 的值为 1 时,能够轻易触发限流。因为我们对 API 层进行了特殊的处理,所以当注解使用到 API 层的时候,HSF 控制台能够返回标准的异常结果,如图:
{
"traceId": "0.2::1e27038515827919991162585e",
"innerCode": "SENTINEL_BLOCK",
"data": null,
"success": false,
"extra": {},
"resultCode": "LARK-F114-000-4-B429",
"class": "com.alipic.common.result.PlainResult",
"dataPresent": false,
"resultMsg": "流量限制"
}
这样,我们针对 API 层以对象为参数的流控基本上就实现了。业务层的使用是一样的,只需要在上层捕获 BlockException
异常进行处理即可。
更多 Sentinel 使用技巧欢迎点击文末“阅读原文”了解更多。
END
往期精彩文章回顾
洛神云网络 SLB 负载均衡新姿势
业务中台实践助力企业数字化转型
中保车服灾备云,为保险公司“上保险”
阿里云重磅发布应用负载均衡ALB,加速企业应用交付
“电”亮数字生活,阿里云助力南方电网智能调度
阿里云为自动驾驶量身打造一体化解决方案,助力行业突破技术瓶颈
一文读懂阿里云网络 2020 云栖大会新品发布
阿里云邀您参加 2020 年数据湖高峰会议
云时代的智能运维平台,助力企业创新迭代
从数据治理、数据资产管理,到数据中台的落地实战!
长按扫描二维码关注凌云时刻
每日收获前沿技术与科技洞见