Sentinel系统自适应限流原理分析

3,492 阅读6分钟

系统自适应限流我觉得在实际的生产环境还是一个比较实用有价值的功能,实际生产环境中,其实我们很难对我们的系统有一个清晰的认识,比如能承载的qps,请求的响应时间等指标。当然你会说我们可以对系统进行压测来获取相应的指标,但这些指标其实跟测试服务机,当时的服务主机环境、负载等因素有关系,所以我们只能对获取到的压测指标有个大概的认识,结合相应的经验来对系统的性能做出一个评估。因此如果你对系统能承载的qps不太清楚,而侧重从系统负载的角度来平衡系统性能的话,通过自适应限流其实是个不错的选择。

系统自适应,“系统”简单说来就是选取几个系统指标,“自适应”就是根据这些指标采用一定的算法来计算出一个结果,再将这个结果和预设的门限值来比较,从而判断是否需要触发限流机制。在Sentinel中选取了如下几个系统指标作为判断标准,这些指标是可以是独立设置的,也可以同时设置:

  • 系统负载load
  • CPU使用率
  • 请求平均响应时间
  • 并发线程数
  • 入口QPS

目前自适应是根据系统负载load来触发的。它借用了TCP BBR的算法思想(各位看官自行脑补),其目的就是要让你的系统在高负载的条件下依然拥有高吞吐量的能力。在Sentinel的实现中,其实就是定时采集系统的负载load的情况,如果load值高于设定的阈值,就会触发BBR的校验,后面会详细介绍。自适应限流针对的是入口流量,即资源的entryType为EntryType.IN。其实这个类型的设置并没有限制,你可以在任何资源上使用,当然最佳实践还是在系统的流量入口处。我在实践中会创建一个拦截器,让所有的流量都通过此拦截器,因此可以在此拦截器中来进行自适应的限流。

上述简单大概的介绍了系统自适应限流的一些情况,如果需要详细的了解相关背景,可以参考官方的文档。下面将进入源码进行详细的分析。系统的自适应限流是在SystemSlot这个类中的entry方法类完成的

SpiOrder(-5000)
public class SystemSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        //系统规则校验
        SystemRuleManager.checkSystem(resourceWrapper);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }

}

进入SystemRuleManager.checkSystem方法:

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
        if (resourceWrapper == null) {
            return;
        }
        //系统全局检测开关是否开启
        if (!checkSystemStatus.get()) {
            return;
        }

        // 判断是否为入口流量
        if (resourceWrapper.getEntryType() != EntryType.IN) {
            return;
        }

        // 获取当前总的qps
        double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
        //如果大于了设置了系统qps,则抛出异常
        if (currentQps > qps) {
            throw new SystemBlockException(resourceWrapper.getName(), "qps");
        }

        //如果当前系统的线程数大于设置值,则抛出异常
        int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
        if (currentThread > maxThread) {
            throw new SystemBlockException(resourceWrapper.getName(), "thread");
        }
		//如果当前系统的平均响应时间大于设置值,则抛出异常
        double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
        if (rt > maxRt) {
            throw new SystemBlockException(resourceWrapper.getName(), "rt");
        }

        // 获取当前系统负载,如果当前负载大于设置值,则会根据BBR算法来进一步判断是否需要限流
        if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
            if (!checkBbr(currentThread)) {
                throw new SystemBlockException(resourceWrapper.getName(), "load");
            }
        }

        //如果当前系统cpu使用值大于设置值,则抛出异常
        if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
            throw new SystemBlockException(resourceWrapper.getName(), "cpu");
        }
    }

BBR的校验是在checkBbr方法中完成,代码其实很简单:

    private static boolean checkBbr(int currentThread) {
        if (currentThread > 1 &&
            currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) {
            return false;
        }
        return true;
    }

从上面的代码中其实可以看出,Sentinel是通过系统的最大qps与最小响应时间的乘积与当前的线程数来做比较。至于为什么这样做,可以参考官方的文档。这里从公式本身出发来解释下这个关系。我们假设maxQps = 100,minRt = 10ms,如果系统以这个指标来运行的话,每秒需要的线程数就是他们的乘积 10 = 100 * 10 / 1000,这个结果代表着系统的最大吞吐量。如果当前运行的线程超过了此值,则表明系统达到了“极限”。 系统的负载和相关的性能指标其实一直在变化的,Sentinel中是通过一个定时任务间隔1s采集系统的相关指标。代码中定义了SystemStatusListener这样一个类,它实现了Runable接口,负责采集系统的指标:

private static SystemStatusListener statusListener = null;
private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
        new NamedThreadFactory("sentinel-system-status-record-task", true));
static {
        statusListener = new SystemStatusListener();
        //当前类创建的时候就会采集系统指标,间隔1s
        scheduler.scheduleAtFixedRate(statusListener, 0, 1, TimeUnit.SECONDS);
    }

看看SystemStatusListener的run方法,可以知道究竟采集了那些指标,这些指标的获取实际上依赖于JMX提供的接口。

@Override
    public void run() {
        try {
           //获取操作系统相关信息
            OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
            currentLoad = osBean.getSystemLoadAverage();

         	//获取系统负载
            double systemCpuUsage = osBean.getSystemCpuLoad();
			//获取jvm相关信息
            RuntimeMXBean runtimeBean = ManagementFactory.getPlatformMXBean(RuntimeMXBean.class);
            //jvm进程占用cpu的时间
            long newProcessCpuTime = osBean.getProcessCpuTime();
            //jvm启动时间
            long newProcessUpTime = runtimeBean.getUptime();
            //可用处理器数量
            int cpuCores = osBean.getAvailableProcessors();
            //两次采集间隔间,jvm占用的cpu时间
            long processCpuTimeDiffInMs = TimeUnit.NANOSECONDS
                    .toMillis(newProcessCpuTime - processCpuTime);
            //两次采集间隔间,jvm运行时间
            long processUpTimeDiffInMs = newProcessUpTime - processUpTime;
            //获取jvm进程的cup使用率
            double processCpuUsage = (double) processCpuTimeDiffInMs / processUpTimeDiffInMs / cpuCores;
            processCpuTime = newProcessCpuTime;
            processUpTime = newProcessUpTime;
			//取系统和jvm cpu使用率中的较大者
            currentCpuUsage = Math.max(processCpuUsage, systemCpuUsage);

        } catch (Throwable e) {
            RecordLog.warn("[SystemStatusListener] Failed to get system metrics from JMX", e);
        }
    }

从上面的代码可以看出,Sentinel获取系统的指标只有两个,系统负载和cup的使用率。这里大家可能会有个疑问,在获取系统负载的时候,取的是系统和jvm中的较大者。按常理来说的话,jvm只是系统中一个进程,系统的负载应该是大于jvm的负载才对,为什么不直接获取系统的负载就行了呢?其实原因在于,我们的系统指标是离散采集的,有可能在采集的那一时刻,系统的负载刚好比较低,而期间可能会存在高负载的情况,这是获取到的值就没有代表性,所以采用了取两者中的较大者,具有更好的代表性。

上面的代码就是整个自适应限流的整个过程,自适应的关键就在于当系统的负载超过了设置的阈值时,与进行一个BBR的校验。