Sentinel流控功能核心源码浅析

Sentinel流控功能核心源码浅析

背景说明

Sentinel是阿里开源的一个面向分布式服务架构的轻量级高可用流量控制组件。关于Sentinel我之前整理了一篇简单介绍可供参考:阿里开源Sentinel流控框架基本介绍与简单使用

本文主要通过对源码加以注释的方式,来简单的讲解Sentinel的代码逻辑,目的在于梳理其核心的功能代码结构,便于后续对相关问题的解决处理,也希望通过阅读源码来加强编码技能。

由于整个Sentinel开源项目下的子项目众多,这里我们仅针对实现核心流控功能的sentinel-core项目来分析。

源码clone自阿里的github开源库:https://github.com/alibaba/Sentinel ,使用的是RELEASEv1.7.0版本分支,过程中参考了项目中的官方文档及其他相关文章。

流控入口

我们在使用Sentinel判断请求是否通过限流时,需要使用SphU.entry()SphO.entry()来获取令牌(Entry),而通过源码中的方法调用可以发现这两种方式最终执行的都是CtSph.entryWithPriority()方法:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException {
    // 获取当前的请求上下文,并进行相关的校验
    Context context = ContextUtil.getContext();
    ...

    // 构建SlotChain
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }
	
    // 构建Entry(令牌)
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // 执行SlotChain中的entry方法
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        // 流控检查失败,拒绝此次请求
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    // 检查通过,返回令牌对象
    return e;
}

可以看到,此处主要是构建SlotChain并执行其entry()方法,而在捕获到流控拒绝异常时会进行以下方法调用:

CtEntry.exit();

┆┈ CtEntry.trueExit();

┆┈┈┈ CtEntry.exitForContext();

最终执行SlotChain中的exit()方法。

Sentinel的工作流程就是围绕着一个个插槽-Slot所组成的插槽链-SlotChain来展开的。每个插槽都有自己的功能,通过一定的编排顺序,来达到最终的限流降级的目的。

SlotChain的构建

SlotChain的构建过程:

CtSph.lookProcessChain();

┆┈ SlotChainProvider.newSlotChain();

┆┈┈┈ SlotChainBuilder.build();

SlotChainBuilder是一个接口,我们可以自行加入自定义的 Slot 并编排 Slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。

Sentinel中默认使用DefaultSlotChainBuilder来构建Slot链:

@Override
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 AuthoritySlot());
    chain.addLast(new SystemSlot());
    chain.addLast(new FlowSlot());
    chain.addLast(new DegradeSlot());
    return chain;
}

这些Slot的功能分别是:

Slot类 功能职责
NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级
ClusterBuilderSlot 存储资源的统计信息以及调用来源信息,例如该资源的 RT, QPS, block 数目、线程数、异常数等,这些信息将用作为多维度限流,降级的依据
StatistcSlot 记录,统计不同纬度的实时调用信息
FlowSlot 根据预设的限流规则,以及前面 slot 统计的状态,来进行限流
AuthorizationSlot 根据黑白名单,来做黑白名单控制
DegradeSlot 通过统计信息,以及预设的规则,来做熔断降级
SystemSlot 根据系统的状态,来控制总的入口流量

Sentinel中预设的SlotChain执行的完整流程:
Sentinel流控功能核心源码浅析_第1张图片


下面针对整个链路中功能性最强也最重要的两个Slot:StatisticSlotFlowSlot来进行详细分析

StatisticSlot

以上最重要的一个Slot非StatisticSlot莫属,因为其他Slot实现限流,熔断等功能,都是基于StatisticSlot统计出来的结果进行规则校验的。

entry方法:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
    try {
        // 检查是否允许此次请求(如果能通过后续Slot的entry方法,说明没有被限流或降级)
        fireEntry(context, resourceWrapper, node, count, prioritized, args);

        // 更新节点统计数据:增加线程数,增加pass请求数
        node.increaseThreadNum();
        node.addPassRequest(count);

        ...	// 更新其他Node节点统计数据、执行entry事件通知回调

    } catch (PriorityWaitException ex) {	// 优先请求等待异常(后面DefaultController中会讲)
        // 更新节点统计数据:增加线程数
        node.increaseThreadNum();

        ... // 更新其他Node节点统计数据、执行entry事件通知回调

    } catch (BlockException e) {	// 后续Slot阻止此次请求
        // 设置错误状态
        context.getCurEntry().setError(e);
        // 更新节点统计数据:增加block Qps
        node.increaseBlockQps(count);

        ...	// 更新其他Node节点统计数据、执行entry事件通知回调

        throw e;
    } catch (Throwable e) {
        // 设置错误状态
        context.getCurEntry().setError(e);
        // 更新节点统计数据:增加异常Qps
        node.increaseExceptionQps(count);

        ...	// 更新其他Node节点统计数据

        throw e;
    }
}

exit方法:

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    DefaultNode node = (DefaultNode)context.getCurNode();

    if (context.getCurEntry().getError() == null) {
        // 计算相应时间
        long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
        int maxStatisticRt = SentinelConfig.statisticMaxRt();
        if (rt > maxStatisticRt) {
            rt = maxStatisticRt;
        }

        // 更新节点统计数据:记录响应时间,增加success请求数
        node.addRtAndSuccess(rt, count);

        // 更新节点统计数据:减少线程数
        node.decreaseThreadNum();     
    } else {
    }

    // 执行exit事件通知回调
    Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
    for (ProcessorSlotExitCallback handler : exitCallbacks) {
        handler.onExit(context, resourceWrapper, count, args);
    }

    // 调用后续Slot的exit方法
    fireExit(context, resourceWrapper, count);
}

简单总结下StatisticSlot中的逻辑,其实就是在entry方法中增加统计数据中的线程数及请求通过计数,如果因后续流控拒绝或其他原因导致此次请求检查异常,则增加相应状态下的错误计数,而在整个流程执行完毕,进入exit方法时,则扣减相应的流量统计数据。

这些统计数据都保存在不同的Node中,供后续Slot执行流控判断使用。

FlowSlot

Sentinel中最基本的功能就是流量控制,而这部分的逻辑主要就是在FlowSlot中实现的

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
    // 检查流量
    checkFlow(resourceWrapper, context, node, count, prioritized);
    // 调用Slot的entry方法
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    // 调用FlowRuleChecker.checkFlow()来进行限流检查
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

后面会在FlowRuleChecker中执行一些判断逻辑,代码调用流程:

FlowSlot.checkFlow();

┆┈ FlowRuleChecker.checkFlow(); // 遍历此Resource下加载的所有Rule

┆┈┈┈ FlowRuleChecker.canPassCheck(); // 判断是集群模式还是单机模式(这里以单机模式为例)

┆┈┈┈┈┈FlowRuleChecker.passLocalCheck();

┆┈┈┈┈┈┈┈TrafficShapingController.canPass();

这里的TrafficShapingController为流控算法接口,其实现类在com.alibaba.csp.sentinel.slots.block.flow.controller包下。

Sentinel初始化加载所有Rule配置时,会由FlowRuleUtil.generateRater()根据配置Rule时指定的controlBehavior(流量控制方式)来决定具体使用哪种流控算法,其对应关系:

流控方式配置-controlBehavior 说明 对应的算法实现类
RuleConstant.CONTROL_BEHAVIOR_DEFAULT 直接拒绝 DefaultController
RuleConstant.CONTROL_BEHAVIOR_WARM_UP 预热/冷启动方式 WarmUpController
RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER 均速排队 RateLimiterController
RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER 上面两种方式结合 WarmUpRateLimiterController

(1)DefaultController

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // 计算已使用令牌数
    int curCount = avgUsedTokens(node);
    // 判断:通过此次请求是否会超过流控阈值
    if (curCount + acquireCount > count) {	//此处的count为Rule中设定的阈值
        // 如果此次请求为优先请求且限流类型为QPS,则尝试占用下一时间区间的令牌数
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            // 计算满足条件的时间区间,等待一定时间后通过此次请求
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                node.addOccupiedPass(acquireCount);
                sleep(waitInMs);

                // 通过异常的方式将此次请求抛出,在StatisticSlot中捕获并执行请求通过的统计流程
                throw new PriorityWaitException(waitInMs);
            }
        }
        return false;
    }
    return true;
}

private int avgUsedTokens(Node node) {
    if (node == null) {
        return DEFAULT_AVG_USED_TOKENS;
    }
    return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}

作为默认的流控算法,DefaultController中的逻辑很简单

首先在avgUsedTokens() 方法中计算已占用流量(令牌数),如果是线程级别的控制,则直接返回统计节点中保存的当前线程数,如果是QPS级别的控制,则返回已通过的QPS数。

然后将已占用的令牌数与此次请求需要的令牌数相加,如果没有超过Rule中设定的阈值则可直接放行。

如果超过了阈值,这里还有一个在QPS级别下针对优先请求(prioritized request)的特殊处理,会将此次请求尝试去占用后续的时间窗口中的令牌数,并等待相应的时间,以保证优先请求尽可能通过。

而不满足上述条件的则返回false,标识请求令牌失败,不予放行此次请求。

(2)RateLimiterController

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    ... // 简单的count校验

    long currentTime = TimeUtil.currentTimeMillis();
    // 计算每两个令牌请求之间的间隔(比如QPS限制为10,那么间隔就是100ms),得出此次请求所需时间
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);

    // 根据上次请求通过时间计算本次请求预计通过的时间点
    long expectedTime = costTime + latestPassedTime.get();

    if (expectedTime <= currentTime) {
        // 通过,设置上次请求通过时间
        latestPassedTime.set(currentTime);
        return true;
    } else {
        // 不可以通过,并计算需要等待的时间
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
        if (waitTime > maxQueueingTimeMs) {
            return false;
        } else {
            long oldTime = latestPassedTime.addAndGet(costTime);
            try {
                // 再次计算等待时间并判断
                waitTime = oldTime - TimeUtil.currentTimeMillis();
                if (waitTime > maxQueueingTimeMs) {
                    latestPassedTime.addAndGet(-costTime);
                    return false;
                }
                // 满足时间条件,sleep后判定通过
                if (waitTime > 0) {
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
            }
        }
    }
    return false;
}

Sentinel中的RateLimiterController是参考了Guava RateLimiter设计的,实现也比较简单。

整个算法中并没有使用到Node中保存的统计数据,首先根据此次请求的的令牌数acquireCount,预估在设定的QPS限制下的所需耗时costTime,然后加上latestPassedTime(上次请求通过时间),得出预计此次请求通过的时间点expectedTime

如果此时间点在当前时间之前,说明当前时间窗口可以容纳此次请求通过,则放行此次请求,并将当前时间设置为latestPassedTime。如果expectedTime在当前时间之后,说明按照目前的请求速率,当前时间窗口无法满足此次请求,需要延后,计算所需等待时间,满足一定限制则可在sleep后放行通过。

(3)WarmUpController

// 初始构造方法
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
    if (coldFactor <= 1) {
        throw new IllegalArgumentException("Cold factor should be larger than 1");
    }
    this.count = count;

    this.coldFactor = coldFactor;
	// 计算告警令牌数
    warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
	// 计算最大令牌数
    maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));

    // 计算斜率
    slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    long passQps = (long) node.passQps();
    long previousQps = (long) node.previousPassQps();
    // 重新计算令牌桶内令牌数
    syncToken(previousQps);

    // 如果进入了警戒线,开始调整他的qps
    long restToken = storedTokens.get();
    if (restToken >= warningToken) {
        long aboveToken = restToken - warningToken;
		// 根据斜率计算告警令牌数
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
        // 低于警戒线,放行
        if (passQps + acquireCount <= warningQps) {
            return true;
        }
    } else {
        // 低于最大阈值,放行
        if (passQps + acquireCount <= count) {
            return true;
        }
    }
    return false;
}

预热算法是将请求随着时间推移,慢慢增加可通过请求量,防止系统在短时间内受到大的流量冲击,保护系统稳定,防止出现过载等情形。Sentinel中WarmUpController的实现参考了Guava的预热及令牌桶算法。

这里涉及到两个参数:

  1. coldFactor:在sentinel.properties文件中配置csp.sentinel.flow.cold.factor,默认值为3
  2. warmUpPeriodInSec:通过Rule配置,默认值为10

使用warm up流控算法后的QPS流量趋势可参考下图(图片截取自阿里云):

Sentinel流控功能核心源码浅析_第2张图片

结合源码来分析:

  1. 当流量在某个时间点开始突增时,在warningToken以下的可以直接放行,就形成了图中第一个红色方框后的近乎垂直的走向。
  2. 然后当流量大于warningToken时,QPS以曲线形态逐渐提升,这个过程中,slope就是曲线的斜率,而warningTokenslope也是不断的重新计算得来,因此可以看到曲线的斜率越来越大。
  3. 当经过这段预热时间,最终QPS不再提升,达到设定的count限制。

关于WarmUpController可参考此文:Sentinel之Slots插槽源码分析流控规则(五),其中对warm up算法讲的比较详细

(4)WarmUpRateLimiterController

WarmUpRateLimiterController类继承了WarmUpController,其流控算法是将匀速排队和预热相结合,可结合上面介绍的两种算法去源码中查看,这里不再具体分析。

总结

以上源码分析的内容总结起来主要有两点:

  1. 在使用Sentinel时,其核心的流控功能是如何运转的,重要的类及其调用流程都有哪些;
  2. Sentinel最终实现流控规则算法的4个Controller的简单介绍和分析。

在阅读Sentinel的源码后,我发现,相比国外的开源项目(比如同类型的Hystrix),作为国内开源项目的Sentinel整体代码结构比较简单和清晰,不存在过多的接口抽象和继承,因此在阅读时也比较容易找到调用流程主线,功能的实现也比较直接,相对来说还是比较好懂的。如果大家还想了解的更多的源码细节,比如控制台、集群模式下的限流、对RPC的支持等内容,建议可以下载源码来深入学习。


我的个人理解可能存在不足,文笔都有限,欢迎各位提出宝贵的意见和建议,期待和大家讨论交流,共同进步。

我的CSDN-未完成的空间

你可能感兴趣的:(sentinel)