您当前的位置: 首页 > 

凌云时刻

暂无认证

  • 0浏览

    0关注

    1437博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Sentinel 实战应用中的小技巧

凌云时刻 发布时间:2020-10-11 15:30:00 ,浏览量:0

 

凌云时刻 · 技术

导读:阿里云上目前已经有 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 接口,如图,以下均以 TImeOutFacadecreateTimeOut 为例,我们可以按照图中的方式进行流控设置:

即当单机的 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 年数据湖高峰会议

云时代的智能运维平台,助力企业创新迭代

从数据治理、数据资产管理,到数据中台的落地实战!

长按扫描二维码关注凌云时刻

每日收获前沿技术与科技洞见

关注
打赏
1663816507
查看更多评论
立即登录/注册

微信扫码登录

0.0652s