Alibaba Sentinel 限流与滑动时间窗口

之前发过一篇文章,介绍了alibaba Sentinel限流功能。Alibaba Sentinel限流功能
限流依赖的基础就是一个基于滑动时间窗口的计数器。

固定时间窗口

介绍滑动时间窗口前,先简单介绍下固定时间窗口,见下图

固定时间窗口

以统计QPS为例,我们可以将时间按照固定间隔进行切分,比如1000ms(一秒),统计每一个时间窗口内的计数,然后得出QPS,这是一种最简单的统计方式。
那么这种方式的缺点是什么呢?

临界流量问题

比如我们定义了规则,QPS不能超过一万,如果在900ms和1100 ms分别进来一万流量,显然是满足流控限制的,但实际上,这个流量已经是两万QPS了。我们的流控限制在这种固定时间窗口下,起不到应有的限流作用,会导致服务过载。

滑动时间窗口

为了规避这个问题,滑动时间窗口将时间切分为多个窗口(一般是两个),窗口指针随着时间往后滑动,见下图

滑动时间窗口

如图,滑动时间窗口将每秒钟时间(1000ms)切分为2个窗口,W1永远指向前半秒(前500ms),w2永远指向后半秒(后500ms),初始时,W1指向[0,500)ms区间,W2指向[500,1000)ms区间,当时间往后走到1001ms时,w1会去指向[1000,1500)ms区间。W2同理,会不断的替换为新的区间,以实现窗口滑动。用W1+W2两个区间的计数之和进行QPS计算,这样就解决了固定时间窗口下的临界流量问题。

Alibaba Sentinel代码实现

Sentinel实现滑动时间窗口,基于的是类OccupiableBucketLeapArray,其特殊的点就是,这个数据结构除了持有两个正常的时间窗口之外,还持有一个完全相同结构的borrowArray,其中包含两个未来的时间窗口。后续将介绍这个特殊结构的作用。
其主要逻辑在父类LeapArray中,实现时间窗口初始化,获取,滑动。
根据当前系统时间戳,去获取归属的时间窗口,主要逻辑包含下述注释中的三步
1.新建时间窗口 2.命中时间窗口 3.时间窗口起始值更新

             WindowWrap<T> old = array.get(idx);
            //1.如果老的时间窗口不存在,则新建新的时间窗口,通过cas的方式进行替换
            if (old == null) {
                /*
                 *     B0       B1      B2    NULL      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            bucket is empty, so create new and update
                 *
                 * If the old bucket is absent, then we create a new bucket at {@code windowStart},
                 * then try to update circular array via a CAS operation. Only one thread can
                 * succeed to update, while other threads yield its time slice.
                 */
                WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                if (array.compareAndSet(idx, null, window)) {
                    // Successfully updated, return the created bucket.
                    return window;
                } else {
                    // Contention failed, the thread will yield its time slice to wait for bucket available.
                    Thread.yield();
                }
            //2.如果时间戳在窗口之内,则直接返回
            } else if (windowStart == old.windowStart()) {
                /*
                 *     B0       B1      B2     B3      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            startTime of Bucket 3: 800, so it's up-to-date
                 *
                 * If current {@code windowStart} is equal to the start timestamp of old bucket,
                 * that means the time is within the bucket, so directly return the bucket.
                 */
                return old;
            //3.如果时间戳已经大于老窗口,则将老窗口的时间指向新的起始值
            } else if (windowStart > old.windowStart()) {
                /*
                 *   (old)
                 *             B0       B1      B2    NULL      B4
                 * |_______||_______|_______|_______|_______|_______||___
                 * ...    1200     1400    1600    1800    2000    2200  timestamp
                 *                              ^
                 *                           time=1676
                 *          startTime of Bucket 2: 400, deprecated, should be reset
                 *
                 * If the start timestamp of old bucket is behind provided time, that means
                 * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
                 * Note that the reset and clean-up operations are hard to be atomic,
                 * so we need a update lock to guarantee the correctness of bucket update.
                 *
                 * The update lock is conditional (tiny scope) and will take effect only when
                 * bucket is deprecated, so in most cases it won't lead to performance loss.
                 */
                if (updateLock.tryLock()) {
                    try {
                        // Successfully get the update lock, now we reset the bucket.
                        return resetWindowTo(old, windowStart);

基于以上的滑动时间窗口,限流的具体过程见注释1,2,3

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        //1.计算当前窗口计数之和
        int curCount = avgUsedTokens(node);
        //2.比较当前流量与规则限制
        if (curCount + acquireCount > count) {
            //3.即使超限,如果prioritized设为true,则认为是重要业务,可以尝试让业务线程sleep到下一个窗口,借用下一个窗口的计数
            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);

                    // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                    throw new PriorityWaitException(waitInMs);
                }
            }
            return false;
        }
        return true;
    }

前两个步骤都很好理解,如果定义了限流规则为一万QPS,当流量超限,不让通过即可,不允许访问服务。
可是如果是重要业务,超限了直接失败显然不行,Sentinel除了上图的两个window,还特意引入了一个包含两个未来时间窗口的borrowArray,先借用未来的计数,给与业务通过,同时让业务线程sleep一段时间,去落在新窗口上,而且当时间滑动到新的窗口时,也不用新建一个空计数的window,直接使用这个borrowArray中window的计数。这也是前文提到OccupiableBucketLeapArray特殊数据结构的作用。

 public MetricBucket newEmptyBucket(long time) {
        MetricBucket newBucket = new MetricBucket();

        MetricBucket borrowBucket = borrowArray.getWindowValue(time);
        if (borrowBucket != null) {
            newBucket.reset(borrowBucket);
        }

        return newBucket;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345