阿里Sentinel 源码解析

前言

阿里Sentinel 是面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。

阿里Sentinel和Hystrix有类似的功能,本文先从使用和源码角度分析,后续再说对比。

首先导入pom文件,这里有两个版本:

如果springboot基于1.5的则导入以下内容:

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-annotation-aspectj</artifactId>
            <version>1.4.0</version>
        </dependency>
	<dependency>
		<groupId>com.alibaba.csp</groupId>
		<artifactId>sentinel-transport-simple-http</artifactId>
		<version>1.4.0</version>
	</dependency>

如果springboot基于2.0+的则导入以下内容:

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
			<version>0.2.0.RELEASE</version>
		</dependency>

两个内容都差不多,spring-cloud-starter-alibaba-sentinel更加智能化一些。本文以springboot 1.5为例。

阿里sentinel熔断主要是自己设定配置,包括流规则、降级规则,热点规则等。

添加配置信息类SentileConfig。

@Configuration
public class SentileConfig {
    @Value("${csp.sentinel.api.port}")
    private String port;
    @Value("${csp.sentinel.dashboard.server}")
    private String server;


    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }

    @PostConstruct
    private void initRules() throws Exception {
        System.setProperty("csp.sentinel.api.port",port);
        System.setProperty("csp.sentinel.dashboard.server",server);

        FlowRule rule1 = new FlowRule();
        rule1.setResource("flowtest");
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setCount(1);   // 每秒调用最大次数为 1 次

        List<FlowRule> rules = new ArrayList<>();
        rules.add(rule1);

        // 将控制规则载入到 Sentinel
        com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager.loadRules(rules);
    }
}

这里我将csp.sentinel.api.port和csp.sentinel.dashboard.server都配置到了类中,我们知道正常客户端启动时,需要加上连接sentinel server的参数,也就是在vm中启动时加上

-Dcsp.sentinel.dashboard.server=localhost:8848   -Dcsp.sentinel.api.port=8987。放到类中,我们就可以在application.yml中定义好了就可以了。springboot2.0+版本可以直接在application.yml中定义,springboot1.5的如果不想在启动参数中加,就需要加上        System.setProperty("csp.sentinel.api.port",port);
System.setProperty("csp.sentinel.dashboard.server",server);这两行代码。

配置类中定义了一个流控规则,名称为flowtest,方式为QPS,每秒最大次数为1次,之后将规则载入到Sentinel。

扫描二维码关注公众号,回复: 15688993 查看本文章

接着我们配置一个Controller。

@RestController
public class Controller {

    @RequestMapping("/hello")
    @SentinelResource(value = "flowtest",fallback = "error" ,blockHandler="blockHandler")
    public String index(@RequestParam String name) {
        return "hello,"+name+"   hashcode:"+this.hashCode();
    }

    public  String blockHandler(String a, BlockException e){
        return "time out!!!"+a;
    }

    public  String error(String a,String b){
        return "error out!!!";
    }
}

在RequestMapping请求中,添加了一个@SentinelResource注解,value=flowtest。这样就全都配置完成了。我们在请求的时候,根据刚才的设置,1s中只能有一个请求,否则就会触发熔断,调用blockHandler方法。

我们看到在配置类中,有一个SentinelResourceAspect的Bean对象,看一下这个类。

@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
    public SentinelResourceAspect() {
    }

    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        Method originMethod = this.resolveMethod(pjp);
        SentinelResource annotation = (SentinelResource)originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        } else {
            String resourceName = this.getResourceName(annotation.value(), originMethod);
            EntryType entryType = annotation.entryType();
            Entry entry = null;

            Object var8;
            try {
                entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs());
                Object result = pjp.proceed();
                var8 = result;
                return var8;
            } catch (BlockException var13) {
                var8 = this.handleBlockException(pjp, annotation, var13);
            } catch (Throwable var14) {
                Tracer.trace(var14);
                throw var14;
            } finally {
                if (entry != null) {
                    entry.exit();
                }

            }

            return var8;
        }
    }
}

可以看到这个类有@Aspect标识,并且切点是所有有@SentinelResource注解的方法。执行Around方法。方法中

entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs())是重点,进入方法。

    public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
        return Env.sph.entry(name, type, count, args);
    }

    //进入CtSph的entry方法
    public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
        StringResourceWrapper resource = new StringResourceWrapper(name, type);
        return this.entry(resource, count, args);
    }

    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        return this.entryWithPriority(resourceWrapper, count, false, args);
    }

    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {
        //获取上下文
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
            return new CtEntry(resourceWrapper, (ProcessorSlot)null, context);
        } else {
            if (context == null) {
                //创建一个默认的sentinel_context,里面包含EntranceNode,根据熔断规则名创建
                context = CtSph.MyContextUtil.myEnter("sentinel_default_context", "", resourceWrapper.getType());
            }
            //Constants.ON = true
            if (!Constants.ON) {
                return new CtEntry(resourceWrapper, (ProcessorSlot)null, context);
            } else {
                //创建链路
                ProcessorSlot<Object> chain = this.lookProcessChain(resourceWrapper);
                if (chain == null) {
                    return new CtEntry(resourceWrapper, (ProcessorSlot)null, context);
                } else {
                    CtEntry e = new CtEntry(resourceWrapper, chain, context);

                    try {
                        //进入每个chain规则
                        chain.entry(context, resourceWrapper, (Object)null, count, prioritized, args);
                    } catch (BlockException var9) {
                        e.exit(count, args);
                        throw var9;
                    } catch (Throwable var10) {
                        RecordLog.info("Sentinel unexpected exception", var10);
                    }

                    return e;
                }
            }
        }
    }

在创建链路方法中,进入chain = this.lookProcessChain(resourceWrapper)方法。

   ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
        ProcessorSlotChain chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
        //第一次进入为null
        if (chain == null) {
            Object var3 = LOCK;
            synchronized(LOCK) {
                chain = (ProcessorSlotChain)chainMap.get(resourceWrapper);
                if (chain == null) {
                    if (chainMap.size() >= 6000) {
                        return null;
                    }
                    //创建chain
                    chain = SlotChainProvider.newSlotChain();
                    Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap(chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }
        return chain;
    }

    public static ProcessorSlotChain newSlotChain() {
        if (builder != null) {
            return builder.build();
        } else {
            resolveSlotChainBuilder();
            if (builder == null) {
                RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default", new Object[0]);
                builder = new DefaultSlotChainBuilder();
            }

            return builder.build();
        }
    }

    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());
        return chain;
    }

可以看到增加了8种slot,我们创建的就是flowslot方法,基于流控规则。之后就会进入

chain.entry(context, resourceWrapper, (Object)null, count, prioritized, args)方法。

我们创建的是FlowSlot,进入FlowSlot.entry方法。

    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
        //进入检验方法
        this.checkFlow(resourceWrapper, context, node, count, prioritized);
        //进入下一个chain
        this.fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
        //获取之前载入到Sentinel的规则
        //即SentileConfig类中的, com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager.loadRules(rules);这句方法
        Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
        List<FlowRule> rules = (List)flowRules.get(resource.getName());
        if (rules != null) {
            Iterator var8 = rules.iterator();

            while(var8.hasNext()) {
                FlowRule rule = (FlowRule)var8.next();
                //进入校验
                if (!this.canPassCheck(rule, context, node, count, prioritized)) {
                    throw new FlowException(rule.getLimitApp());
                }
            }
        }
    }

    boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, int count, boolean prioritized) {
        return FlowRuleChecker.passCheck(rule, context, node, count, prioritized);
    }

    static boolean passCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
        String limitApp = rule.getLimitApp();
        if (limitApp == null) {
            return true;
        } else {
            //是否为集群模式,我们创建的是针对单实例,进入passLocalCheck
            return rule.isClusterMode() ? passClusterCheck(rule, context, node, acquireCount, prioritized) : passLocalCheck(rule, context, node, acquireCount, prioritized);
        }
    }

    private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) {
        Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
        return selectedNode == null ? true : rule.getRater().canPass(selectedNode, acquireCount);
    }

最后执行.canPass方法,canPass方法中有几种

我们使用的是默认的方法,还有按比例,温和启动等方式。 下面一个一个分析:

(一)DefaultController

    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        int curCount = this.avgUsedTokens(node);
        return (double)(curCount + acquireCount) <= this.count;
    }

    private int avgUsedTokens(Node node) {
        if (node == null) {
            return -1;
        } else {
            //grade模式 0=线程 1=活动窗口
            return this.grade == 0 ? node.curThreadNum() : (int)node.passQps();
        }
    }

如果1s内的请求数量小于设置的数量,则通过。

(二)RateLimiterController(漏桶)

   public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        //获取当前时间
        long currentTime = TimeUtil.currentTimeMillis();
        //计算单位时间内每一次请求的花费时间
        long costTime = Math.round(1.0D * (double)acquireCount / this.count * 1000.0D);
        //这次请求预计花费的时间
        long expectedTime = costTime + this.latestPassedTime.get();
        //如果这次请求时间小于当前时间,说明之前的请求时间过早,加上这次的平均花费时间小于当前时间,则通过,并设置这次的请求时间为当前时间
        if (expectedTime <= currentTime) {
            this.latestPassedTime.set(currentTime);
            return true;
        } else {
            //如果本次请求预计花费时间>当前时间,则设置等待时间
            //等待时间=平均花费时间+上次请求时间-当前时间,就是超过平均速度了,要等一会
            long waitTime = costTime + this.latestPassedTime.get() - TimeUtil.currentTimeMillis();
            //如果大于最大等待时间,则返回false
            if (waitTime >= (long)this.maxQueueingTimeMs) {
                return false;
            } else {
                //没超过则再获取一次上次请求时间+平均花费时间
                long oldTime = this.latestPassedTime.addAndGet(costTime);

                try {
                    //再设置一次等待时间,因为有可能高并发
                    waitTime = oldTime - TimeUtil.currentTimeMillis();
                    if (waitTime >= (long)this.maxQueueingTimeMs) {
                        this.latestPassedTime.addAndGet(-costTime);
                        return false;
                    } else {
                        //睡对应时间后放行
                        Thread.sleep(waitTime);
                        return true;
                    }
                } catch (InterruptedException var15) {
                    return false;
                }
            }
        }
    }

(三)WarmUpController(令牌桶)

   public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        //获取通过数量
        long passQps = node.passQps();
        //获取上一个滑动窗口通过数量
        long previousQps = node.previousPassQps();
        //置令牌数量
        this.syncToken(previousQps);
        //获取当前令牌数量
        long restToken = this.storedTokens.get();
        //如果当前数量 大于 预警令牌数量,说明还处于冷启动状态
        if (restToken >= (long)this.warningToken) {
            long aboveToken = restToken - (long)this.warningToken;
            //计算可通过数量
            double warningQps = Math.nextUp(1.0D / ((double)aboveToken * this.slope + 1.0D / this.count));
            if ((double)(passQps + (long)acquireCount) <= warningQps) {
                return true;
            }
        }
        //如果小于,则不超过阀值即可
         else if ((double)(passQps + (long)acquireCount) <= this.count) {
            return true;
        }

        return false;
    }
    
    //置令牌数量方法
    protected void syncToken(long passQps) {
        //获取当前时间
        long currentTime = TimeUtil.currentTimeMillis();
        //精确到秒
        currentTime -= currentTime % 1000L;
        //上一次填充时间
        long oldLastFillTime = this.lastFilledTime.get();
        //如果不在同一时间段
        if (currentTime > oldLastFillTime) {
            //获取当前令牌数量
            long oldValue = this.storedTokens.get();
            //计算新数量
            long newValue = this.coolDownTokens(currentTime, passQps);
            //填充补充的令牌
            if (this.storedTokens.compareAndSet(oldValue, newValue)) {
                //减去上一时间段通过的数量
                long currentValue = this.storedTokens.addAndGet(0L - passQps);
                if (currentValue < 0L) {
                    this.storedTokens.set(0L);
                }

                this.lastFilledTime.set(currentTime);
            }

        }
    }

    private long coolDownTokens(long currentTime, long passQps) {
        //获取上一个时间段令牌数量
        long oldValue = this.storedTokens.get();
        long newValue = oldValue;
        //如果当前桶中的数量小于预警令牌,说明一时间请求的数量过大,补充新的令牌数量
        //比如count设置为10,预热时间为2分钟,则warningToken = 10,maxToken =20
        //假设上一秒请求18个,还剩oldValue=2,那么这一秒补充2+(1)*10=12 个
        if (oldValue < (long)this.warningToken) {
            newValue = (long)((double)oldValue + (double)(currentTime - this.lastFilledTime.get()) * this.count / 1000.0D);
        }
        //如果上一秒请求数不多,剩余令牌数量大于预警令牌数,且前一次请求数小于阀值除以冷却系数,则直接补满    
        else if (oldValue > (long)this.warningToken && passQps < (long)((int)this.count / this.coldFactor)) {
            newValue = (long)((double)oldValue + (double)(currentTime - this.lastFilledTime.get()) * this.count / 1000.0D);
        }
        //如果上一秒请求数不多,剩余令牌数量大于预警令牌数,且前一次请求数大于阀值除以冷却系数,则不补充。还是oldValue。
        return Math.min(newValue, (long)this.maxToken);
    }

 附个请求示例方便理解:

猜你喜欢

转载自blog.csdn.net/yytree123/article/details/108570415