Alibaba Sentinel 限流、熔断实现详解

前言

这篇文章是 Alibaba Sentinel熔断降级/限流框架不完全解析 的附属文章。

讲的是其限流、熔断的实现。本文主要讲算法实现, 跟Sentinel关系不大, 无需对Sentinel加以了解

本文会辅以部分源码解析, 源码在git 上: https://github.com/alibaba/Sentinel

限流

理解参阅文:https://www.cnblogs.com/taromilk/p/11877242.html

快速失败限流(普通限流 DefaultController)

快速失败很简单, 超过了阈值就抛出异常。
主要依赖时间窗口。
对应实现类: com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController

原理主要需要了解滑动窗口

滑动窗口

写了两页PPT, 直接看图就好了:
滑动窗口设计
滑动窗口的滑动:
滑动窗口
于是, 当前定义时间段(秒级)就由秒级时间段的多个窗口的每个窗口的QPS之和累计即可

热加载限流(WarnUp)

一般会对比Guava的热加载限流, 确实Sentinel的实现上有参考的影子, 但是它自身的实现还是不一样的(可分别读两者, 没必然关联
这里的实现非常巧妙, 纯阅读源码会被带坑里去, 花了我好久的时间
参阅源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController

热加载用的是令牌桶的形式, 其中一个牛逼的地方在于:

  1. 系统启动的时候能支持热加载;
  2. 系统运行中, 如果有低负载期,也会有热加载流程

流量波动期

Sentinel的令牌桶主要参照令牌桶中的**剩余令牌数**概念, 有这么几个重点:

  1. 令牌桶不能空, 空了就没法取令牌了, 需要有个警戒值
  2. 桶满的时候,为系统最低负载期(冷了很久, 或者刚启动)
  3. 剩余令牌未到警戒值, 系统处于热加载阶段, 可以算当前时间点的令牌数
  4. 剩余令牌超过警戒值, 系统热加载完毕,需要及时添加令牌保证桶不空

于是问题简化为判断当前剩余令牌数与警戒值的关系。
下面直接进行源码分析

Sentinel给令牌桶添加令牌(com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController):

    protected void syncToken(long passQps) {
        long currentTime = TimeUtil.currentTimeMillis();
        // 当前时间都是取整的
        currentTime = currentTime - currentTime % 1000;
        long oldLastFillTime = lastFilledTime.get();
        // 单位秒只添加一次令牌(同时意味着热加载时期, 单位秒的限流大小是固定的)
        if (currentTime <= oldLastFillTime) {
            return;
        }

        long oldValue = storedTokens.get();
        // 判断 添加多少个令牌 ↓↓↓
        long newValue = coolDownTokens(currentTime, passQps);

        // 为并发做准备
        if (storedTokens.compareAndSet(oldValue, newValue)) {
            // 当前剩余令牌数减去上一秒的令牌数, 然后再参与计算当前时间的令牌数(这个值在后续作为分母, 减掉部分之后分数才会变大)
            long currentValue = storedTokens.addAndGet(0 - passQps);
            if (currentValue < 0) {
                storedTokens.set(0L);
            }
            lastFilledTime.set(currentTime);
        }
    }

添加多少个令牌:

    private long coolDownTokens(long currentTime, long passQps) {
        long oldValue = storedTokens.get();
        // 大部分 热加载 时期, 这个值(剩余令牌数)就是下一秒参与计算的令牌数。
        long newValue = oldValue;

        // 剩余令牌数小于警戒值, 需要加令牌。
        if (oldValue < warningToken) {
            // 添加的个数就是  现在到上次添加令牌数的时间差, 每秒添加限流count个令牌。
            // 这儿的时间差好像没什么必要, 毕竟只要小于警戒值就会加。 每次消耗的量一定低于count数(大于了就不是限流了)
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        } else if (oldValue > warningToken) {
            // 这个地方的目的, 个人理解就是QPS过小的时候, 必须让它一直处于热加载期(一直添加, 就会一直大于警戒值)
            if (passQps < (int)count / coldFactor) {
                newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
            }
        }
        // 大部分热加载时期, 初始剩余令牌数都会被一直减到达到热加载完毕为止。
        // 然后才会逐步添加
        return Math.min(newValue, maxToken);
    }

上述是Sentinel加令牌的策略, 但是热加载时期, 每个时刻限定的QPS还需要**单独算**。
这个跟上述加令牌的逻辑是独立的, 看各人的实现

Sentinel的 算法也很简单, 热加载有这么几个初始值:

        // 限流数目
        this.count = count;
        // 冷加载因子, 这个值参与得到 警戒值和斜率 的计算, 固定值3
        this.coldFactor = coldFactor;
        // 警戒值, 限流大小 * 热加载时间 / 2。
        warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
        // 最大令牌数, 两倍于 警戒值
        maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
        // 斜率
        slope = (coldFactor - 1.0) / count / (maxToken - warningToken);

获取当前时间点的可允许通过的QPS:

// 此处的aboveToken是 令牌桶中  剩余令牌数 比 警戒值 多的部分。  在热加载时期, 剩余令牌数  一直在减少。
long aboveToken = restToken - warningToken;

// 这个算当前可允许通过的QPS的算法, 是Sentinel家的实现, 没家都可以有自家的实现的。
// 例如Sentinel约定的初始值是 1/2 * count;
// 热加载期可允许通过的QPS有上一秒通过了的QPS的运算参与
Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));

排队限流(RateLimiterController)

一般会对比Guava的热加载限流, 确实Sentinel的实现上有参考的影子, 但是它自身的实现还是不一样的(可分别读两者, 没必然关联
这个的实现就简单易懂多了, 直接源码解析。
参阅源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

排队限流原则:

  1. QPS count 按秒切分为N个单位, 每个单位:1000(ms) / count
  2. 每个单位只让一个QPS通过
  3. 通不过,预期时间能通过, sleep, 就像漏桶一样屯着
  4. 通不过,预期时间通不过, fail
  5. 并发流量中每个并发都是可以参与到每个单位的抢占的。 不过并发多了, 可能导致限流不准

排队限流的源码解析

    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        // 默认值, 1
        if (acquireCount <= 0) {
            return true;
        }
        // 漏桶可以直接限成0
        if (count <= 0) {
            return false;
        }

        long currentTime = TimeUtil.currentTimeMillis();
        // 预计花费时间就是一个时间单位
        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 {
                    // 得到抢占后的时间(可能是好多并发时间一起的)与当前时间的时间差。 如果这个时间差还比预计等待时间差大
                    // 也就是说现在的QPS超量了, 需要放弃。
                    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;
    }

排队限流

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值